Skip to content
Open
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
22 changes: 20 additions & 2 deletions flake8_dunder_all/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@
DALL000 = "DALL000 Module lacks __all__."
DALL001 = "DALL001 __all__ not sorted alphabetically"
DALL002 = "DALL002 __all__ not a list or tuple of strings."
DALL100 = "DALL100 Top-level __dir__ function definition is required."
DALL101 = "DALL101 Top-level __dir__ function definition is required in __init__.py."


class AlphabeticalOptions(Enum):
Expand Down Expand Up @@ -106,6 +108,8 @@ class Visitor(ast.NodeVisitor):

def __init__(self, use_endlineno: bool = False) -> None:
self.found_all = False
self.found_lineno = -1
self.found_dir = False
self.members = set()
self.last_import = 0
self.use_endlineno = use_endlineno
Expand Down Expand Up @@ -177,6 +181,10 @@ def handle_def(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.Clas
if not node.name.startswith('_') and "overload" not in decorators:
self.members.add(node.name)

if node.name == "__dir__":
self.found_dir = True
self.found_lineno = node.lineno

def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
"""
Visit ``def foo(): ...``.
Expand Down Expand Up @@ -306,14 +314,16 @@ class Plugin:
A Flake8 plugin which checks to ensure modules have defined ``__all__``.

:param tree: The abstract syntax tree (AST) to check.
:param filename: The filename being checked.
"""

name: str = __name__
version: str = __version__ #: The plugin version
dunder_all_alphabetical: AlphabeticalOptions = AlphabeticalOptions.NONE

def __init__(self, tree: ast.AST):
def __init__(self, tree: ast.AST, filename: str):
self._tree = tree
self._filename = filename

def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]:
"""
Expand Down Expand Up @@ -351,11 +361,19 @@ def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]:
yield visitor.all_lineno, 0, f"{DALL001} (lowercase first).", type(self)

elif not visitor.members:
return
pass

else:
yield 1, 0, DALL000, type(self)

# Require top-level __dir__ function
if not visitor.found_dir:
if self._filename.endswith("__init__.py"):
if visitor.members:
yield 1, 0, DALL101, type(self)
else:
yield 1, 0, DALL100, type(self)

@classmethod
def add_options(cls, option_manager: OptionManager) -> None: # noqa: D102 # pragma: no cover

Expand Down
2 changes: 1 addition & 1 deletion tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@


def results(s: str) -> Set[str]:
return {"{}:{}: {}".format(*r) for r in Plugin(ast.parse(s)).run()}
return {"{}:{}: {}".format(*r) for r in Plugin(ast.parse(s), "mod.py").run() if "DALL0" in r[2]}


testing_source_a = '''
Expand Down
53 changes: 53 additions & 0 deletions tests/test_dir_required.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from __future__ import annotations

# stdlib
import ast
import inspect
from typing import Any

# this package
from flake8_dunder_all import Plugin


def from_source(source: str, filename: str) -> list[tuple[int, int, str, type[Any]]]:
source_clean = inspect.cleandoc(source)
plugin = Plugin(ast.parse(source_clean), filename)
return list(plugin.run())


def test_dir_required_non_init():
source = """
import foo
"""
results = from_source(source, "module.py")
assert any("DALL100" in r[2] for r in results)


def test_dir_required_non_init_with_dir():
# __dir__ defined, should not yield DALL100
source_with_dir = """
def __dir__():
return []\n"""
results = from_source(source_with_dir, "module.py")
assert not any("DALL100" in r[2] for r in results)


def test_dir_required_empty():
source = """\nimport foo\n"""
# No __dir__ defined but no members present, should not yield DALL101
results = from_source(source, "__init__.py")
assert not any("DALL101" in r[2] for r in results)


def test_dir_required_init():
source = """\nimport foo\n\nclass Foo: ...\n"""
# No __dir__ defined, should yield DALL101
results = from_source(source, "__init__.py")
assert any("DALL101" in r[2] for r in results)


def test_dir_required_init_with_dir():
# __dir__ defined, should not yield DALL101
source_with_dir = """\ndef __dir__():\n return []\n"""
results = from_source(source_with_dir, "__init__.py")
assert not any("DALL101" in r[2] for r in results)
16 changes: 8 additions & 8 deletions tests/test_flake8_dunder_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,9 @@ def test_plugin(source: str, expects: Set[str]):
]
)
def test_plugin_alphabetical(source: str, expects: Set[str], dunder_all_alphabetical: AlphabeticalOptions):
plugin = Plugin(ast.parse(source))
plugin = Plugin(ast.parse(source), "mod.py")
plugin.dunder_all_alphabetical = dunder_all_alphabetical
assert {"{}:{}: {}".format(*r) for r in plugin.run()} == expects
assert {"{}:{}: {}".format(*r) for r in plugin.run() if "DALL0" in r[2]} == expects


@pytest.mark.parametrize(
Expand Down Expand Up @@ -210,9 +210,9 @@ def test_plugin_alphabetical_ann_assign(
expects: Set[str],
dunder_all_alphabetical: AlphabeticalOptions,
):
plugin = Plugin(ast.parse(source))
plugin = Plugin(ast.parse(source), "mod.py")
plugin.dunder_all_alphabetical = dunder_all_alphabetical
assert {"{}:{}: {}".format(*r) for r in plugin.run()} == expects
assert {"{}:{}: {}".format(*r) for r in plugin.run() if "DALL0" in r[2]} == expects


@pytest.mark.parametrize(
Expand All @@ -229,16 +229,16 @@ def test_plugin_alphabetical_ann_assign(
]
)
def test_plugin_alphabetical_not_list(source: str, dunder_all_alphabetical: AlphabeticalOptions):
plugin = Plugin(ast.parse(source))
plugin = Plugin(ast.parse(source), "mod.py")
plugin.dunder_all_alphabetical = dunder_all_alphabetical
msg = "1:0: DALL002 __all__ not a list or tuple of strings."
assert {"{}:{}: {}".format(*r) for r in plugin.run()} == {msg}
assert {"{}:{}: {}".format(*r) for r in plugin.run() if "DALL0" in r[2]} == {msg}


def test_plugin_alphabetical_tuple():
plugin = Plugin(ast.parse("__all__ = ('bar',\n'foo')"))
plugin = Plugin(ast.parse("__all__ = ('bar',\n'foo')"), "mod.py")
plugin.dunder_all_alphabetical = AlphabeticalOptions.IGNORE
assert {"{}:{}: {}".format(*r) for r in plugin.run()} == set()
assert {"{}:{}: {}".format(*r) for r in plugin.run() if "DALL0" in r[2]} == set()


@pytest.mark.parametrize(
Expand Down
3 changes: 2 additions & 1 deletion tests/test_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def test_subprocess(tmp_pathplus: PathPlus, monkeypatch):
assert result.stderr == b''
assert result.stdout == b"""\
demo.py:1:1: DALL000 Module lacks __all__.
demo.py:1:1: DALL100 Top-level __dir__ function definition is required.
demo.py:2:1: W191 indentation contains tabs
demo.py:2:1: W293 blank line contains whitespace
demo.py:4:1: W191 indentation contains tabs
Expand Down Expand Up @@ -84,7 +85,7 @@ def test_subprocess_noqa(tmp_pathplus: PathPlus, monkeypatch):
monkeypatch.delenv("COV_CORE_DATAFILE", raising=False)
monkeypatch.setenv("PYTHONWARNINGS", "ignore")

(tmp_pathplus / "demo.py").write_text("# noq" + "a: DALL000\n\n\t\ndef foo():\n\tpass\n\t")
(tmp_pathplus / "demo.py").write_text(" # noqa: DALL000,DALL100 \n\n\t\ndef foo():\n\tpass\n\t")

with in_directory(tmp_pathplus):
result = subprocess.run(
Expand Down
Loading