Skip to content

Commit

Permalink
Release v3.0
Browse files Browse the repository at this point in the history
  • Loading branch information
sco1 committed Jan 22, 2023
2 parents e9e1110 + 8eb49ee commit c63884c
Show file tree
Hide file tree
Showing 22 changed files with 265 additions and 1,123 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 2.9.1
current_version = 3.0.0
commit = False

[bumpversion:file:README.md]
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lint_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
python-version: ["3.8", "3.9", "3.10", "3.11"]
fail-fast: false

steps:
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
# Changelog
Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html) (`<major>`.`<minor>`.`<patch>`)

## [v3.0.0]
### Added
* Add `ANN402` for the presence of type comments
### Changed
* Python 3.8.1 is now the minimum supported version
* Flake8 v5.0 is now the minimum supported version

### Removed
* Remove support for [PEP 484-style](https://www.python.org/dev/peps/pep-0484/#type-comments) type comments
* See: https://mail.python.org/archives/list/typing-sig@python.org/thread/66JDHQ2I3U3CPUIYA43W7SPEJLLPUETG/
* See: https://github.com/python/mypy/issues/12947
* Remove `ANN301`

## [v2.9.1]
### Changed
* #144 Unpin the version ceiling for `attrs`.
Expand Down
63 changes: 5 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# flake8-annotations
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flake8-annotations/2.9.1?logo=python&logoColor=FFD43B)](https://pypi.org/project/flake8-annotations/)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flake8-annotations/3.0.0?logo=python&logoColor=FFD43B)](https://pypi.org/project/flake8-annotations/)
[![PyPI](https://img.shields.io/pypi/v/flake8-annotations?logo=Python&logoColor=FFD43B)](https://pypi.org/project/flake8-annotations/)
[![PyPI - License](https://img.shields.io/pypi/l/flake8-annotations?color=magenta)](https://github.com/sco1/flake8-annotations/blob/main/LICENSE)
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/sco1/flake8-annotations/main.svg)](https://results.pre-commit.ci/latest/github/sco1/flake8-annotations/main)
[![Open in Visual Studio Code](https://img.shields.io/badge/Open%20in-VSCode.dev-blue)](https://github.dev/sco1/flake8-annotations)

`flake8-annotations` is a plugin for [Flake8](http://flake8.pycqa.org/en/latest/) that detects the absence of [PEP 3107-style](https://www.python.org/dev/peps/pep-3107/) function annotations and [PEP 484-style](https://www.python.org/dev/peps/pep-0484/#type-comments) type comments (see: [Caveats](#Caveats-for-PEP-484-style-Type-Comments)).
`flake8-annotations` is a plugin for [Flake8](http://flake8.pycqa.org/en/latest/) that detects the absence of [PEP 3107-style](https://www.python.org/dev/peps/pep-3107/) function annotations.

What this won't do: Check variable annotations (see: [PEP 526](https://www.python.org/dev/peps/pep-0526/)), respect stub files, or replace [mypy](http://mypy-lang.org/).
What this won't do: replace [mypy](http://mypy-lang.org/), check type comments (see: [PEP 484](https://peps.python.org/pep-0484/#type-comments)), check variable annotations (see: [PEP 526](https://www.python.org/dev/peps/pep-0526/)), or respect stub files.

## Installation
Install from PyPi with your favorite `pip` invocation:
Expand All @@ -32,7 +32,7 @@ cog.out(
]]] -->
```bash
$ flake8 --version
5.0.4 (flake8-annotations: 2.9.1, mccabe: 0.7.0, pycodestyle: 2.9.1, pyflakes:2.5.0) CPython 3.10.6 on Darwin
6.0.0 (flake8-annotations: 3.0.0, mccabe: 0.7.0, pycodestyle: 2.10.0, pyflakes: 3.0.1) CPython 3.11.0 on Darwin
```
<!-- [[[end]]] -->

Expand Down Expand Up @@ -62,17 +62,12 @@ With the exception of `ANN4xx`-level warnings, all warnings are enabled by defau
| `ANN205` | Missing return type annotation for staticmethod |
| `ANN206` | Missing return type annotation for classmethod |

### Type Comments
**Deprecation notice**: Support for type comments will be removed in `3.0`. See [this issue](https://github.com/sco1/flake8-annotations/issues/95) for more information.
| ID | Description |
|----------|-----------------------------------------------------------|
| `ANN301` | PEP 484 disallows both type annotations and type comments |

### Opinionated Warnings
These warnings are disabled by default.
| ID | Description |
|----------|------------------------------------------------------------------------|
| `ANN401` | Dynamically typed expressions (typing.Any) are disallowed.<sup>2</sup> |
| `ANN402` | Type comments are disallowed. |

Use [`extend-select`](https://flake8.pycqa.org/en/latest/user/options.html#cmdoption-flake8-extend-ignore) to enable opinionated warnings without overriding other implicit configurations<sup>3</sup>.

Expand Down Expand Up @@ -195,54 +190,6 @@ Will not raise linting errors for missing annotations for the arguments & return

Decorator(s) to treat as `typing.overload` may be specified by the [`--overload-decorators`](#--overload-decorators-liststr) configuration option.

## Caveats for PEP 484-style Type Comments
**Deprecation notice**: Support for type comments will be removed in `3.0`. See [this issue](https://github.com/sco1/flake8-annotations/issues/95) for more information.
### Mixing argument-level and function-level type comments
Support is provided for mixing argument-level and function-level type comments.

```py
def foo(
arg1, # type: bool
arg2, # type: bool
): # type: (...) -> bool
pass
```

**Note:** If present, function-level type comments will override any argument-level type comments.

### Partial type comments
Partially type hinted functions are supported for non-static class methods.

For example:

```py
class Foo:
def __init__(self):
# type: () -> None
...

def bar(self, a):
# type: (int) -> int
...
```
Will consider `bar`'s `self` argument as unannotated and use the `int` type hint for `a`.

Partial type comments utilizing ellipses as placeholders is also supported:

```py
def foo(arg1, arg2):
# type: (bool) -> bool
pass
```
Will show `arg2` as missing a type hint.

```py
def foo(arg1, arg2):
# type: (..., bool) -> bool
pass
```
Will show `arg1` as missing a type hint.

## Dynamic Typing Caveats
Support is only provided for the following patterns:
* `from typing import any; foo: Any`
Expand Down
9 changes: 1 addition & 8 deletions flake8_annotations/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1 @@
import sys

if sys.version_info >= (3, 8):
PY_GTE_38 = True
else:
PY_GTE_38 = False

__version__ = "2.9.1"
__version__ = "3.0.0"
135 changes: 5 additions & 130 deletions flake8_annotations/ast_walker.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,19 @@
from __future__ import annotations

import ast
import typing as t
from itertools import zip_longest

from attrs import define

from flake8_annotations import PY_GTE_38
from flake8_annotations.enums import AnnotationType, ClassDecoratorType, FunctionType

# Check if we can use the stdlib ast module instead of typed_ast; stdlib ast gains native type
# comment support in Python 3.8
if PY_GTE_38:
import ast
from ast import Ellipsis as ast_Ellipsis
else:
from typed_ast import ast3 as ast # type: ignore[no-redef]
from typed_ast.ast3 import Ellipsis as ast_Ellipsis # type: ignore[assignment]


AST_DECORATOR_NODES = t.Union[ast.Attribute, ast.Call, ast.Name]
AST_DEF_NODES = t.Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef]
AST_FUNCTION_TYPES = t.Union[ast.FunctionDef, ast.AsyncFunctionDef]

# The order of AST_ARG_TYPES must match Python's grammar
# See: https://docs.python.org/3/library/ast.html#abstract-grammar
AST_ARG_TYPES: t.Tuple[str, ...] = ("args", "vararg", "kwonlyargs", "kwarg")
if PY_GTE_38:
# Positional-only args introduced in Python 3.8
# If posonlyargs are present, they will be before other argument types
AST_ARG_TYPES = ("posonlyargs",) + AST_ARG_TYPES
AST_ARG_TYPES: t.Tuple[str, ...] = ("posonlyargs", "args", "vararg", "kwonlyargs", "kwarg")


@define(slots=True)
Expand All @@ -40,7 +25,6 @@ class Argument:
col_offset: int
annotation_type: AnnotationType
has_type_annotation: bool = False
has_3107_annotation: bool = False
has_type_comment: bool = False
is_dynamically_typed: bool = False

Expand All @@ -59,45 +43,33 @@ def from_arg_node(cls, node: ast.arg, annotation_type_name: str) -> Argument:
annotation_type = AnnotationType[annotation_type_name]
new_arg = cls(node.arg, node.lineno, node.col_offset, annotation_type)

new_arg.has_type_annotation = False
if node.annotation:
new_arg.has_type_annotation = True
new_arg.has_3107_annotation = True

if cls._is_annotated_any(node.annotation):
new_arg.is_dynamically_typed = True

if node.type_comment:
new_arg.has_type_annotation = True
new_arg.has_type_comment = True

if cls._is_annotated_any(node.type_comment):
new_arg.is_dynamically_typed = True

return new_arg

@staticmethod
def _is_annotated_any(arg_expr: t.Union[ast.expr, str]) -> bool:
def _is_annotated_any(arg_expr: ast.expr) -> bool:
"""
Check if the provided expression node is annotated with `typing.Any`.
Support is provided for the following patterns:
* `from typing import Any; foo: Any`
* `import typing; foo: typing.Any`
* `import typing as <alias>; foo: <alias>.Any`
Type comments are also supported. Inline type comments are assumed to be passed here as
`str`, and function-level type comments are assumed to be passed as `ast.expr`.
"""
if isinstance(arg_expr, ast.Name):
if arg_expr.id == "Any":
return True
elif isinstance(arg_expr, ast.Attribute):
if arg_expr.attr == "Any":
return True
elif isinstance(arg_expr, str):
if arg_expr.split(".", maxsplit=1)[-1] == "Any":
return True

return False

Expand Down Expand Up @@ -252,19 +224,15 @@ def from_function_node(
return_arg = Argument("return", def_end_lineno, def_end_col_offset, AnnotationType.RETURN)
if node.returns:
return_arg.has_type_annotation = True
return_arg.has_3107_annotation = True
new_function.is_return_annotated = True

if Argument._is_annotated_any(node.returns):
return_arg.is_dynamically_typed = True

new_function.args.append(return_arg)

# Type comments in-line with input arguments are handled by the Argument class
# If a function-level type comment is present, attempt to parse for any missed type hints
if node.type_comment:
new_function.has_type_comment = True
new_function = cls.try_type_comment(new_function, node)

# Check for the presence of non-`None` returns using the special-case return node visitor
return_visitor = ReturnVisitor(node)
Expand All @@ -278,10 +246,7 @@ def colon_seeker(node: AST_FUNCTION_TYPES, lines: t.List[str]) -> t.Tuple[int, i
"""
Find the line & column indices of the function definition's closing colon.
Processing paths are Python version-dependent, as there are differences in where the
docstring is placed in the AST:
* Python >= 3.8, docstrings are contained in the body of the function node
* Python < 3.8, docstrings are contained in the function node
For Python >= 3.8, docstrings are contained in the body of the function node.
NOTE: AST's line numbers are 1-indexed, column offsets are 0-indexed. Since `lines` is a
list, it will be 0-indexed.
Expand All @@ -290,23 +255,9 @@ def colon_seeker(node: AST_FUNCTION_TYPES, lines: t.List[str]) -> t.Tuple[int, i
if node.lineno == node.body[0].lineno:
return Function._single_line_colon_seeker(node, lines[node.lineno - 1])

# With Python < 3.8, the function node includes the docstring & the body does not, so
# we have rewind through any docstrings, if present, before looking for the def colon
# We should end up with lines[def_end_lineno - 1] having the colon
def_end_lineno = node.body[0].lineno
if not PY_GTE_38:
# If the docstring is on one line then no rewinding is necessary.
n_triple_quotes = lines[def_end_lineno - 1].count('"""')
if n_triple_quotes == 1: # pragma: no branch
# Docstring closure, rewind until the opening is found & take the line prior
while True:
def_end_lineno -= 1
if '"""' in lines[def_end_lineno - 1]: # pragma: no branch
# Docstring has closed
break

# Once we've gotten here, we've found the line where the docstring begins, so we have
# to step up one more line to get to the close of the def
def_end_lineno = node.body[0].lineno
def_end_lineno -= 1

# Use str.rfind() to account for annotations on the same line, definition closure should
Expand All @@ -324,82 +275,6 @@ def _single_line_colon_seeker(node: AST_FUNCTION_TYPES, line: str) -> t.Tuple[in

return node.lineno, def_end_col_offset

@staticmethod
def try_type_comment(func_obj: Function, node: AST_FUNCTION_TYPES) -> Function:
"""
Attempt to infer type hints from a function-level type comment.
If a function is type commented it is assumed to have a return annotation, otherwise Python
will fail to parse the hint.
"""
# If we're in this function then the node is guaranteed to have a type comment, so we can
# ignore mypy's complaint about an incompatible type for `node.type_comment`
# Because we're passing in the `func_type` arg, we know that our return is guaranteed to be
# ast.FunctionType
hint_tree: ast.FunctionType = ast.parse(node.type_comment, "<func_type>", "func_type") # type: ignore[assignment, arg-type] # noqa: E501
hint_tree = Function._maybe_inject_class_argument(hint_tree, func_obj)

for arg, hint_comment in zip_longest(func_obj.args, hint_tree.argtypes):
if isinstance(hint_comment, ast_Ellipsis):
continue

if arg and hint_comment:
arg.has_type_annotation = True
arg.has_type_comment = True

if Argument._is_annotated_any(hint_comment):
arg.is_dynamically_typed = True

# Return arg is always last
func_obj.args[-1].has_type_annotation = True
func_obj.args[-1].has_type_comment = True
func_obj.is_return_annotated = True
if Argument._is_annotated_any(hint_tree.returns):
arg.is_dynamically_typed = True

return func_obj

@staticmethod
def _maybe_inject_class_argument(
hint_tree: ast.FunctionType, func_obj: Function
) -> ast.FunctionType:
"""
Inject `self` or `cls` args into a type comment to align with PEP 3107-style annotations.
Because PEP 484 does not describe a method to provide partial function-level type comments,
there is a potential for ambiguity in the context of both class methods and classmethods
when aligning type comments to method arguments.
These two class methods, for example, should lint equivalently:
def bar(self, a):
# type: (int) -> int
...
def bar(self, a: int) -> int
...
When this example type comment is parsed by `ast` and then matched with the method's
arguments, it associates the `int` hint to `self` rather than `a`, so a dummy hint needs to
be provided in situations where `self` or `class` are not hinted in the type comment in
order to achieve equivalent linting results to PEP-3107 style annotations.
A dummy `ast.Ellipses` constant is injected if the following criteria are met:
1. The function node is either a class method or classmethod
2. The number of hinted args is at least 1 less than the number of function args
"""
if not func_obj.is_class_method:
# Short circuit
return hint_tree

if func_obj.class_decorator_type != ClassDecoratorType.STATICMETHOD:
if len(hint_tree.argtypes) < (len(func_obj.args) - 1): # Subtract 1 to skip return arg
# Ignore mypy's objection to this assignment, Ellipsis subclasses expr so I'm not
# sure how to make Mypy happy with this but I think it still makes semantic sense
hint_tree.argtypes = [ast.Ellipsis()] + hint_tree.argtypes

return hint_tree

@staticmethod
def get_function_type(function_name: str) -> FunctionType:
"""
Expand Down
Loading

0 comments on commit c63884c

Please sign in to comment.