Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

attempts to preserve substring indentation #52

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions docstring_parser/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,21 @@ def deprecation(self) -> T.Optional[DocstringDeprecated]:
if isinstance(item, DocstringDeprecated):
return item
return None

def strip_initial_whitespace(string):
"""
Removes initial whitespace up to a newline, if no characters are present.
Different from ''.strip(), since it will preserve indents on lines with text.
Also performs rstrip().
"""
if not string:
return string
lines = string.splitlines()
lines[0] = lines[0].strip()
for i, line in enumerate(lines):
if line and not line.isspace():
break
else:
i = 0
return '\n'.join(lines[i:]).rstrip()

22 changes: 10 additions & 12 deletions docstring_parser/epydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"""
import inspect
import re
import textwrap
import typing as T

from .common import (
Expand All @@ -15,11 +16,12 @@
DocstringStyle,
ParseError,
RenderingStyle,
strip_initial_whitespace
)


def _clean_str(string: str) -> T.Optional[str]:
string = string.strip()
string = strip_initial_whitespace(string)
if len(string) > 0:
return string
return None
Expand Down Expand Up @@ -114,10 +116,7 @@ def parse(text: str) -> Docstring:
'Error parsing meta information near "{}".'.format(chunk)
)

desc = desc_chunk.strip()
if "\n" in desc:
first_line, rest = desc.split("\n", 1)
desc = first_line + "\n" + inspect.cleandoc(rest)
desc = textwrap.dedent(strip_initial_whitespace(desc_chunk))
stream.append((base, key, args, desc))

# Combine type_name, arg_name, and description information
Expand Down Expand Up @@ -210,17 +209,16 @@ def compose(
def process_desc(desc: T.Optional[str], is_type: bool) -> str:
if not desc:
return ""
ret = "\n".join(
[indent + line for line in desc.splitlines()]
)

if rendering_style == RenderingStyle.EXPANDED or (
rendering_style == RenderingStyle.CLEAN and not is_type
):
(first, *rest) = desc.splitlines()
return "\n".join(
["\n" + indent + first] + [indent + line for line in rest]
)

(first, *rest) = desc.splitlines()
return "\n".join([" " + first] + [indent + line for line in rest])
return "\n" + ret
# Skip first indent
return " " + ret[len(indent):]

parts: T.List[str] = []
if docstring.short_description:
Expand Down
19 changes: 13 additions & 6 deletions docstring_parser/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import inspect
import re
import textwrap
import typing as T
from collections import OrderedDict, namedtuple
from enum import IntEnum
Expand All @@ -19,6 +20,7 @@
DocstringStyle,
ParseError,
RenderingStyle,
strip_initial_whitespace
)


Expand Down Expand Up @@ -112,11 +114,7 @@ def _build_meta(self, text: str, title: str) -> DocstringMeta:
# Split spec and description
before, desc = text.split(":", 1)
if desc:
desc = desc[1:] if desc[0] == " " else desc
if "\n" in desc:
first_line, rest = desc.split("\n", 1)
desc = first_line + "\n" + inspect.cleandoc(rest)
desc = desc.strip("\n")
desc = textwrap.dedent(strip_initial_whitespace(desc))

return self._build_multi_meta(section, before, desc)

Expand Down Expand Up @@ -277,7 +275,16 @@ def parse(self, text: str) -> Docstring:
c_splits.append((c_matches[j].end(), c_matches[j + 1].start()))
c_splits.append((c_matches[-1].end(), len(chunk)))
for j, (start, end) in enumerate(c_splits):
part = chunk[start:end].strip("\n")
part = chunk[start:end]
# Take indent away from subsequent lines here, since inner scope
# doesn't know about the indent level
lines = []
for line in part.splitlines():
if line.startswith(indent):
line = line[len(indent):]
lines.append(line)
# Remove preliminary indentation
part = '\n'.join(lines)
ret.meta.append(self._build_meta(part, title))

return ret
Expand Down
10 changes: 8 additions & 2 deletions docstring_parser/numpydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import inspect
import itertools
import re
import textwrap
import typing as T

from .common import (
Expand All @@ -17,6 +18,7 @@
DocstringReturns,
DocstringStyle,
RenderingStyle,
strip_initial_whitespace
)


Expand All @@ -27,7 +29,11 @@ def _pairwise(iterable: T.Iterable, end=None) -> T.Iterable:


def _clean_str(string: str) -> T.Optional[str]:
string = string.strip()
# strip_initial_whitespace normally preserves indentation
# if a leading \n is present, but this shouldn't happen
# with numpy
string = textwrap.dedent(string.strip('\n'))
string = strip_initial_whitespace(string)
if len(string) > 0:
return string
return None
Expand Down Expand Up @@ -98,7 +104,7 @@ def parse(self, text: str) -> T.Iterable[DocstringMeta]:
end = next_match.start() if next_match is not None else None
value = text[start:end]
yield self._parse_item(
key=match.group(), value=inspect.cleandoc(value)
key=match.group(), value=value
)


Expand Down
7 changes: 3 additions & 4 deletions docstring_parser/rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import inspect
import re
import textwrap
import typing as T

from .common import (
Expand All @@ -19,6 +20,7 @@
DocstringStyle,
ParseError,
RenderingStyle,
strip_initial_whitespace
)


Expand Down Expand Up @@ -138,10 +140,7 @@ def parse(text: str) -> Docstring:
'Error parsing meta information near "{}".'.format(chunk)
) from ex
args = args_chunk.split()
desc = desc_chunk.strip()
if "\n" in desc:
first_line, rest = desc.split("\n", 1)
desc = first_line + "\n" + inspect.cleandoc(rest)
desc = textwrap.dedent(strip_initial_whitespace(desc_chunk))

ret.meta.append(_build_meta(args, desc))

Expand Down
13 changes: 6 additions & 7 deletions docstring_parser/tests/test_epydoc.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def test_meta_with_multiline_description() -> None:
assert docstring.short_description == "Short description"
assert len(docstring.meta) == 1
assert docstring.meta[0].args == ["meta"]
assert docstring.meta[0].description == "asd\n1\n 2\n3"
assert docstring.meta[0].description == "asd\n 1\n 2\n 3"


def test_multiple_meta() -> None:
Expand All @@ -227,7 +227,7 @@ def test_multiple_meta() -> None:
assert docstring.short_description == "Short description"
assert len(docstring.meta) == 3
assert docstring.meta[0].args == ["meta1"]
assert docstring.meta[0].description == "asd\n1\n 2\n3"
assert docstring.meta[0].description == "asd\n 1\n 2\n 3"
assert docstring.meta[1].args == ["meta2"]
assert docstring.meta[1].description == "herp"
assert docstring.meta[2].args == ["meta3"]
Expand Down Expand Up @@ -617,7 +617,7 @@ def test_broken_meta() -> None:
)
def test_compose(source: str, expected: str) -> None:
"""Test compose in default mode."""
assert compose(parse(source)) == expected
assert compose(parse(source), indent="") == expected


@pytest.mark.parametrize(
Expand Down Expand Up @@ -654,15 +654,14 @@ def test_compose(source: str, expected: str) -> None:
"@type multiline: str?\n"
"@param multiline:\n"
" long description 5,\n"
" defaults to 'bye'",
" defaults to 'bye'",
),
],
)
def test_compose_clean(source: str, expected: str) -> None:
"""Test compose in clean mode."""
assert (
compose(parse(source), rendering_style=RenderingStyle.CLEAN)
== expected
compose(parse(source), rendering_style=RenderingStyle.CLEAN) == expected
)


Expand Down Expand Up @@ -704,7 +703,7 @@ def test_compose_clean(source: str, expected: str) -> None:
" str?\n"
"@param multiline:\n"
" long description 5,\n"
" defaults to 'bye'",
" defaults to 'bye'",
),
],
)
Expand Down
14 changes: 7 additions & 7 deletions docstring_parser/tests/test_google.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ def test_meta_with_multiline_description() -> None:
assert len(docstring.meta) == 1
assert docstring.meta[0].args == ["param", "spam"]
assert docstring.meta[0].arg_name == "spam"
assert docstring.meta[0].description == "asd\n1\n 2\n3"
assert docstring.meta[0].description == "asd\n 1\n 2\n 3"


def test_default_args():
Expand Down Expand Up @@ -379,7 +379,7 @@ def test_multiple_meta() -> None:
assert len(docstring.meta) == 3
assert docstring.meta[0].args == ["param", "spam"]
assert docstring.meta[0].arg_name == "spam"
assert docstring.meta[0].description == "asd\n1\n 2\n3"
assert docstring.meta[0].description == "asd\n 1\n 2\n 3"
assert docstring.meta[1].args == ["raises", "bla"]
assert docstring.meta[1].type_name == "bla"
assert docstring.meta[1].description == "herp"
Expand Down Expand Up @@ -436,7 +436,7 @@ def test_params() -> None:
assert docstring.params[0].arg_name == "name"
assert docstring.params[0].type_name is None
assert docstring.params[0].description == (
"description 1\nwith multi-line text"
"description 1\n with multi-line text"
)
assert docstring.params[1].arg_name == "priority"
assert docstring.params[1].type_name == "int"
Expand Down Expand Up @@ -491,7 +491,7 @@ def test_attributes() -> None:
assert docstring.params[0].arg_name == "name"
assert docstring.params[0].type_name is None
assert docstring.params[0].description == (
"description 1\nwith multi-line text"
"description 1\n with multi-line text"
)
assert docstring.params[1].arg_name == "priority"
assert docstring.params[1].type_name == "int"
Expand Down Expand Up @@ -873,7 +873,7 @@ def test_empty_example() -> None:
message (str, optional): description 4, defaults to 'hello'
multiline (str?):
long description 5,
defaults to 'bye'
defaults to 'bye'
""",
"Short description\n"
"\n"
Expand Down Expand Up @@ -914,7 +914,7 @@ def test_compose(source: str, expected: str) -> None:
message (str, optional): description 4, defaults to 'hello'
multiline (str?):
long description 5,
defaults to 'bye'
defaults to 'bye'
""",
"Short description\n"
"\n"
Expand Down Expand Up @@ -965,7 +965,7 @@ def test_compose_clean(source: str, expected: str) -> None:
" description 4, defaults to 'hello'\n"
" multiline (str, optional):\n"
" long description 5,\n"
" defaults to 'bye'",
" defaults to 'bye'",
),
],
)
Expand Down
4 changes: 2 additions & 2 deletions docstring_parser/tests/test_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ def test_meta_with_multiline_description() -> None:
assert docstring.short_description == "Short description"
assert len(docstring.meta) == 1
assert docstring.meta[0].args == ["meta"]
assert docstring.meta[0].description == "asd\n1\n 2\n3"
assert docstring.meta[0].description == "asd\n 1\n 2\n 3"


def test_multiple_meta() -> None:
Expand All @@ -227,7 +227,7 @@ def test_multiple_meta() -> None:
assert docstring.short_description == "Short description"
assert len(docstring.meta) == 3
assert docstring.meta[0].args == ["meta1"]
assert docstring.meta[0].description == "asd\n1\n 2\n3"
assert docstring.meta[0].description == "asd\n 1\n 2\n 3"
assert docstring.meta[1].args == ["meta2"]
assert docstring.meta[1].description == "herp"
assert docstring.meta[2].args == ["meta3"]
Expand Down