Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
52 changes: 41 additions & 11 deletions flake8_routable.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Python imports
import abc
import ast
import importlib.metadata as importlib_metadata
import tokenize
Expand Down Expand Up @@ -66,7 +67,46 @@ def is_all_passed(self):
)


class Visitor(ast.NodeVisitor):
class ImportVisitor(abc.ABC):
"""Base class for checking imports with Flake8."""

def _check_relative_import(self, node: ast.ImportFrom):
"""ROU106 - Relative imports are not allowed."""
if node.level > 0:
self.errors.append((node.lineno, node.col_offset, ROU106))

def _check_imports_from_tests(self, node: ast.ImportFrom):
"""ROU101 - Import from a tests directory."""
if not node.module:
return

if "tests" in node.module:
self.errors.append((node.lineno, node.col_offset, ROU101))

def _check_subpackage_model_imports(self, node: ast.ImportFrom):
"""ROU108 - Import from model module instead of sub-packages."""
if not node.module:
return

allow_subpackage_imports = [
"records.api",
"django",
"djmoney",
]

if any(node.module.startswith(allowed) for allowed in allow_subpackage_imports):
return

if ".models." in node.module:
self.errors.append((node.lineno, node.col_offset, ROU108))

def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
self._check_relative_import(node)
self._check_imports_from_tests(node)
self._check_subpackage_model_imports(node)


class Visitor(ImportVisitor, ast.NodeVisitor):
"""Linting errors that use the AST."""

def __init__(self) -> None:
Expand Down Expand Up @@ -148,16 +188,6 @@ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
else:
has_non_docstring_before_import = True

def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
if node.module is not None and "tests" in node.module:
self.errors.append((node.lineno, node.col_offset, ROU101))

if node.level > 0:
self.errors.append((node.lineno, node.col_offset, ROU106))

if node.module is not None and ".models." in node.module:
self.errors.append((node.lineno, node.col_offset, ROU108))

def visit_Set(self, node: ast.Set) -> None:
if not self._is_ordered(node.elts):
self.errors.append((node.lineno, node.col_offset, ROU103))
Expand Down
12 changes: 12 additions & 0 deletions tests/test_flake8_routable.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,18 @@ def test_non_model_named_like_a_model_subpackage_import(self):
error = results("from app.like_a_model.subpackage import ModelA, ModelB")
assert error == set()

def test_django_model_subpackage_import_allowed(self):
error = results("from django.db.models.manager import RelatedManager")
assert error == set()

def test_djmoney_model_subpackage_import_allowed(self):
error = results("from djmoney.models.fields import CurrencyField")
assert error == set()

def test_records_api_pseudo_model_subpackage_import_allowed(self):
error = results("from records.api.models.payments import SwiftChargeOption")
assert error == set()

def test_model_subpackage_import(self):
error = results("from app.models.subpackage import ModelA, ModelB")
assert error == {"1:0: ROU108 Import from model module instead of sub-packages"}
Expand Down