Skip to content

Commit

Permalink
Add a warning for tuple[int] annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexWaygood committed Jul 9, 2023
1 parent 5792181 commit c6b0000
Show file tree
Hide file tree
Showing 7 changed files with 60 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
# produces false positives if you're surrounding things with double quotes

[flake8]
extend-select = B
max-line-length = 80
max-complexity = 12
noqa-require-code = true
select = B,C,E,F,W,Y,B9,NQA
per-file-ignores =
*.py: B905, B907, B950, E203, E501, W503, W291, W293
*.pyi: B, E301, E302, E305, E501, E701, E704, W503
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Change Log

## Unreleased

* Introduce Y090, which warns if you have an annotation such as `tuple[int]` or
`Tuple[int]`. These mean "a tuple of length 1, in which the sole element is
of type `int`". This is sometimes what you want, but more usually you'll want
`tuple[int, ...]`, which means "a tuple of arbitrary (possibly 0) length, in
which all elements are of type `int`".

This error code is disabled by default due to the risk of false-positive
errors. To enable it, use the `--extend-select=Y090` option.

## 23.6.0

Features:
Expand Down
10 changes: 9 additions & 1 deletion ERRORCODES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## List of warnings

The following warnings are currently emitted:
The following warnings are currently emitted by default:

| Code | Description
|------|-------------
Expand Down Expand Up @@ -60,3 +60,11 @@ The following warnings are currently emitted:
| 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]]`.
| 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.
| 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.

The following error codes are also provided, but are disabled by default due to the risk of false-positive errors. To enable these error codes, use
`--extend-select={code1,code2,...}` on the command line or in your flake8
configuration file:

| Code | Description
|------|------------
| 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`".
20 changes: 19 additions & 1 deletion pyi.py
Original file line number Diff line number Diff line change
Expand Up @@ -1355,10 +1355,18 @@ def visit_BinOp(self, node: ast.BinOp) -> None:

self._check_union_members(members, is_pep_604_union=True)

def _Y090_error(self, node: ast.Subscript) -> None:
current_code = unparse(node)
typ = unparse(node.slice)
copied_node = deepcopy(node)
copied_node.slice = ast.Tuple(elts=[copied_node.slice, ast.Constant(...)])
suggestion = unparse(copied_node)
self.error(node, Y090.format(original=current_code, typ=typ, new=suggestion))

def visit_Subscript(self, node: ast.Subscript) -> None:
subscripted_object = node.value
subscripted_object_name = _get_name_of_class_if_from_modules(
subscripted_object, modules=_TYPING_MODULES
subscripted_object, modules=_TYPING_MODULES | {"builtins"}
)
self.visit(subscripted_object)
if subscripted_object_name == "Literal":
Expand All @@ -1370,6 +1378,8 @@ def visit_Subscript(self, node: ast.Subscript) -> None:
self._visit_slice_tuple(node.slice, subscripted_object_name)
else:
self.visit(node.slice)
if subscripted_object_name in {"tuple", "Tuple"}:
self._Y090_error(node)

def _visit_slice_tuple(self, node: ast.Tuple, parent: str | None) -> None:
if parent == "Union":
Expand Down Expand Up @@ -2002,6 +2012,7 @@ def run(self) -> Iterable[Error]:
def add_options(parser: OptionManager) -> None:
"""This is brittle, there's multiple levels of caching of defaults."""
parser.parser.set_defaults(filename="*.py,*.pyi")
parser.extend_default_ignore(DISABLED_BY_DEFAULT)
parser.add_option(
"--no-pyi-aware-file-checker",
default=False,
Expand Down Expand Up @@ -2109,3 +2120,10 @@ def parse_options(options: argparse.Namespace) -> None:
Y057 = (
"Y057 Do not use {module}.ByteString, which has unclear semantics and is deprecated"
)
Y090 = (
'Y090 "{original}" means '
'"a tuple of length 1, in which the sole element is of type {typ!r}". '
'Perhaps you meant "{new}"?'
)

DISABLED_BY_DEFAULT = ["Y090"]
7 changes: 7 additions & 0 deletions tests/disabled_by_default.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# This test file checks that disabled-by-default error codes aren't triggered,
# unless they're explicitly enabled
from typing import Tuple # Y022 Use "tuple[Foo, Bar]" instead of "typing.Tuple[Foo, Bar]" (PEP 585 syntax)

# These would trigger Y090, but it's disabled by default
x: tuple[int]
y: Tuple[str]
8 changes: 8 additions & 0 deletions tests/single_element_tuples.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# flags: --extend-select=Y090
import builtins
import typing

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, ...]"?
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, ...]"?
c: tuple[int, ...]
d: typing.Tuple[builtins.str, builtins.complex] # Y022 Use "tuple[Foo, Bar]" instead of "typing.Tuple[Foo, Bar]" (PEP 585 syntax)
9 changes: 5 additions & 4 deletions tests/test_pyi_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ def test_pyi_file(path: str) -> None:
expected_output += f"{path}:{lineno}: {match.group(1)}{message}\n"

bad_flag_msg = (
"--ignore flags in test files override the .flake8 config file. "
"Use --extend-ignore instead."
)
"--{flag} flags in test files override the .flake8 config file. "
"Use --extend-{flag} instead."
).format

for flag in flags:
option = flag.split("=")[0]
assert option != "--ignore", bad_flag_msg
assert option not in {"--ignore", "--select"}, bad_flag_msg(option[2:])

# Silence DeprecationWarnings from our dependencies (pyflakes, flake8-bugbear, etc.)
#
Expand Down

0 comments on commit c6b0000

Please sign in to comment.