Skip to content

Commit 2a86db8

Browse files
Add a warning for tuple[int] annotations (#410)
Fixes #131 The error code is disabled by default, so as to avoid false positives. If this PR is merged, I can work on creating a non-blocking mypy_primer-esque workflow for typeshed (which runs flake8 only on changed files, with `--extend-select=Y090`, and posts a comment showing the new errors) to flag potential issues in PRs. Co-authored-by: Jelle Zijlstra <[email protected]>
1 parent 5792181 commit 2a86db8

7 files changed

+73
-7
lines changed

.flake8

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@
2424
# produces false positives if you're surrounding things with double quotes
2525

2626
[flake8]
27+
extend-select = B9
2728
max-line-length = 80
2829
max-complexity = 12
2930
noqa-require-code = true
30-
select = B,C,E,F,W,Y,B9,NQA
3131
per-file-ignores =
3232
*.py: B905, B907, B950, E203, E501, W503, W291, W293
3333
*.pyi: B, E301, E302, E305, E501, E701, E704, W503

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Change Log
22

3+
## Unreleased
4+
5+
* Introduce Y090, which warns if you have an annotation such as `tuple[int]` or
6+
`Tuple[int]`. These mean "a tuple of length 1, in which the sole element is
7+
of type `int`". This is sometimes what you want, but more usually you'll want
8+
`tuple[int, ...]`, which means "a tuple of arbitrary (possibly 0) length, in
9+
which all elements are of type `int`".
10+
11+
This error code is disabled by default due to the risk of false-positive
12+
errors. To enable it, use the `--extend-select=Y090` option.
13+
314
## 23.6.0
415

516
Features:

ERRORCODES.md

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## List of warnings
22

3-
The following warnings are currently emitted:
3+
The following warnings are currently emitted by default:
44

55
| Code | Description
66
|------|-------------
@@ -60,3 +60,18 @@ The following warnings are currently emitted:
6060
| Y055 | Unions of the form `type[X] \| type[Y]` can be simplified to `type[X \| Y]`. Similarly, `Union[type[X], type[Y]]` can be simplified to `type[Union[X, Y]]`.
6161
| Y056 | Do not call methods such as `.append()`, `.extend()` or `.remove()` on `__all__`. Different type checkers have varying levels of support for calling these methods on `__all__`. Use `+=` instead, which is known to be supported by all major type checkers.
6262
| Y057 | Do not use `typing.ByteString` or `collections.abc.ByteString`. These types have unclear semantics, and are deprecated; use `typing_extensions.Buffer` or a union such as `bytes \| bytearray \| memoryview` instead. See [PEP 688](https://peps.python.org/pep-0688/) for more details.
63+
64+
## Warnings disabled by default
65+
66+
The following error codes are also provided, but are disabled by default due to
67+
the risk of false-positive errors. To enable these error codes, use
68+
`--extend-select={code1,code2,...}` on the command line or in your flake8
69+
configuration file.
70+
71+
Note that `--extend-select` **will not work** if you have
72+
`--select` specified on the command line or in your configuration file. We
73+
recommend only using `--extend-select`, never `--select`.
74+
75+
| Code | Description
76+
|------|------------
77+
| Y090 | `tuple[int]` means "a tuple of length 1, in which the sole element is of type `int`". Consider using `tuple[int, ...]` instead, which means "a tuple of arbitrary (possibly 0) length, in which all elements are of type `int`".

pyi.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -1355,10 +1355,22 @@ def visit_BinOp(self, node: ast.BinOp) -> None:
13551355

13561356
self._check_union_members(members, is_pep_604_union=True)
13571357

1358+
def _Y090_error(self, node: ast.Subscript) -> None:
1359+
current_code = unparse(node)
1360+
typ = unparse(node.slice)
1361+
copied_node = deepcopy(node)
1362+
new_slice = ast.Tuple(elts=[copied_node.slice, ast.Constant(...)])
1363+
if sys.version_info >= (3, 9):
1364+
copied_node.slice = new_slice
1365+
else:
1366+
copied_node.slice = ast.Index(new_slice)
1367+
suggestion = unparse(copied_node)
1368+
self.error(node, Y090.format(original=current_code, typ=typ, new=suggestion))
1369+
13581370
def visit_Subscript(self, node: ast.Subscript) -> None:
13591371
subscripted_object = node.value
13601372
subscripted_object_name = _get_name_of_class_if_from_modules(
1361-
subscripted_object, modules=_TYPING_MODULES
1373+
subscripted_object, modules=_TYPING_MODULES | {"builtins"}
13621374
)
13631375
self.visit(subscripted_object)
13641376
if subscripted_object_name == "Literal":
@@ -1370,6 +1382,8 @@ def visit_Subscript(self, node: ast.Subscript) -> None:
13701382
self._visit_slice_tuple(node.slice, subscripted_object_name)
13711383
else:
13721384
self.visit(node.slice)
1385+
if subscripted_object_name in {"tuple", "Tuple"}:
1386+
self._Y090_error(node)
13731387

13741388
def _visit_slice_tuple(self, node: ast.Tuple, parent: str | None) -> None:
13751389
if parent == "Union":
@@ -2002,6 +2016,7 @@ def run(self) -> Iterable[Error]:
20022016
def add_options(parser: OptionManager) -> None:
20032017
"""This is brittle, there's multiple levels of caching of defaults."""
20042018
parser.parser.set_defaults(filename="*.py,*.pyi")
2019+
parser.extend_default_ignore(DISABLED_BY_DEFAULT)
20052020
parser.add_option(
20062021
"--no-pyi-aware-file-checker",
20072022
default=False,
@@ -2109,3 +2124,10 @@ def parse_options(options: argparse.Namespace) -> None:
21092124
Y057 = (
21102125
"Y057 Do not use {module}.ByteString, which has unclear semantics and is deprecated"
21112126
)
2127+
Y090 = (
2128+
'Y090 "{original}" means '
2129+
'"a tuple of length 1, in which the sole element is of type {typ!r}". '
2130+
'Perhaps you meant "{new}"?'
2131+
)
2132+
2133+
DISABLED_BY_DEFAULT = ["Y090"]

tests/disabled_by_default.pyi

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# This test file checks that disabled-by-default error codes aren't triggered,
2+
# unless they're explicitly enabled
3+
from typing import ( # Y022 Use "tuple[Foo, Bar]" instead of "typing.Tuple[Foo, Bar]" (PEP 585 syntax)
4+
Tuple,
5+
)
6+
7+
# These would trigger Y090, but it's disabled by default
8+
x: tuple[int]
9+
y: Tuple[str]

tests/single_element_tuples.pyi

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# flags: --extend-select=Y090
2+
import builtins
3+
import typing
4+
5+
a: tuple[int] # Y090 "tuple[int]" means "a tuple of length 1, in which the sole element is of type 'int'". Perhaps you meant "tuple[int, ...]"?
6+
b: typing.Tuple[builtins.str] # Y022 Use "tuple[Foo, Bar]" instead of "typing.Tuple[Foo, Bar]" (PEP 585 syntax) # Y090 "typing.Tuple[builtins.str]" means "a tuple of length 1, in which the sole element is of type 'builtins.str'". Perhaps you meant "typing.Tuple[builtins.str, ...]"?
7+
c: tuple[int, ...]
8+
d: typing.Tuple[builtins.str, builtins.complex] # Y022 Use "tuple[Foo, Bar]" instead of "typing.Tuple[Foo, Bar]" (PEP 585 syntax)

tests/test_pyi_files.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@ def test_pyi_file(path: str) -> None:
3535
expected_output += f"{path}:{lineno}: {match.group(1)}{message}\n"
3636

3737
bad_flag_msg = (
38-
"--ignore flags in test files override the .flake8 config file. "
39-
"Use --extend-ignore instead."
40-
)
38+
"--{flag} flags in test files override the .flake8 config file. "
39+
"Use --extend-{flag} instead."
40+
).format
41+
4142
for flag in flags:
4243
option = flag.split("=")[0]
43-
assert option != "--ignore", bad_flag_msg
44+
assert option not in {"--ignore", "--select"}, bad_flag_msg(option[2:])
4445

4546
# Silence DeprecationWarnings from our dependencies (pyflakes, flake8-bugbear, etc.)
4647
#

0 commit comments

Comments
 (0)