Skip to content

Commit

Permalink
Merge pull request #8278 from tk0miya/8255_hexadecimal_default_value
Browse files Browse the repository at this point in the history
Fix #8255: py domain: number in defarg is changed to decimal
  • Loading branch information
tk0miya committed Oct 5, 2020
2 parents 2371be5 + cc941db commit 408ebe4
Show file tree
Hide file tree
Showing 5 changed files with 46 additions and 19 deletions.
2 changes: 2 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ Bugs fixed
* #8277: sphinx-build: missing and redundant spacing (and etc) for console
output on building
* #7973: imgconverter: Check availability of imagemagick many times
* #8255: py domain: number in default argument value is changed from hexadecimal
to decimal
* #8093: The highlight warning has wrong location in some builders (LaTeX,
singlehtml and so on)
* #8239: Failed to refer a token in productionlist if it is indented
Expand Down
11 changes: 9 additions & 2 deletions sphinx/pycode/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,19 @@ def parse(code: str, mode: str = 'exec') -> "ast.AST":
return ast.parse(code, mode=mode)


def unparse(node: Optional[ast.AST]) -> Optional[str]:
def unparse(node: Optional[ast.AST], code: str = '') -> Optional[str]:
"""Unparse an AST to string."""
if node is None:
return None
elif isinstance(node, str):
return node
return _UnparseVisitor().visit(node)
return _UnparseVisitor(code).visit(node)


# a greatly cut-down version of `ast._Unparser`
class _UnparseVisitor(ast.NodeVisitor):
def __init__(self, code: str = '') -> None:
self.code = code

def _visit_op(self, node: ast.AST) -> str:
return OPERATORS[node.__class__]
Expand Down Expand Up @@ -195,6 +197,11 @@ def visit_Tuple(self, node: ast.Tuple) -> str:
def visit_Constant(self, node: ast.Constant) -> str:
if node.value is Ellipsis:
return "..."
elif isinstance(node.value, (int, float, complex)):
if self.code and sys.version_info > (3, 8):
return ast.get_source_segment(self.code, node)
else:
return repr(node.value)
else:
return repr(node.value)

Expand Down
25 changes: 13 additions & 12 deletions sphinx/util/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,13 +600,14 @@ def stringify_signature(sig: inspect.Signature, show_annotation: bool = True,

def signature_from_str(signature: str) -> inspect.Signature:
"""Create a Signature object from string."""
module = ast.parse('def func' + signature + ': pass')
code = 'def func' + signature + ': pass'
module = ast.parse(code)
function = cast(ast.FunctionDef, module.body[0]) # type: ignore

return signature_from_ast(function)
return signature_from_ast(function, code)


def signature_from_ast(node: ast.FunctionDef) -> inspect.Signature:
def signature_from_ast(node: ast.FunctionDef, code: str = '') -> inspect.Signature:
"""Create a Signature object from AST *node*."""
args = node.args
defaults = list(args.defaults)
Expand All @@ -626,39 +627,39 @@ def signature_from_ast(node: ast.FunctionDef) -> inspect.Signature:
if defaults[i] is Parameter.empty:
default = Parameter.empty
else:
default = ast_unparse(defaults[i])
default = ast_unparse(defaults[i], code)

annotation = ast_unparse(arg.annotation) or Parameter.empty
annotation = ast_unparse(arg.annotation, code) or Parameter.empty
params.append(Parameter(arg.arg, Parameter.POSITIONAL_ONLY,
default=default, annotation=annotation))

for i, arg in enumerate(args.args):
if defaults[i + posonlyargs] is Parameter.empty:
default = Parameter.empty
else:
default = ast_unparse(defaults[i + posonlyargs])
default = ast_unparse(defaults[i + posonlyargs], code)

annotation = ast_unparse(arg.annotation) or Parameter.empty
annotation = ast_unparse(arg.annotation, code) or Parameter.empty
params.append(Parameter(arg.arg, Parameter.POSITIONAL_OR_KEYWORD,
default=default, annotation=annotation))

if args.vararg:
annotation = ast_unparse(args.vararg.annotation) or Parameter.empty
annotation = ast_unparse(args.vararg.annotation, code) or Parameter.empty
params.append(Parameter(args.vararg.arg, Parameter.VAR_POSITIONAL,
annotation=annotation))

for i, arg in enumerate(args.kwonlyargs):
default = ast_unparse(args.kw_defaults[i]) or Parameter.empty
annotation = ast_unparse(arg.annotation) or Parameter.empty
default = ast_unparse(args.kw_defaults[i], code) or Parameter.empty
annotation = ast_unparse(arg.annotation, code) or Parameter.empty
params.append(Parameter(arg.arg, Parameter.KEYWORD_ONLY, default=default,
annotation=annotation))

if args.kwarg:
annotation = ast_unparse(args.kwarg.annotation) or Parameter.empty
annotation = ast_unparse(args.kwarg.annotation, code) or Parameter.empty
params.append(Parameter(args.kwarg.arg, Parameter.VAR_KEYWORD,
annotation=annotation))

return_annotation = ast_unparse(node.returns) or Parameter.empty
return_annotation = ast_unparse(node.returns, code) or Parameter.empty

return inspect.Signature(params, return_annotation=return_annotation)

Expand Down
13 changes: 13 additions & 0 deletions tests/test_domain_py.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,19 @@ def test_pyfunction_signature_full_py38(app):
[desc_parameter, desc_sig_operator, "/"])])


@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.')
def test_pyfunction_with_number_literals(app):
text = ".. py:function:: hello(age=0x10, height=1_6_0)"
doctree = restructuredtext.parse(app, text)
assert_node(doctree[1][0][1],
[desc_parameterlist, ([desc_parameter, ([desc_sig_name, "age"],
[desc_sig_operator, "="],
[nodes.inline, "0x10"])],
[desc_parameter, ([desc_sig_name, "height"],
[desc_sig_operator, "="],
[nodes.inline, "1_6_0"])])])


def test_optional_pyfunction_signature(app):
text = ".. py:function:: compile(source [, filename [, symbol]]) -> ast object"
doctree = restructuredtext.parse(app, text)
Expand Down
14 changes: 9 additions & 5 deletions tests/test_pycode_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,20 @@
])
def test_unparse(source, expected):
module = ast.parse(source)
assert ast.unparse(module.body[0].value) == expected
assert ast.unparse(module.body[0].value, source) == expected


def test_unparse_None():
assert ast.unparse(None) is None


@pytest.mark.skipif(sys.version_info < (3, 8), reason='python 3.8+ is required.')
def test_unparse_py38():
source = "lambda x=0, /, y=1, *args, z, **kwargs: x + y + z"
expected = "lambda x=0, /, y=1, *args, z, **kwargs: ..."
@pytest.mark.parametrize('source,expected', [
("lambda x=0, /, y=1, *args, z, **kwargs: x + y + z",
"lambda x=0, /, y=1, *args, z, **kwargs: ..."), # posonlyargs
("0x1234", "0x1234"), # Constant
("1_000_000", "1_000_000"), # Constant
])
def test_unparse_py38(source, expected):
module = ast.parse(source)
assert ast.unparse(module.body[0].value) == expected
assert ast.unparse(module.body[0].value, source) == expected

0 comments on commit 408ebe4

Please sign in to comment.