From 4692138eb83e73e5bcc68ba773ad4309bc363689 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Fri, 2 Jun 2023 21:14:31 +0200 Subject: [PATCH 1/7] stubgen: Preserve simple defaults in function signatures Fixes #13238 See also https://github.com/python/typeshed/issues/8988 --- mypy/stubgen.py | 33 +++++++++++++++++--- test-data/unit/stubgen.test | 61 +++++++++++++++++++++++++------------ 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index ba71456af4a4..d138de65e87f 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -749,14 +749,15 @@ def visit_func_def(self, o: FuncDef) -> None: args.append("*") if arg_.initializer: + default = self.get_str_default_of_node(arg_.initializer) if not annotation: typename = self.get_str_type_of_node(arg_.initializer, True, False) if typename == "": - annotation = "=..." + annotation = f"={default}" else: - annotation = f": {typename} = ..." + annotation = f": {typename} = {default}" else: - annotation += " = ..." + annotation += f" = {default}" arg = name + annotation elif kind == ARG_STAR: arg = f"*{name}{annotation}" @@ -1401,8 +1402,11 @@ def get_str_type_of_node( return "bytes" if isinstance(rvalue, FloatExpr): return "float" - if isinstance(rvalue, UnaryExpr) and isinstance(rvalue.expr, IntExpr): - return "int" + if isinstance(rvalue, UnaryExpr): + if isinstance(rvalue.expr, IntExpr): + return "int" + if isinstance(rvalue.expr, FloatExpr): + return "float" if isinstance(rvalue, NameExpr) and rvalue.name in ("True", "False"): return "bool" if can_infer_optional and isinstance(rvalue, NameExpr) and rvalue.name == "None": @@ -1414,6 +1418,25 @@ def get_str_type_of_node( else: return "" + def get_str_default_of_node(self, rvalue: Expression) -> str: + default = "..." + if isinstance(rvalue, NameExpr): + if rvalue.name in ("None", "True", "False"): + default = rvalue.name + elif isinstance(rvalue, (IntExpr, FloatExpr)): + default = f"{rvalue.value}" + elif isinstance(rvalue, UnaryExpr): + if isinstance(rvalue.expr, (IntExpr, FloatExpr)): + default = f"{rvalue.op}{rvalue.expr.value}" + elif isinstance(rvalue, StrExpr): + default = repr(rvalue.value) + elif isinstance(rvalue, BytesExpr): + default = f"b{rvalue.value!r}" + + if len(default) > 200: # TODO: what's a good limit? + default = "..." # long literals are not useful in stubs + return default + def print_annotation(self, t: Type) -> str: printer = AnnotationPrinter(self) return t.accept(printer) diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index e1818dc4c4bc..24dad0c82e45 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -22,35 +22,35 @@ def g(arg) -> None: ... def f(a, b=2): ... def g(b=-1, c=0): ... [out] -def f(a, b: int = ...) -> None: ... -def g(b: int = ..., c: int = ...) -> None: ... +def f(a, b: int = 2) -> None: ... +def g(b: int = -1, c: int = 0) -> None: ... [case testDefaultArgNone] def f(x=None): ... [out] from _typeshed import Incomplete -def f(x: Incomplete | None = ...) -> None: ... +def f(x: Incomplete | None = None) -> None: ... [case testDefaultArgBool] def f(x=True, y=False): ... [out] -def f(x: bool = ..., y: bool = ...) -> None: ... +def f(x: bool = True, y: bool = False) -> None: ... [case testDefaultArgStr] -def f(x='foo'): ... +def f(x='foo',y="how's quotes"): ... [out] -def f(x: str = ...) -> None: ... +def f(x: str = 'foo', y: str = "how's quotes") -> None: ... [case testDefaultArgBytes] -def f(x=b'foo'): ... +def f(x=b'foo',y=b"what's up"): ... [out] -def f(x: bytes = ...) -> None: ... +def f(x: bytes = b'foo', y: bytes = b"what's up") -> None: ... [case testDefaultArgFloat] -def f(x=1.2): ... +def f(x=1.2,y=1e-6,z=0.0,w=-0.0,v=+1.0): ... [out] -def f(x: float = ...) -> None: ... +def f(x: float = 1.2, y: float = 1e-06, z: float = 0.0, w: float = -0.0, v: float = +1.0) -> None: ... [case testDefaultArgOther] def f(x=ord): ... @@ -111,10 +111,10 @@ def i(a, *, b=1): ... def j(a, *, b=1, **c): ... [out] def f(a, *b, **c) -> None: ... -def g(a, *b, c: int = ...) -> None: ... -def h(a, *b, c: int = ..., **d) -> None: ... -def i(a, *, b: int = ...) -> None: ... -def j(a, *, b: int = ..., **c) -> None: ... +def g(a, *b, c: int = 1) -> None: ... +def h(a, *b, c: int = 1, **d) -> None: ... +def i(a, *, b: int = 1) -> None: ... +def j(a, *, b: int = 1, **c) -> None: ... [case testClass] class A: @@ -298,8 +298,8 @@ y: Incomplete def f(x, *, y=1): ... def g(x, *, y=1, z=2): ... [out] -def f(x, *, y: int = ...) -> None: ... -def g(x, *, y: int = ..., z: int = ...) -> None: ... +def f(x, *, y: int = 1) -> None: ... +def g(x, *, y: int = 1, z: int = 2) -> None: ... [case testProperty] class A: @@ -983,8 +983,8 @@ from _typeshed import Incomplete class A: x: Incomplete - def __init__(self, a: Incomplete | None = ...) -> None: ... - def method(self, a: Incomplete | None = ...) -> None: ... + def __init__(self, a: Incomplete | None = None) -> None: ... + def method(self, a: Incomplete | None = None) -> None: ... [case testAnnotationImportsFrom] import foo @@ -2142,7 +2142,7 @@ from _typeshed import Incomplete as _Incomplete Y: _Incomplete -def g(x: _Incomplete | None = ...) -> None: ... +def g(x: _Incomplete | None = None) -> None: ... x: _Incomplete @@ -3052,7 +3052,7 @@ class P(Protocol): [case testNonDefaultKeywordOnlyArgAfterAsterisk] def func(*, non_default_kwarg: bool, default_kwarg: bool = True): ... [out] -def func(*, non_default_kwarg: bool, default_kwarg: bool = ...): ... +def func(*, non_default_kwarg: bool, default_kwarg: bool = True): ... [case testNestedGenerator] def f1(): @@ -3317,3 +3317,24 @@ def gen2() -> _Generator[_Incomplete, _Incomplete, _Incomplete]: ... class X(_Incomplete): ... class Y(_Incomplete): ... + +[case testIgnoreLongDefaults] +def f(x='abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\ +abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\ +abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\ +abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'): ... + +def g(x=b'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\ +abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\ +abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz\ +abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz'): ... + +def h(x=123456789012345678901234567890123456789012345678901234567890\ +123456789012345678901234567890123456789012345678901234567890\ +123456789012345678901234567890123456789012345678901234567890\ +123456789012345678901234567890123456789012345678901234567890): ... + +[out] +def f(x: str = ...) -> None: ... +def g(x: bytes = ...) -> None: ... +def h(x: int = ...) -> None: ... From 8401f8efb10779f1c04a20213869fac62a7b4736 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Wed, 7 Jun 2023 16:04:58 +0200 Subject: [PATCH 2/7] Also add defaults to simple builtin containers --- mypy/stubgen.py | 40 +++++++++++++++++++++++++++++++++++++ test-data/unit/stubgen.test | 26 ++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index d138de65e87f..603cfe7fa41c 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -100,6 +100,7 @@ NameExpr, OpExpr, OverloadedFuncDef, + SetExpr, Statement, StrExpr, TupleExpr, @@ -1432,6 +1433,45 @@ def get_str_default_of_node(self, rvalue: Expression) -> str: default = repr(rvalue.value) elif isinstance(rvalue, BytesExpr): default = f"b{rvalue.value!r}" + elif isinstance(rvalue, TupleExpr): + items_defaults = [] + for e in rvalue.items: + e_default = self.get_str_default_of_node(e) + if e_default == "...": + break + items_defaults.append(e_default) + else: + closing = ",)" if len(items_defaults) == 1 else ")" + default = "(" + ", ".join(items_defaults) + closing + elif isinstance(rvalue, ListExpr): + items_defaults = [] + for e in rvalue.items: + e_default = self.get_str_default_of_node(e) + if e_default == "...": + break + items_defaults.append(e_default) + else: + default = "[" + ", ".join(items_defaults) + "]" + elif isinstance(rvalue, SetExpr): + items_defaults = [] + for e in rvalue.items: + e_default = self.get_str_default_of_node(e) + if e_default == "...": + break + items_defaults.append(e_default) + else: + if items_defaults: + default = "{" + ", ".join(items_defaults) + "}" + elif isinstance(rvalue, DictExpr): + items_defaults = [] + for k, v in rvalue.items: + k_default = self.get_str_default_of_node(k) + v_default = self.get_str_default_of_node(v) + if k_default == "..." or v_default == "...": + break + items_defaults.append(f"{k_default}: {v_default}") + else: + default = "{" + ", ".join(items_defaults) + "}" if len(default) > 200: # TODO: what's a good limit? default = "..." # long literals are not useful in stubs diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index 24dad0c82e45..4fdd36c4a4d6 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -3338,3 +3338,29 @@ def h(x=123456789012345678901234567890123456789012345678901234567890\ def f(x: str = ...) -> None: ... def g(x: bytes = ...) -> None: ... def h(x: int = ...) -> None: ... + +[case testDefaultsOfBuiltinContainers] +def f(x=(), y=(1,), z=(1, 2)): ... +def g(x=[], y=[1, 2]): ... +def h(x={}, y={1: 2, 3: 4}): ... +def i(x={1, 2, 3}): ... +def j(x=[(1,"a"), (2,"b")]): ... + +[out] +def f(x=(), y=(1,), z=(1, 2)) -> None: ... +def g(x=[], y=[1, 2]) -> None: ... +def h(x={}, y={1: 2, 3: 4}) -> None: ... +def i(x={1, 2, 3}) -> None: ... +def j(x=[(1, 'a'), (2, 'b')]) -> None: ... + +[case testDefaultsOfBuiltinContainersWithNonTrivialContent] +def f(x=(1, u.v), y=(k(),), z=(w,)): ... +def g(x=[1, u.v], y=[k()], z=[w]): ... +def h(x={1: u.v}, y={k(): 2}, z={w: w}): ... +def i(x={u.v, 2}, y={3, k()}, z={w}): ... + +[out] +def f(x=..., y=..., z=...) -> None: ... +def g(x=..., y=..., z=...) -> None: ... +def h(x=..., y=..., z=...) -> None: ... +def i(x=..., y=..., z=...) -> None: ... From c55605fa5a585c03e237b87ef977e748c32d8c75 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Wed, 7 Jun 2023 16:17:20 +0200 Subject: [PATCH 3/7] Fix self check --- mypy/stubgen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 603cfe7fa41c..4c2ec297a224 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -1465,7 +1465,7 @@ def get_str_default_of_node(self, rvalue: Expression) -> str: elif isinstance(rvalue, DictExpr): items_defaults = [] for k, v in rvalue.items: - k_default = self.get_str_default_of_node(k) + k_default = self.get_str_default_of_node(k) if k is not None else "..." v_default = self.get_str_default_of_node(v) if k_default == "..." or v_default == "...": break From 86242985448a84fe83876368c733088458741892 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Tue, 27 Jun 2023 22:52:09 +0200 Subject: [PATCH 4/7] Improve handling of `{**something}` case --- mypy/stubgen.py | 4 +++- test-data/unit/stubgen.test | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 4c2ec297a224..61bf637d562b 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -1465,7 +1465,9 @@ def get_str_default_of_node(self, rvalue: Expression) -> str: elif isinstance(rvalue, DictExpr): items_defaults = [] for k, v in rvalue.items: - k_default = self.get_str_default_of_node(k) if k is not None else "..." + if k is None: + break + k_default = self.get_str_default_of_node(k) v_default = self.get_str_default_of_node(v) if k_default == "..." or v_default == "...": break diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index 4fdd36c4a4d6..1d4c02fc23e9 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -3356,11 +3356,11 @@ def j(x=[(1, 'a'), (2, 'b')]) -> None: ... [case testDefaultsOfBuiltinContainersWithNonTrivialContent] def f(x=(1, u.v), y=(k(),), z=(w,)): ... def g(x=[1, u.v], y=[k()], z=[w]): ... -def h(x={1: u.v}, y={k(): 2}, z={w: w}): ... +def h(x={1: u.v}, y={k(): 2}, z={m: m}, w={**n}): ... def i(x={u.v, 2}, y={3, k()}, z={w}): ... [out] def f(x=..., y=..., z=...) -> None: ... def g(x=..., y=..., z=...) -> None: ... -def h(x=..., y=..., z=...) -> None: ... +def h(x=..., y=..., z=..., w=...) -> None: ... def i(x=..., y=..., z=...) -> None: ... From e432e5c20e17eac73e4da684aa43eccb6963d238 Mon Sep 17 00:00:00 2001 From: Ali Hamdan Date: Sun, 26 Nov 2023 22:46:51 +0100 Subject: [PATCH 5/7] Fix non-ascii bytes and add a test for nan and infinity --- mypy/stubgen.py | 2 +- test-data/unit/stubgen.test | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mypy/stubgen.py b/mypy/stubgen.py index 8167539190db..fff6ab058459 100755 --- a/mypy/stubgen.py +++ b/mypy/stubgen.py @@ -1257,7 +1257,7 @@ def get_str_default_of_node(self, rvalue: Expression) -> tuple[str, bool]: elif isinstance(rvalue, StrExpr): return repr(rvalue.value), True elif isinstance(rvalue, BytesExpr): - return f"b{rvalue.value!r}", True + return "b" + repr(rvalue.value).replace("\\\\", "\\"), True elif isinstance(rvalue, TupleExpr): items_defaults = [] for e in rvalue.items: diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test index 4e574297884e..cd38242ce031 100644 --- a/test-data/unit/stubgen.test +++ b/test-data/unit/stubgen.test @@ -58,14 +58,16 @@ def f(x='foo'): ... def f(x: str = ...): ... [case testDefaultArgBytes] -def f(x=b'foo',y=b"what's up"): ... +def f(x=b'foo',y=b"what's up",z=b'\xc3\xa0 la une'): ... [out] -def f(x: bytes = b'foo', y: bytes = b"what's up") -> None: ... +def f(x: bytes = b'foo', y: bytes = b"what's up", z: bytes = b'\xc3\xa0 la une') -> None: ... [case testDefaultArgFloat] def f(x=1.2,y=1e-6,z=0.0,w=-0.0,v=+1.0): ... +def g(x=float("nan"), y=float("inf"), z=float("-inf")): ... [out] def f(x: float = 1.2, y: float = 1e-06, z: float = 0.0, w: float = -0.0, v: float = +1.0) -> None: ... +def g(x=..., y=..., z=...) -> None: ... [case testDefaultArgOther] def f(x=ord): ... From d8e7460bed4b1ad5064d383c26b25ff3f8b77e1b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 26 Nov 2023 16:55:29 -0800 Subject: [PATCH 6/7] Update mypy/stubdoc.py --- mypy/stubdoc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/stubdoc.py b/mypy/stubdoc.py index 9612a09abb70..dbf47508bcf0 100644 --- a/mypy/stubdoc.py +++ b/mypy/stubdoc.py @@ -37,7 +37,7 @@ class ArgSig: """Signature info for a single argument.""" def __init__( - self, name: str, type: str | None = None, default: bool = False, default_value: str = "..." + self, name: str, type: str | None = None, *, default: bool = False, default_value: str = "..." ) -> None: self.name = name self.type = type From 4011c16c40f45c1cdf45c56a6d5ba4b365561ccd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 00:55:50 +0000 Subject: [PATCH 7/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/stubdoc.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mypy/stubdoc.py b/mypy/stubdoc.py index dbf47508bcf0..126ac44e142e 100644 --- a/mypy/stubdoc.py +++ b/mypy/stubdoc.py @@ -37,7 +37,12 @@ class ArgSig: """Signature info for a single argument.""" def __init__( - self, name: str, type: str | None = None, *, default: bool = False, default_value: str = "..." + self, + name: str, + type: str | None = None, + *, + default: bool = False, + default_value: str = "...", ) -> None: self.name = name self.type = type