Skip to content

Commit 875ca7f

Browse files
authored
Release v2.0.1
2 parents fa11dad + afc88fa commit 875ca7f

17 files changed

+301
-146
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,6 @@ coverage.xml
4141
cov.xml
4242
htmlcov
4343
.pytest_cache/
44+
45+
# mypy
46+
.mypy_cache/

.pre-commit-config.yaml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
repos:
2-
- repo: local
2+
- repo: local
33
hooks:
44
- id: flake8
55
name: Flake8
@@ -8,3 +8,19 @@ repos:
88
language: python
99
types: [python]
1010
require_serial: true
11+
- repo: https://github.com/psf/black
12+
rev: stable
13+
hooks:
14+
- id: black
15+
- repo: https://github.com/pre-commit/pre-commit-hooks
16+
rev: v2.5.0
17+
hooks:
18+
- id: check-merge-conflict
19+
- id: check-toml
20+
- id: check-yaml
21+
- id: end-of-file-fixer
22+
- id: mixed-line-ending
23+
- repo: https://github.com/pre-commit/pygrep-hooks
24+
rev: v1.5.1
25+
hooks:
26+
- id: python-check-blanket-noqa

CHANGELOG.md

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

4+
## [v2.0.1]
5+
### Added
6+
* #71 Add `pep8-naming` to linting toolchain
7+
* Expand pre-commit hooks
8+
* Add `black`
9+
* Add `check-merge-conflict`
10+
* Add `check-toml`
11+
* Add `check-yaml`
12+
* Add `end-of-file-fixer`
13+
* Add `mixed-line-ending`
14+
* Add `python-check-blanket-noqa`
15+
16+
### Changed
17+
* Add argument names to `Argument` and `Function` `__repr__` methods to make the string more helpful to read
18+
19+
### Fixed
20+
* #70 Fix incorrect column index for missing return annotations when other annotations are present on the same line of source
21+
* #69 Fix misclassification of `None` returning functions when they contained nested functions with non-`None` returns (thanks @isidentical!)
22+
* #67 Fix methods of nested classes being improperly classified as "regular" functions (thanks @isidentical!)
23+
424
## [v2.0.0]
525
### Changed
626
* #64 Change prefix from `TYP` to `ANN` in order to deconflict with `flake8-typing-imports`

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21-
SOFTWARE.
21+
SOFTWARE.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ You can verify it's being picked up by invoking the following in your shell:
2323

2424
```bash
2525
$ flake8 --version
26-
3.7.8 (flake8-annotations: 2.0.0, mccabe: 0.6.1, pycodestyle: 2.5.0, pyflakes: 2.1.1) CPython 3.7.4 on Darwin
26+
3.7.8 (flake8-annotations: 2.0.1, mccabe: 0.6.1, pycodestyle: 2.5.0, pyflakes: 2.1.1) CPython 3.7.4 on Darwin
2727
```
2828

2929
## Table of Warnings

flake8_annotations/__init__.py

Lines changed: 75 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import sys
22
from itertools import zip_longest
3-
from typing import List, Union
3+
from typing import List, Set, Union
44

55
from flake8_annotations.enums import AnnotationType, ClassDecoratorType, FunctionType
66

@@ -17,14 +17,15 @@
1717

1818
PY_GTE_38 = False
1919

20-
__version__ = "2.0.0"
20+
__version__ = "2.0.1"
2121

2222
AST_ARG_TYPES = ("args", "vararg", "kwonlyargs", "kwarg")
2323
if PY_GTE_38:
2424
# Positional-only args introduced in Python 3.8
2525
AST_ARG_TYPES += ("posonlyargs",)
2626

2727
AST_FUNCTION_TYPES = Union[ast.FunctionDef, ast.AsyncFunctionDef]
28+
AST_DEF_NODES = Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef]
2829

2930

3031
class Argument:
@@ -60,8 +61,9 @@ def __str__(self) -> str:
6061
def __repr__(self) -> str:
6162
"""Format the Argument object into its "official" representation."""
6263
return (
63-
f"Argument({self.argname!r}, {self.lineno}, {self.col_offset}, {self.annotation_type}, "
64-
f"{self.has_type_annotation}, {self.has_3107_annotation}, {self.has_type_comment})"
64+
f"Argument(argname={self.argname!r}, lineno={self.lineno}, col_offset={self.col_offset}, " # noqa: E501
65+
f"annotation_type={self.annotation_type}, has_type_annotation={self.has_type_annotation}, " # noqa: E501
66+
f"has_3107_annotation={self.has_3107_annotation}, has_type_comment={self.has_type_comment})" # noqa: E501
6567
)
6668

