Skip to content

Commit fbad8a5

Browse files
authored
Allow simple container literals as default values (#358)
Resolves #357
1 parent f611897 commit fbad8a5

6 files changed

Lines changed: 72 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ New error codes:
1111
it now applies everywhere.
1212

1313
Other changes:
14+
* Y011/Y014/Y015: Simple container literals (`list`, `dict`, `tuple` and `set`
15+
literals) are now allowed as default values.
1416
* Y052 is now emitted more consistently.
1517
* Some things that used to result in Y011, Y014 or Y015 being emitted
1618
now result in Y053 or Y054 being emitted.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ currently emitted:
4444
| Y008 | Unrecognized platform in a `sys.platform` check. To prevent you from typos, we warn if you use a platform name outside a small set of known platforms (e.g. `"linux"` and `"win32"`).
4545
| Y009 | Empty class or function body should contain `...`, not `pass`. This is just a stylistic choice, but it's the one typeshed made.
4646
| Y010 | Function body must contain only `...`. Stub files should not contain code, so function bodies should be empty.
47-
| Y011 | Only simple default values (`int`, `float`, `complex`, `bytes`, `str`, `bool`, `None` or `...`) are allowed for typed function arguments. Type checkers ignore the default value, so the default value is not useful information for type-checking, but it may be useful information for other users of stubs such as IDEs. If you're writing a stub for a function that has a more complex default value, use `...` instead of trying to reproduce the runtime default exactly in the stub. Also use `...` for very long numbers, very long strings, very long bytes, or defaults that vary according to the machine Python is being run on.
47+
| Y011 | Only simple default values (`int`, `float`, `complex`, `bytes`, `str`, `bool`, `None`, `...`, or simple container literals) are allowed for typed function arguments. Type checkers ignore the default value, so the default value is not useful information for type-checking, but it may be useful information for other users of stubs such as IDEs. If you're writing a stub for a function that has a more complex default value, use `...` instead of trying to reproduce the runtime default exactly in the stub. Also use `...` for very long numbers, very long strings, very long bytes, or defaults that vary according to the machine Python is being run on.
4848
| Y012 | Class body must not contain `pass`.
4949
| Y013 | Non-empty class body must not contain `...`.
5050
| Y014 | Only simple default values are allowed for any function arguments. A stronger version of Y011 that includes arguments without type annotations.

pyi.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -719,13 +719,42 @@ def _analyse_union(members: Sequence[ast.expr]) -> UnionAnalysis:
719719
)
720720

721721

722-
def _is_valid_default_value_with_annotation(node: ast.expr) -> bool:
722+
def _is_valid_default_value_with_annotation(
723+
node: ast.expr, allow_containers=True
724+
) -> bool:
723725
"""Is `node` valid as a default value for a function or method parameter in a stub?
724726
725727
Note that this function is *also* used to determine
726728
the validity of default values for ast.AnnAssign nodes.
727729
(E.g. `foo: int = 5` is OK, but `foo: TypeVar = TypeVar("foo")` is not.)
728730
"""
731+
# lists, tuples, sets
732+
if isinstance(node, (ast.List, ast.Tuple, ast.Set)):
733+
return (
734+
allow_containers
735+
and len(node.elts) <= 10
736+
and all(
737+
_is_valid_default_value_with_annotation(elt, allow_containers=False)
738+
for elt in node.elts
739+
)
740+
)
741+
742+
# dicts
743+
if isinstance(node, ast.Dict):
744+
return (
745+
allow_containers
746+
and len(node.keys) <= 10
747+
and all(
748+
(
749+
subnode is not None
750+
and _is_valid_default_value_with_annotation(
751+
subnode, allow_containers=False
752+
)
753+
)
754+
for subnode in chain(node.keys, node.values)
755+
)
756+
)
757+
729758
# `...`, bools, None, str, bytes,
730759
# positive ints, positive floats, positive complex numbers with no real part
731760
if isinstance(node, (ast.Ellipsis, ast.NameConstant, ast.Str, ast.Bytes, ast.Num)):

tests/attribute_annotations.pyi

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ field9 = None # Y026 Use typing_extensions.TypeAlias for type aliases, e.g. "fi
3232
Field95: TypeAlias = None
3333
Field96: TypeAlias = int | None
3434
Field97: TypeAlias = None | typing.SupportsInt | builtins.str | float | bool
35+
field19 = [1, 2, 3] # Y052 Need type annotation for "field19"
36+
field191: list[int] = [1, 2, 3]
37+
field20 = (1, 2, 3) # Y052 Need type annotation for "field20"
38+
field201: tuple[int, ...] = (1, 2, 3)
39+
field21 = {1, 2, 3} # Y052 Need type annotation for "field21"
40+
field211: set[int] = {1, 2, 3}
41+
field212 = {"foo": "bar"} # Y052 Need type annotation for "field212"
42+
field213: dict[str, str] = {"foo": "bar"}
43+
field22: Final = {"foo": 5}
3544

3645
# Tests for Final
3746
field11: Final = 1
@@ -47,10 +56,16 @@ field182: Final = os.pathsep
4756
field183: Final = None
4857

