diff --git a/CHANGELOG.md b/CHANGELOG.md index c00e2c0a..cb92b7cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Features: * introduce Y033 (always use annotations in stubs, rather than type comments). * introduce Y034 (detect common errors where return types are hardcoded, but they should use `TypeVar`s instead). +* introduce Y035 (`__all__` in a stub has the same semantics as at runtime). ## 22.1.0 diff --git a/README.md b/README.md index a1013383..02df231e 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ currently emitted: | Y032 | The second argument of an `__eq__` or `__ne__` method should usually be annotated with `object` rather than `Any`. | Y033 | Do not use type comments (e.g. `x = ... # type: int`) in stubs, even if the stub supports Python 2. Always use annotations instead (e.g. `x: int`). | Y034 | Y034 detects common errors where certain methods are annotated as having a fixed return type, despite returning `self` at runtime. Such methods should be annotated with `_typeshed.Self`.<br><br>This check looks for `__new__`, `__enter__` and `__aenter__` methods that return the class's name unparameterised. It also looks for `__iter__` methods that return `Iterator`, even if the class inherits directly from `Iterator`, and for `__aiter__` methods that return `AsyncIterator`, even if the class inherits directly from `AsyncIterator`. The check excludes methods decorated with `@overload` or `@abstractmethod`. +| Y035 | `__all__` in a stub file should always have a value, as `__all__` in a `.pyi` file has identical semantics to `__all__` in a `.py` file. E.g. write `__all__ = ["foo", "bar"]` instead of `__all__: list[str]`. Many error codes enforce modern conventions, and some cannot yet be used in all cases: diff --git a/pyi.py b/pyi.py index df1ab41d..eec09464 100644 --- a/pyi.py +++ b/pyi.py @@ -660,6 +660,12 @@ def visit_Expr(self, node: ast.Expr) -> None: self.generic_visit(node) def visit_AnnAssign(self, node: ast.AnnAssign) -> None: + if _is_name(node.target, "__all__") and not self.in_class.active: + with self.string_literals_allowed.enabled(): + self.generic_visit(node) + if node.value is None: + self.error(node, Y035) + return self.generic_visit(node) if _is_TypeAlias(node.annotation): return @@ -1258,3 +1264,4 @@ def parse_options(cls, optmanager, options, extra_args) -> None: ) Y033 = 'Y033 Do not use type comments in stubs (e.g. use "x: int" instead of "x = ... # type: int")' Y034 = 'Y034 {methods} usually return "self" at runtime. Consider using "_typeshed.Self" in "{method_name}", e.g. "{suggested_syntax}"' +Y035 = 'Y035 "__all__" in a stub file must have a value, as it has the same semantics as "__all__" at runtime.' diff --git a/tests/__all__.pyi b/tests/__all__.pyi new file mode 100644 index 00000000..a20a0162 --- /dev/null +++ b/tests/__all__.pyi @@ -0,0 +1,7 @@ +__all__: list[str] # Y035 "__all__" in a stub file must have a value, as it has the same semantics as "__all__" at runtime. +__all__: list[str] = ["foo", "bar", "baz"] +__all__ = ["foo", "bar", "baz"] + +foo: int = ... +bar: str = ... +baz: list[set[bytes]] = ...