6769
@classmethod
@@ -147,9 +149,12 @@ def __str__(self) -> str:
147149
def __repr__(self) -> str:
148150
"""Format the Function object into its "official" representation."""
149151
return (
150-
f"Function({self.name!r}, {self.lineno}, {self.col_offset}, {self.function_type}, "
151-
f"{self.is_class_method}, {self.class_decorator_type}, {self.is_return_annotated}, "
152-
f"{self.has_type_comment}, {self.has_only_none_returns}, {self.args})"
152+
f"Function(name={self.name!r}, lineno={self.lineno}, col_offset={self.col_offset}, "
153+
f"function_type={self.function_type}, is_class_method={self.is_class_method}, "
154+
f"class_decorator_type={self.class_decorator_type}, "
155+
f"is_return_annotated={self.is_return_annotated}, "
156+
f"has_type_comment={self.has_type_comment}, "
157+
f"has_only_none_returns={self.has_only_none_returns}, args={self.args})"
153158
)
154159

155160
@classmethod
@@ -192,7 +197,9 @@ def from_function_node(cls, node: AST_FUNCTION_TYPES, lines: List[str], **kwargs
192197
while True:
193198
# To account for multiline docstrings, rewind through the lines until we find the line
194199
# containing the :
195-
colon_loc = lines[def_end_lineno - 1].find(":")
200+
# Use str.rfind() to account for annotations on the same line, definition closure should
201+
# be the last : on the line
202+
colon_loc = lines[def_end_lineno - 1].rfind(":")
196203
if colon_loc == -1:
197204
def_end_lineno -= 1
198205
else:
@@ -215,7 +222,7 @@ def from_function_node(cls, node: AST_FUNCTION_TYPES, lines: List[str], **kwargs
215222
new_function = cls.try_type_comment(new_function, node)
216223

217224
# Check for the presence of non-`None` returns using the special-case return node visitor
218-
return_visitor = ReturnVisitor()
225+
return_visitor = ReturnVisitor(node)
219226
return_visitor.visit(node)
220227
new_function.has_only_none_returns = return_visitor.has_only_none_returns
221228

@@ -298,49 +305,35 @@ class FunctionVisitor(ast.NodeVisitor):
298305

299306
def __init__(self, lines: List[str]):
300307
self.lines = lines
301-
self.function_definitions = []
308+
self.function_definitions: List[Function] = []
309+
self._context: List[AST_DEF_NODES] = []
302310

303-
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
311+
def switch_context(self, node: AST_DEF_NODES) -> None:
304312
"""
305-
Handle a visit to a function definition.
313+
Utilize a context switcher as a generic function visitor in order to track function context.
306314
307-
Note: This will not contain class methods, these are included in the body of ClassDef
308-
statements
309-
"""
310-
self.function_definitions.append(Function.from_function_node(node, self.lines))
311-
self.generic_visit(node) # Walk through any nested functions
312-
313-
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
314-
"""
315-
Handle a visit to a coroutine definition.
316-
317-
Note: This will not contain class methods, these are included in the body of ClassDef
318-
statements
319-
"""
320-
self.function_definitions.append(Function.from_function_node(node, self.lines))
321-
self.generic_visit(node) # Walk through any nested functions
315+
Without keeping track of context, it's challenging to reliably differentiate class methods
316+
from "regular" functions, especially in the case of nested classes.
322317
323-
def visit_ClassDef(self, node: ast.ClassDef) -> None:
318+
Thank you for the inspiration @isidentical :)
324319
"""
325-
Handle a visit to a class definition.
320+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
321+
# Check for non-empty context first to prevent IndexErrors for non-nested nodes
322+
if self._context and isinstance(self._context[-1], ast.ClassDef):
323+
# Check if current context is a ClassDef node & pass the appropriate flag
324+
self.function_definitions.append(
325+
Function.from_function_node(node, self.lines, is_class_method=True)
326+
)
327+
else:
328+
self.function_definitions.append(Function.from_function_node(node, self.lines))
326329

327-
Class methods will all be contained in the body of the node
328-
"""
329-
method_nodes = [
330-
child_node
331-
for child_node in node.body
332-
if isinstance(child_node, (ast.FunctionDef, ast.AsyncFunctionDef))
333-
]
334-
self.function_definitions.extend(
335-
[
336-
Function.from_function_node(method_node, self.lines, is_class_method=True)
337-
for method_node in method_nodes
338-
]
339-
)
330+
self._context.append(node)
331+
self.generic_visit(node)
332+
self._context.pop()
340333

341-
# Use ast.NodeVisitor.generic_visit to start down the nested method chain
342-
for sub_node in node.body:
343-
self.generic_visit(sub_node)
334+
visit_FunctionDef = switch_context
335+
visit_AsyncFunctionDef = switch_context
336+
visit_ClassDef = switch_context
344337

345338

346339
class ReturnVisitor(ast.NodeVisitor):
@@ -353,13 +346,29 @@ class ReturnVisitor(ast.NodeVisitor):
353346
If the function node being visited has no return statement, or contains only return
354347
statement(s) that explicitly return `None`, the `instance.has_only_none_returns` flag will be
355348
set to `True`.
349+
350+
Due to the generic visiting being done, we need to keep track of the context in which a
351+
non-`None` return node is found. These functions are added to a set that is checked to see
352+
whether nor not the parent node is present.
356353
"""
357354

358-
def __init__(self):
359-
self.has_only_none_returns = True
355+
def __init__(self, parent_node: AST_FUNCTION_TYPES):
356+
self.parent_node = parent_node
357+
self._context: List[AST_FUNCTION_TYPES] = []
358+
self._non_none_return_nodes: Set[AST_FUNCTION_TYPES] = set()
359+
360+
@property
361+
def has_only_none_returns(self) -> bool:
362+
"""Return `True` if the parent node isn't in the visited nodes that don't return `None`."""
363+
return self.parent_node not in self._non_none_return_nodes
360364

361365
def visit_Return(self, node: ast.Return) -> None:
362-
"""Check each Return node to see if it returns anything other than `None`."""
366+
"""
367+
Check each Return node to see if it returns anything other than `None`.
368+
369+
If the node being visited returns anything other than `None`, its parent context is added to
370+
the set of non-returning child nodes of the parent node.
371+
"""
363372
if node.value is not None:
364373
# In the event of an explicit `None` return (`return None`), the node body will be an
365374
# instance of either `ast.Constant` (3.8+) or `ast.NameConstant`, which we need to check
@@ -368,4 +377,21 @@ def visit_Return(self, node: ast.Return) -> None:
368377
if node.value.value is None:
369378
return
370379

371-
self.has_only_none_returns = False
380+
self._non_none_return_nodes.add(self._context[-1])
381+
382+
def switch_context(self, node: AST_FUNCTION_TYPES) -> None:
383+
"""
384+
Utilize a context switcher as a generic visitor in order to properly track function context.
385+
386+
Using a traditional `ast.generic_visit` setup, return nodes of nested functions are visited
387+
without any knowledge of their context, causing the top-level function to potentially be
388+
mis-classified.
389+
390+
Thank you for the inspiration @isidentical :)
391+
"""
392+
self._context.append(node)
393+
self.generic_visit(node)
394+
self._context.pop()
395+
396+
visit_FunctionDef = switch_context
397+
visit_AsyncFunctionDef = switch_context

flake8_annotations/checker.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from argparse import Namespace
22
from functools import lru_cache
3-
from typing import Generator, List
3+
from typing import Generator, List, Tuple
44

55
from flake8.options.manager import OptionManager
66
from flake8_annotations import (
@@ -20,6 +20,8 @@
2020
else:
2121
from typed_ast import ast3 as ast
2222

23+
FORMATTED_ERROR = Tuple[int, int, str, error_codes.Error]
24+
2325

2426
class TypeHintChecker:
2527
"""Top level checker for linting the presence of type hints in function definitions."""
@@ -32,8 +34,9 @@ def __init__(self, tree: ast.Module, lines: List[str]):
3234
# Request `lines` here and join to allow for correct handling of input from stdin
3335
self.lines = lines
3436
self.tree = self.get_typed_tree("".join(lines)) # flake8 doesn't strip newlines
37+
self.suppress_none_returning: bool # Set by flake8's config parser
3538

36-
def run(self) -> Generator[error_codes.Error, None, None]:
39+
def run(self) -> Generator[FORMATTED_ERROR, None, None]:
3740
"""
3841
This method is called by flake8 to perform the actual check(s) on the source code.
3942

0 commit comments

Comments
 (0)