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

preprocessing numpy types #7690

Merged
merged 51 commits into from Jul 25, 2020
Merged
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
a7bbedf
add a test for the parameter type conversions
keewis Jan 31, 2020
b1f43d2
use textwrap for a normal indentation depth
keewis May 17, 2020
bc25a3d
try to mark literals as such
keewis May 17, 2020
bafb24d
actually apply the type conversion
keewis May 17, 2020
bdea34e
don't treat instance as special
keewis May 17, 2020
bd33b61
update the translations
keewis May 17, 2020
70363c3
flake8
keewis May 17, 2020
e982213
more flake8
keewis May 19, 2020
ace9331
move the numpy type spec parsing function out of NumpyDocstring
keewis May 22, 2020
8ab210f
don't use the obj role if it is not necessary
keewis May 22, 2020
25937f7
move tokenize_type_spec to its own function and add tests for it
keewis May 22, 2020
fc70205
get the type converter function to work, verified by new tests
keewis May 22, 2020
2882c34
fix the expected parameters section to match the current status
keewis May 22, 2020
ad89b1f
replace the hard-coded mapping of translations with a config option
keewis May 29, 2020
e1d7eda
rename the configuration option
keewis May 29, 2020
27733d6
replace the custom role with markup
keewis May 29, 2020
b846db7
emit a warning instead of raising an error
keewis May 29, 2020
ce60b55
properly use sphinx's logger
keewis May 29, 2020
eab4912
update the splitting regexp to handle braces in strings and escaped q…
keewis May 29, 2020
9835f1f
test that braces and quotes in strings work
keewis May 29, 2020
9bfbe25
set a default so translations don't to be specified
keewis May 29, 2020
e3b7e16
move the regexes to top-level
keewis May 29, 2020
20e3600
treat value sets as literals
keewis May 29, 2020
dc8c7ac
update the integration test
keewis May 29, 2020
af6071e
expect a warning instead of an error
keewis May 29, 2020
b0da0e5
remove the default for the default translation
keewis May 29, 2020
37e0251
make invalid value sets a literal to avoid further warnings
keewis May 29, 2020
866c822
move the warnings to token_type
keewis Jun 4, 2020
d177e58
reimplement the value set combination function using collections.deque
keewis Jun 4, 2020
1140f7b
also check type specs without actual types
keewis Jun 4, 2020
26855f9
also test invalid string tokens
keewis Jun 4, 2020
4d0b4f2
add back the trailing whitespace
keewis Jun 7, 2020
fedceb2
move the binary operator "or" to before the newline
keewis Jun 7, 2020
7d8aaf2
remove a debug print
keewis Jul 7, 2020
f4817be
use the format method instead of f-strings
keewis Jul 7, 2020
804df88
use :class: as default role and only fall back to :obj: for singletons
keewis Jul 12, 2020
f30c0cb
rewrite the invalid token_type test to check the warnings
keewis Jul 12, 2020
4fc22cd
use the dedent function imported at module-level
keewis Jul 12, 2020
fc43f49
add back the trailing whitespace
keewis Jul 12, 2020
2b981b6
make sure singletons actually use :obj:
keewis Jul 12, 2020
922054e
replace .format with %-style string interpolation
keewis Jul 12, 2020
660b818
add type hints and location information
keewis Jul 12, 2020
cc8baf6
only transform the types if napoleon_use_param is true
keewis Jul 13, 2020
274d9fe
don't try to generate test cases in code
keewis Jul 14, 2020
9b42560
support pandas-style default spec by postprocessing tokens
keewis Jul 21, 2020
ae35f81
allow mapping to a long name
keewis Jul 21, 2020
9200484
don't provide a empty line number
keewis Jul 25, 2020
530793d
update the link to the official docstring guide
keewis Jul 25, 2020
8feb5f9
mention that the type aliases only work with napoleon_use_param
keewis Jul 25, 2020
6ae1c60
add a section about napoleon_type_aliases to the documentation
keewis Jul 25, 2020
864dd0b
add a comment about default not being a official keyword
keewis Jul 25, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions sphinx/ext/napoleon/__init__.py
Expand Up @@ -41,6 +41,7 @@ class Config:
napoleon_use_param = True
napoleon_use_rtype = True
napoleon_use_keyword = True
napoleon_type_aliases = None
napoleon_custom_sections = None

