Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce a check disallowing module-level defaults, and improve Y015 error messages #145

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Change Log
----------

Unreleased
~~~~~~~~~~

* introduce Y032 (disallow default values for most module-level assignments).
Similar to the Y092 check that was removed in 22.1.0. However, this check is
enabled by default, whereas Y092 was disabled by default.

22.1.0
~~~~~~

Expand Down
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ currently emitted:
syntax wherever possible. (In situations where this is not possible, such as
if a field is a Python keyword or an invalid identifier, this error will not
be raised.)
* Y032: Unless the object is used elswhere in the same file as an annotation,
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
a module-level attribute should not have a default value.

Many error codes enforce modern conventions, and some cannot yet be used in
all cases:
Expand Down
24 changes: 20 additions & 4 deletions pyi.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ def __init__(self, filename: Path = Path("none")) -> None:
self.typevarlike_defs: dict[TypeVarInfo, ast.Assign] = {}
# Mapping of each name in the file to the no. of occurrences
self.all_name_occurrences: Counter[str] = Counter()
self.suspicious_global_assignments: dict[str, ast.AnnAssign] = {}
Akuli marked this conversation as resolved.
Show resolved Hide resolved
self.string_literals_allowed = NestingCounter()
self.in_function = NestingCounter()
self.in_class = NestingCounter()
Expand Down Expand Up @@ -472,10 +473,16 @@ def visit_Expr(self, node: ast.Expr) -> None:

def visit_AnnAssign(self, node: ast.AnnAssign) -> None:
self.generic_visit(node)
if isinstance(node.annotation, ast.Name) and node.annotation.id == "TypeAlias":
return
if node.value and not isinstance(node.value, ast.Ellipsis):
self.error(node.value, Y015)
if isinstance(node.annotation, ast.Name):
if node.annotation.id == "TypeAlias":
return
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
if isinstance(node.target, ast.Name):
if self.in_class.active:
if node.value and not isinstance(node.value, ast.Ellipsis):
self.error(node.value, Y015)
else:
if node.value:
self.suspicious_global_assignments[node.target.id] = node

def _check_union_members(self, members: Sequence[ast.expr]) -> None:
members_by_dump: dict[str, list[ast.expr]] = {}
Expand Down Expand Up @@ -891,6 +898,14 @@ def run(self, tree: ast.AST) -> Iterable[Error]:
def_node,
Y018.format(typevarlike_cls=cls_name, typevar_name=typevar_name),
)
# Only raise Y032 if the name is not used anywhere within the same file,
# as otherwise stock flake8 raises spurious errors about the name being undefined.
# See https://github.com/python/typeshed/pull/6930
for thing_name, assign_node in self.suspicious_global_assignments.items():
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
if self.all_name_occurrences[thing_name] == 1:
self.error(assign_node, Y032)
elif not isinstance(assign_node.value, ast.Ellipsis):
self.error(assign_node, Y015)
yield from self.errors


Expand Down Expand Up @@ -980,3 +995,4 @@ def parse_options(cls, optmanager, options, extra_args) -> None:
Y029 = "Y029 Defining __repr__ or __str__ in a stub is almost always redundant"
Y030 = "Y030 Multiple Literal members in a union. {suggestion}"
Y031 = "Y031 Use class-based syntax for TypedDicts where possible"
Y032 = "Y032 Default value unnecessary for module-level attribute"
15 changes: 11 additions & 4 deletions tests/attribute_annotations.pyi
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
field1: int
field2: int = ...
field0: int
field1: int = ... # Y032 Default value unnecessary for module-level attribute
field2: int = 0 # Y032 Default value unnecessary for module-level attribute
field3 = ... # type: int
field4: int = 0 # Y015 Attribute must not have a default value other than "..."
field5 = 0 # type: int # Y015 Attribute must not have a default value other than "..."
field6 = 0 # Y015 Attribute must not have a default value other than "..."
field7 = b"" # Y015 Attribute must not have a default value other than "..."
field8: str = ...

thing_using_field3: field3
thing_using_field4: field4
thing_using_field5: field5
thing_using_field6: field6
thing_using_field7: field7
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved

class Foo:
field1: int
field2: int = ...
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
field3 = ... # type: int
field4: int = 0 # Y015 Attribute must not have a default value other than "..."
field5 = 0 # type: int # Y015 Attribute must not have a default value other than "..."
field6 = 0 # Y015 Attribute must not have a default value other than "..."
field7 = b"" # Y015 Attribute must not have a default value other than "..."
thing_using_field8_in_class: field8
3 changes: 3 additions & 0 deletions tests/typevar.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ _Ts = TypeVarTuple("_Ts") # Y018 TypeVarTuple "_Ts" is not used
_UsedTypeVar = TypeVar("_UsedTypeVar")
def func(arg: _UsedTypeVar) -> _UsedTypeVar: ...

_UsedOnlyInClassDef = TypeVar("_UsedOnlyInClassDef")
class Foo(list[_UsedOnlyInClassDef]): ...

_TypeVarUsedInBinOp = TypeVar("_TypeVarUsedInBinOp", bound=str)
def func2(arg: _TypeVarUsedInBinOp | int) -> _TypeVarUsedInBinOp | int: ...

Expand Down