Skip to content

Commit 9058f8f

Browse files
authored
Release v2.6.0
2 parents f1fccd2 + 1b72427 commit 9058f8f

23 files changed

+670
-216
lines changed

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
# Changelog
22
Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html) (`<major>`.`<minor>`.`<patch>`)
33

4+
## [v2.6.0]
5+
### Added
6+
* #98 Add `--dispatch-decorators` to support suppression of all errors from functions decorated by decorators such as `functools.singledispatch` and `functools.singledispatchmethod`.
7+
* #99 Add `--overload-decorators` to support generic aliasing of the `typing.overload` decorator.
8+
9+
### Fixed
10+
* #106 Fix incorrect parsing of multiline docstrings with less than two lines of content, causing incorrect line numbers for yielded errors in Python versions prior to 3.8
11+
412
## [v2.5.0]
513
### Added
6-
* #103 add `--allow-untyped-nested` to suppress all errors from dynamically typted nested functions. A function is considered dynamically typed if it does not contain any type hints.
14+
* #103 Add `--allow-untyped-nested` to suppress all errors from dynamically typted nested functions. A function is considered dynamically typed if it does not contain any type hints.
715

816
## [v2.4.1]
917
### Fixed

README.md

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
# flake8-annotations
22
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flake8-annotations)](https://pypi.org/project/flake8-annotations/)
33
[![PyPI](https://img.shields.io/pypi/v/flake8-annotations)](https://pypi.org/project/flake8-annotations/)
4+
[![PyPI - License](https://img.shields.io/pypi/l/flake8-annotations?color=magenta)](https://github.com/sco1/flake8-annotations/blob/master/LICENSE)
5+
[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit)
6+
[![Code style: black](https://img.shields.io/badge/code%20style-black-black)](https://github.com/psf/black)
47

5-
`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)).
8+
`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)).
69

710
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/).
811

@@ -19,7 +22,7 @@ You can verify it's being picked up by invoking the following in your shell:
1922

2023
```bash
2124
$ flake8 --version
22-
3.8.4 (flake8-annotations: 2.5.0, mccabe: 0.6.1, pycodestyle: 2.6.0, pyflakes: 2.2.0) CPython 3.9.0 on Darwin
25+
3.8.4 (flake8-annotations: 2.6.0, mccabe: 0.6.1, pycodestyle: 2.6.0, pyflakes: 2.2.0) CPython 3.9.0 on Darwin
2326
```
2427

2528
## Table of Warnings
@@ -86,6 +89,61 @@ Allow omission of a return type hint for `__init__` if at least one argument is
8689

8790
Default: `False`
8891

92+
### `--dispatch-decorators`: `list[str]`
93+
Comma-separated list of decorators flake8-annotations should consider as dispatch decorators. Linting errors are suppressed for functions decorated with at least one of these functions.
94+
95+
Decorators are matched based on their attribute name. For example, `"singledispatch"` will match any of the following:
96+
* `import functools; @functools.singledispatch`
97+
* `import functools as fnctls; @fnctls.singledispatch`
98+
* `from functools import singledispatch; @singledispatch`
99+
100+
**NOTE:** Deeper imports, such as `a.b.singledispatch` are not supported.
101+
102+
See: [Generic Functions](#generic-functions) for additional information.
103+
104+
Default: `"singledispatch, singledispatchmethod"`
105+
106+
### `--overload-decorators`: `list[str]`
107+
Comma-separated list of decorators flake8-annotations should consider as [`typing.overload`](https://docs.python.org/3/library/typing.html#typing.overload) decorators.
108+
109+
Decorators are matched based on their attribute name. For example, `"overload"` will match any of the following:
110+
* `import typing; @typing.overload`
111+
* `import typing as t; @t.overload`
112+
* `from typing import overload; @overload`
113+
114+
**NOTE:** Deeper imports, such as `a.b.overload` are not supported.
115+
116+
See: [The `typing.overload` Decorator](#the-typingoverload-decorator) for additional information.
117+
118+
Default: `"overload"`
119+
120+
121+
## Generic Functions
122+
Per the Python Glossary, a [generic function](https://docs.python.org/3/glossary.html#term-generic-function) is defined as:
123+
124+
> A function composed of multiple functions implementing the same operation for different types. Which implementation should be used during a call is determined by the dispatch algorithm.
125+
126+
In the standard library we have some examples of decorators for implementing these generic functions: [`functools.singledispatch`](https://docs.python.org/3/library/functools.html#functools.singledispatch) and [`functools.singledispatchmethod`](https://docs.python.org/3/library/functools.html#functools.singledispatchmethod). In the spirit of the purpose of these decorators, errors for missing annotations for functions decorated with at least one of these are ignored.
127+
128+
For example, this code:
129+
130+
```py
131+
import functools
132+
133+
@functools.singledispatch
134+
def foo(a):
135+
print(a)
136+
137+
@foo.register
138+
def _(a: list) -> None:
139+
for idx, thing in enumerate(a):
140+
print(idx, thing)
141+
```
142+
143+
Will not raise any linting errors for `foo`.
144+
145+
Decorator(s) to treat as defining generic functions may be specified by the [`--dispatch-decorators`](#--dispatch-decorators-liststr) configuration option.
146+
89147
## The `typing.overload` Decorator
90148
Per the [`typing`](https://docs.python.org/3/library/typing.html#typing.overload) documentation:
91149

@@ -109,7 +167,7 @@ def foo(a):
109167

110168
Will not raise linting errors for missing annotations for the arguments & return of the non-decorated `foo` definition.
111169

112-
**NOTE:** If importing directly, the `typing.overload` decorator will not be recognized if it is imported with an alias (e.g. `from typing import overload as please_dont_do_this`). Aliasing of the `typing` module is supported (e.g. `import typing as t; @t.overload`).
170+
Decorator(s) to treat as `typing.overload` may be specified by the [`--overload-decorators`](#--overload-decorators-liststr) configuration option.
113171

114172
## Caveats for PEP 484-style Type Comments
115173

flake8_annotations/__init__.py

Lines changed: 85 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,34 @@
1717

1818
PY_GTE_38 = False
1919

20-
__version__ = "2.5.0"
20+
__version__ = "2.6.0"
2121

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

3030
AST_FUNCTION_TYPES = Union[ast.FunctionDef, ast.AsyncFunctionDef]
3131
AST_DEF_NODES = Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef]
32+
AST_DECORATOR_NODES = Union[ast.Attribute, ast.Call, ast.Name]
3233

3334

3435
class Argument:
3536
"""Represent a function argument & its metadata."""
3637

38+
__slots__ = [
39+
"argname",
40+
"lineno",
41+
"col_offset",
42+
"annotation_type",
43+
"has_type_annotation",
44+
"has_3107_annotation",
45+
"has_type_comment",
46+
]
47+
3748
def __init__(
3849
self,
3950
argname: str,
@@ -102,6 +113,21 @@ class Function:
102113
aligns with ast's naming convention.
103114
"""
104115

116+
__slots__ = [
117+
"name",
118+
"lineno",
119+
"col_offset",
120+
"function_type",
121+
"is_class_method",
122+
"class_decorator_type",
123+
"is_return_annotated",
124+
"has_type_comment",
125+
"has_only_none_returns",
126+
"is_nested",
127+
"decorator_list",
128+
"args",
129+
]
130+
105131
def __init__(
106132
self,
107133
name: str,
@@ -113,8 +139,8 @@ def __init__(
113139
is_return_annotated: bool = False,
114140
has_type_comment: bool = False,
115141
has_only_none_returns: bool = True,
116-
is_overload_decorated: bool = False,
117142
is_nested: bool = False,
143+
decorator_list: List[AST_DECORATOR_NODES] = None,
118144
args: List[Argument] = None,
119145
):
120146
self.name = name
@@ -126,8 +152,8 @@ def __init__(
126152
self.is_return_annotated = is_return_annotated
127153
self.has_type_comment = has_type_comment
128154
self.has_only_none_returns = has_only_none_returns
129-
self.is_overload_decorated = is_overload_decorated
130155
self.is_nested = is_nested
156+
self.decorator_list = decorator_list
131157
self.args = args
132158

133159
def is_fully_annotated(self) -> bool:
@@ -142,14 +168,57 @@ def is_dynamically_typed(self) -> bool:
142168
"""Determine if the function is dynamically typed, defined as completely lacking hints."""
143169
return not any(arg.has_type_annotation for arg in self.args)
144170

145-
def get_missed_annotations(self) -> List:
171+
def get_missed_annotations(self) -> List[Argument]:
146172
"""Provide a list of arguments with missing type annotations."""
147173
return [arg for arg in self.args if not arg.has_type_annotation]
148174

149-
def get_annotated_arguments(self) -> List:
175+
def get_annotated_arguments(self) -> List[Argument]:
150176
"""Provide a list of arguments with type annotations."""
151177
return [arg for arg in self.args if arg.has_type_annotation]
152178

179+
def has_decorator(self, check_decorators: Set[str]) -> bool:
180+
"""
181+
Determine whether the function node is decorated by any of the provided decorators.
182+
183+
Decorator matching is done against the provided `check_decorators` set, allowing the user
184+
to specify any expected aliasing in the relevant flake8 configuration option. Decorators are
185+
assumed to be either a module attribute (e.g. `@typing.overload`) or name
186+
(e.g. `@overload`). For the case of a module attribute, only the attribute is checked
187+
against `overload_decorators`.
188+
189+
NOTE: Deeper decorator imports (e.g. `a.b.overload`) are not explicitly supported
190+
"""
191+
for decorator in self.decorator_list:
192+
# Drop to a helper to allow for simpler handling of callable decorators
193+
return self._decorator_checker(decorator, check_decorators)
194+
else:
195+
return False
196+
197+
def _decorator_checker(
198+
self, decorator: AST_DECORATOR_NODES, check_decorators: Set[str]
199+
) -> bool:
200+
"""
201+
Check the provided decorator for a match against the provided set of check names.
202+
203+
Decorators are assumed to be of the following form:
204+
* `a.name` or `a.name()`
205+
* `name` or `name()`
206+
207+
NOTE: Deeper imports (e.g. `a.b.name`) are not explicitly supported.
208+
"""
209+
if isinstance(decorator, ast.Name):
210+
# e.g. `@overload`, where `decorator.id` will be the name
211+
if decorator.id in check_decorators:
212+
return True
213+
elif isinstance(decorator, ast.Attribute):
214+
# e.g. `@typing.overload`, where `decorator.attr` will be the name
215+
if decorator.attr in check_decorators:
216+
return True
217+
elif isinstance(decorator, ast.Call): # pragma: no branch
218+
# e.g. `@overload()` or `@typing.overload()`, where `decorator.func` will be `ast.Name`
219+
# or `ast.Attribute`, which we can check recursively
220+
return self._decorator_checker(decorator.func, check_decorators)
221+
153222
def __str__(self) -> str:
154223
"""
155224
Format the Function object into a readable representation.
@@ -176,8 +245,8 @@ def __repr__(self) -> str:
176245
f"is_return_annotated={self.is_return_annotated}, "
177246
f"has_type_comment={self.has_type_comment}, "
178247
f"has_only_none_returns={self.has_only_none_returns}, "
179-
f"is_overload_decorated={self.is_overload_decorated}, "
180248
f"is_nested={self.is_nested}, "
249+
f"decorator_list={self.decorator_list}, "
181250
f"args={self.args}"
182251
")"
183252
)
@@ -194,7 +263,6 @@ def from_function_node(cls, node: AST_FUNCTION_TYPES, lines: List[str], **kwargs
194263
following kwargs will be overridden:
195264
* function_type
196265
* class_decorator_type
197-
* is_overload_decorated
198266
* args
199267
"""
200268
# Extract function types from function name
@@ -204,8 +272,8 @@ def from_function_node(cls, node: AST_FUNCTION_TYPES, lines: List[str], **kwargs
204272
if kwargs.get("is_class_method", False):
205273
kwargs["class_decorator_type"] = cls.get_class_decorator_type(node)
206274

207-
# Check for `typing.overload` decorator
208-
kwargs["is_overload_decorated"] = cls.has_overload_decorator(node)
275+
# Store raw decorator list for use by property methods
276+
kwargs["decorator_list"] = node.decorator_list
209277

210278
new_function = cls(node.name, node.lineno, node.col_offset, **kwargs)
211279

@@ -261,25 +329,25 @@ def colon_seeker(node: AST_FUNCTION_TYPES, lines: List[str]) -> Tuple[int, int]:
261329
if node.lineno == node.body[0].lineno:
262330
return Function._single_line_colon_seeker(node, lines[node.lineno - 1])
263331

264-
def_end_lineno = node.body[0].lineno - 1
265-
266332
# With Python < 3.8, the function node includes the docstring & the body does not, so
267333
# we have rewind through any docstrings, if present, before looking for the def colon
334+
# We should end up with lines[def_end_lineno - 1] having the colon
335+
def_end_lineno = node.body[0].lineno
268336
if not PY_GTE_38:
269-
# This list index is a little funky, since we've already subtracted 1 outside of this
270-
# context, we can leave it as-is since it will index the list to the line prior to where
271-
# the function node's body begins.
272337
# If the docstring is on one line then no rewinding is necessary.
273-
n_triple_quotes = lines[def_end_lineno].count('"""')
338+
n_triple_quotes = lines[def_end_lineno - 1].count('"""')
274339
if n_triple_quotes == 1: # pragma: no branch
275340
# Docstring closure, rewind until the opening is found & take the line prior
276341
while True:
277342
def_end_lineno -= 1
278343
if '"""' in lines[def_end_lineno - 1]:
279344
# Docstring has closed
280-
def_end_lineno -= 1
281345
break
282346

347+
# Once we've gotten here, we've found the line where the docstring begins, so we have
348+
# to step up one more line to get to the close of the def
349+
def_end_lineno -= 1
350+
283351
# Use str.rfind() to account for annotations on the same line, definition closure should
284352
# be the last : on the line
285353
def_end_col_offset = lines[def_end_lineno - 1].rfind(":") + 1
@@ -407,29 +475,6 @@ def get_class_decorator_type(
407475
else:
408476
return None
409477

410-
@staticmethod
411-
def has_overload_decorator(function_node: AST_FUNCTION_TYPES) -> bool:
412-
"""
413-
Determine whether the provided function node is decorated by `typing.overload`.
414-
415-
NOTE: For simplicity, this check will not identify the `typing.overload` decorator if it has
416-
been aliased (e.g. `from typing import overload as please_dont_do_this`).
417-
"""
418-
# Depending on how the decorator is imported, it will appear in the function node's
419-
# decorator list as an instance of either `ast.Name` (e.g. `@overload`) or `ast.Attribute`
420-
# (e.g. `@typing.overload`)
421-
# It assumed that the overload decorator will never be present as a callable
422-
# (e.g. `@overload()`)
423-
for decorator in function_node.decorator_list:
424-
if isinstance(decorator, ast.Name):
425-
if decorator.id == "overload":
426-
return True
427-
elif isinstance(decorator, ast.Attribute):
428-
if decorator.attr == "overload":
429-
return True
430-
else:
431-
return False
432-
433478

434479
class FunctionVisitor(ast.NodeVisitor):
435480
"""An ast.NodeVisitor instance for walking the AST and describing all contained functions."""

0 commit comments

Comments
 (0)