.. _Google style:
Expand Down Expand Up @@ -236,6 +237,9 @@ def __unicode__(self):

:returns: *bool* -- True if successful, False otherwise

napoleon_type_aliases : :obj:`dict` (Defaults to None)
Add a mapping of strings to string, translating types in numpy style docstrings.
keewis marked this conversation as resolved.
Show resolved Hide resolved

napoleon_custom_sections : :obj:`list` (Defaults to None)
Add a list of custom sections to include, expanding the list of parsed sections.

Expand Down Expand Up @@ -263,6 +267,7 @@ def __unicode__(self):
'napoleon_use_param': (True, 'env'),
'napoleon_use_rtype': (True, 'env'),
'napoleon_use_keyword': (True, 'env'),
'napoleon_type_aliases': (None, 'env'),
'napoleon_custom_sections': (None, 'env')
}

Expand Down
188 changes: 186 additions & 2 deletions sphinx/ext/napoleon/docstring.py
Expand Up @@ -10,6 +10,7 @@
:license: BSD, see LICENSE for details.
"""

import collections
import inspect
import re
from functools import partial
Expand All @@ -18,13 +19,15 @@
from sphinx.application import Sphinx
from sphinx.config import Config as SphinxConfig
from sphinx.ext.napoleon.iterators import modify_iter
from sphinx.locale import _
from sphinx.locale import _, __
from sphinx.util import logging

logger = logging.getLogger(__name__)

if False:
# For type annotation
from typing import Type # for python3.5.1


_directive_regex = re.compile(r'\.\. \S+::')
_google_section_regex = re.compile(r'^(\s|\w)+:\s*$')
_google_typed_arg_regex = re.compile(r'\s*(.+?)\s*\(\s*(.*[^\s]+)\s*\)')
Expand All @@ -33,11 +36,19 @@
_xref_or_code_regex = re.compile(
r'((?::(?:[a-zA-Z0-9]+[\-_+:.])*[a-zA-Z0-9]+:`.+?`)|'
r'(?:``.+``))')
_xref_regex = re.compile(
r'(?::(?:[a-zA-Z0-9]+[\-_+:.])*[a-zA-Z0-9]+:`.+?`)'
)
_bullet_list_regex = re.compile(r'^(\*|\+|\-)(\s+\S|\s*$)')
_enumerated_list_regex = re.compile(
r'^(?P<paren>\()?'
r'(\d+|#|[ivxlcdm]+|[IVXLCDM]+|[a-zA-Z])'
r'(?(paren)\)|\.)(\s+\S|\s*$)')
_token_regex = re.compile(
r"(\sor\s|\sof\s|:\s|,\s|[{]|[}]"
tk0miya marked this conversation as resolved.
Show resolved Hide resolved
r'|"(?:\\"|[^"])*"'
r"|'(?:\\'|[^'])*')"
)


class GoogleDocstring:
Expand Down Expand Up @@ -780,6 +791,163 @@ def _strip_empty(self, lines: List[str]) -> List[str]:
return lines


def _recombine_set_tokens(tokens: List[str]) -> List[str]:
token_queue = collections.deque(tokens)
keywords = ("optional", "default")

def takewhile_set(tokens):
open_braces = 0
previous_token = None
while True:
try:
token = tokens.popleft()
except IndexError:
break

if token == ", ":
tk0miya marked this conversation as resolved.
Show resolved Hide resolved
previous_token = token
continue

if token in keywords:
tokens.appendleft(token)
if previous_token is not None:
tokens.appendleft(previous_token)
break

if previous_token is not None:
yield previous_token
previous_token = None

if token == "{":
open_braces += 1
elif token == "}":
open_braces -= 1

yield token

if open_braces == 0:
break

def combine_set(tokens):
while True:
try:
token = tokens.popleft()
except IndexError:
break

if token == "{":
tokens.appendleft("{")
yield "".join(takewhile_set(tokens))
else:
yield token

return list(combine_set(token_queue))


def _tokenize_type_spec(spec: str) -> List[str]:
def postprocess(item):
if item.startswith("default"):
return [item[:7], item[7:]]
else:
return [item]

tokens = list(
item
for raw_token in _token_regex.split(spec)
for item in postprocess(raw_token)
if item
)
return tokens


def _token_type(token: str, location: str = None) -> str:
if token.startswith(" ") or token.endswith(" "):
type_ = "delimiter"
elif (
token.isnumeric() or
(token.startswith("{") and token.endswith("}")) or
(token.startswith('"') and token.endswith('"')) or
(token.startswith("'") and token.endswith("'"))
):
type_ = "literal"
elif token.startswith("{"):
logger.warning(
__("invalid value set (missing closing brace): %s"),
token,
location=location,
)
type_ = "literal"
elif token.endswith("}"):
logger.warning(
__("invalid value set (missing opening brace): %s"),
token,
location=location,
)
type_ = "literal"
elif token.startswith("'") or token.startswith('"'):
logger.warning(
__("malformed string literal (missing closing quote): %s"),
token,
location=location,
)
type_ = "literal"
elif token.endswith("'") or token.endswith('"'):
logger.warning(
__("malformed string literal (missing opening quote): %s"),
token,
location=location,
)
type_ = "literal"
elif token in ("optional", "default"):
type_ = "control"
elif _xref_regex.match(token):
type_ = "reference"
else:
type_ = "obj"

return type_


def _convert_numpy_type_spec(_type: str, location: str = None, translations: dict = {}) -> str:
def convert_obj(obj, translations, default_translation):
translation = translations.get(obj, obj)

# use :class: (the default) only if obj is not a standard singleton (None, True, False)
if translation in ("None", "True", "False") and default_translation == ":class:`%s`":
default_translation = ":obj:`%s`"

if _xref_regex.match(translation) is None:
translation = default_translation % translation

return translation

tokens = _tokenize_type_spec(_type)
combined_tokens = _recombine_set_tokens(tokens)
types = [
(token, _token_type(token, location))
for token in combined_tokens
]

# don't use the object role if it's not necessary
default_translation = (
":class:`%s`"
if not all(type_ == "obj" for _, type_ in types)
else "%s"
)

converters = {
"literal": lambda x: "``%s``" % x,
"obj": lambda x: convert_obj(x, translations, default_translation),
"control": lambda x: "*%s*" % x,
"delimiter": lambda x: x,
"reference": lambda x: x,
}

converted = "".join(converters.get(type_)(token) for token, type_ in types)

return converted


class NumpyDocstring(GoogleDocstring):
"""Convert NumPy style docstrings to reStructuredText.

Expand Down Expand Up @@ -879,6 +1047,16 @@ def __init__(self, docstring: Union[str, List[str]], config: SphinxConfig = None
self._directive_sections = ['.. index::']
super().__init__(docstring, config, app, what, name, obj, options)

def _get_location(self) -> str:
filepath = inspect.getfile(self._obj) if self._obj is not None else ""
name = self._name
line = ""

if filepath is None and name is None:
return None

return ":".join([filepath, "docstring of %s" % name, line])
keewis marked this conversation as resolved.
Show resolved Hide resolved

def _consume_field(self, parse_type: bool = True, prefer_type: bool = False
) -> Tuple[str, str, List[str]]:
line = next(self._line_iter)
Expand All @@ -888,6 +1066,12 @@ def _consume_field(self, parse_type: bool = True, prefer_type: bool = False
_name, _type = line, ''
_name, _type = _name.strip(), _type.strip()
_name = self._escape_args_and_kwargs(_name)
if self._config.napoleon_use_param:
_type = _convert_numpy_type_spec(
_type,
location=self._get_location(),
translations=self._config.napoleon_type_aliases or {},
)
tk0miya marked this conversation as resolved.
Show resolved Hide resolved

if prefer_type and not _type:
_type, _name = _name, _type
Expand Down