4958
# We *should* emit Y015 for more complex default values
50-
field19 = [1, 2, 3] # Y015 Only simple default values are allowed for assignments
51-
field20 = (1, 2, 3) # Y015 Only simple default values are allowed for assignments
52-
field21 = {1, 2, 3} # Y015 Only simple default values are allowed for assignments
53-
field22: Final = {"foo": 5} # Y015 Only simple default values are allowed for assignments
59+
field221: list[int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] # Y015 Only simple default values are allowed for assignments
60+
field222: list[int] = [100000000000000000000000000000] # Y054 Numeric literals with a string representation >10 characters long are not permitted
61+
field223: list[int] = [*range(10)] # Y015 Only simple default values are allowed for assignments
62+
field224: list[int] = list(range(10)) # Y015 Only simple default values are allowed for assignments
63+
field225: list[object] = [{}, 1, 2] # Y015 Only simple default values are allowed for assignments
64+
field226: tuple[str | tuple[str, ...], ...] = ("foo", ("foo", "bar")) # Y015 Only simple default values are allowed for assignments
65+
field227: dict[str, object] = {"foo": {"foo": "bar"}} # Y015 Only simple default values are allowed for assignments
66+
field228: dict[str, list[object]] = {"foo": []} # Y015 Only simple default values are allowed for assignments
67+
# When parsed, this case results in `None` being placed in the `.keys` list for the `ast.Dict` node
68+
field229: dict[int, int] = {1: 2, **{3: 4}} # Y015 Only simple default values are allowed for assignments
5469
field23 = "foo" + "bar" # Y015 Only simple default values are allowed for assignments
5570
field24 = b"foo" + b"bar" # Y015 Only simple default values are allowed for assignments
5671
field25 = 5 * 5 # Y015 Only simple default values are allowed for assignments
@@ -91,10 +106,13 @@ class Foo:
91106
else:
92107
field19 = "w" # Y052 Need type annotation for "field19"
93108

94-
field20 = [1, 2, 3] # Y015 Only simple default values are allowed for assignments
95-
field21 = (1, 2, 3) # Y015 Only simple default values are allowed for assignments
96-
field22 = {1, 2, 3} # Y015 Only simple default values are allowed for assignments
97-
field23: Final = {"foo": 5} # Y015 Only simple default values are allowed for assignments
109+
field20 = [1, 2, 3] # Y052 Need type annotation for "field20"
110+
field201: list[int] = [1, 2, 3]
111+
field21 = (1, 2, 3) # Y052 Need type annotation for "field21"
112+
field211: tuple[int, ...] = (1, 2, 3)
113+
field22 = {1, 2, 3} # Y052 Need type annotation for "field22"
114+
field221: set[int] = {1, 2, 3}
115+
field23: Final = {"foo": 5}
98116
field24 = "foo" + "bar" # Y015 Only simple default values are allowed for assignments
99117
field25 = b"foo" + b"bar" # Y015 Only simple default values are allowed for assignments
100118
field26 = 5 * 5 # Y015 Only simple default values are allowed for assignments

tests/defaults.pyi

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,16 @@ def f901(x, y: str = os.pathsep) -> None: ... # Y011 Only simple default values
3535
def f10(x, y: str = ..., *args: "int") -> None: ... # Y020 Quoted annotations should never be used in stubs
3636
def f11(*, x: str = "x") -> None: ...
3737
def f12(*, x: str = ..., **kwargs: "int") -> None: ... # Y020 Quoted annotations should never be used in stubs
38-
def f13(x: list[str] = ["foo", "bar", "baz"]) -> None: ... # Y011 Only simple default values allowed for typed arguments
39-
def f14(x: tuple[str, ...] = ("foo", "bar", "baz")) -> None: ... # Y011 Only simple default values allowed for typed arguments
40-
def f15(x: set[str] = {"foo", "bar", "baz"}) -> None: ... # Y011 Only simple default values allowed for typed arguments
38+
def f13(x: list[str] = ["foo", "bar", "baz"]) -> None: ...
39+
def f14(x: tuple[str, ...] = ("foo", "bar", "baz")) -> None: ...
40+
def f15(x: set[str] = {"foo", "bar", "baz"}) -> None: ...
41+
def f151(x: dict[int, int] = {1: 2}) -> None: ...
42+
# When parsed, this case results in `None` being placed in the `.keys` list for the `ast.Dict` node
43+
def f152(x: dict[int, int] = {1: 2, **{3: 4}}) -> None: ... # Y011 Only simple default values allowed for typed arguments
44+
def f153(x: list[int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) -> None: ... # Y011 Only simple default values allowed for typed arguments
45+
def f154(x: tuple[str, ...] = ("foo", ("bar", "baz"))) -> None: ... # Y011 Only simple default values allowed for typed arguments
46+
def f141(x: list[int] = [*range(10)]) -> None: ... # Y011 Only simple default values allowed for typed arguments
47+
def f142(x: list[int] = list(range(10))) -> None: ... # Y011 Only simple default values allowed for typed arguments
4148
def f16(x: frozenset[bytes] = frozenset({b"foo", b"bar", b"baz"})) -> None: ... # Y011 Only simple default values allowed for typed arguments
4249
def f17(x: str = "foo" + "bar") -> None: ... # Y011 Only simple default values allowed for typed arguments
4350
def f18(x: str = b"foo" + b"bar") -> None: ... # Y011 Only simple default values allowed for typed arguments

tests/quotes.pyi

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ __all__ += ["h"]
99
__all__.extend(["i"])
1010
__all__.append("j")
1111
__all__.remove("j")
12-
__match_args__ = ('foo',) # Y015 Only simple default values are allowed for assignments
13-
__slots__ = ('foo',) # Y015 Only simple default values are allowed for assignments
12+
__match_args__ = ('foo',) # Y052 Need type annotation for "__match_args__"
13+
__slots__ = ('foo',) # Y052 Need type annotation for "__slots__"
1414

1515
def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs
1616
def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs
@@ -26,7 +26,7 @@ Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used i
2626
class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs
2727
"""Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs
2828

29-
__all__ = ('foo',) # Y015 Only simple default values are allowed for assignments
29+
__all__ = ('foo',) # Y052 Need type annotation for "__all__"
3030
__match_args__ = ('foo', 'bar')
3131
__slots__ = ('foo', 'bar')
3232

0 commit comments

Comments
 (0)