Skip to content

Commit f17d744

Browse files
committed
feat: add DALL1xx for __dir__
Signed-off-by: Henry Schreiner <henryfs@princeton.edu>
1 parent 3427373 commit f17d744

File tree

5 files changed

+74
-7
lines changed

5 files changed

+74
-7
lines changed

flake8_dunder_all/__init__.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@
6969
DALL000 = "DALL000 Module lacks __all__."
7070
DALL001 = "DALL001 __all__ not sorted alphabetically"
7171
DALL002 = "DALL002 __all__ not a list or tuple of strings."
72+
DALL100 = "DALL100 Top-level __dir__ function definition is required."
73+
DALL101 = "DALL101 Top-level __dir__ function definition is required in __init__.py."
7274

7375

7476
class AlphabeticalOptions(Enum):
@@ -106,6 +108,8 @@ class Visitor(ast.NodeVisitor):
106108

107109
def __init__(self, use_endlineno: bool = False) -> None:
108110
self.found_all = False
111+
self.found_lineno = -1
112+
self.found_dir = False
109113
self.members = set()
110114
self.last_import = 0
111115
self.use_endlineno = use_endlineno
@@ -177,6 +181,10 @@ def handle_def(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.Clas
177181
if not node.name.startswith('_') and "overload" not in decorators:
178182
self.members.add(node.name)
179183

184+
if node.name == "__dir__":
185+
self.found_dir = True
186+
self.found_lineno = node.lineno
187+
180188
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
181189
"""
182190
Visit ``def foo(): ...``.
@@ -351,11 +359,20 @@ def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]:
351359
yield visitor.all_lineno, 0, f"{DALL001} (lowercase first).", type(self)
352360

353361
elif not visitor.members:
354-
return
362+
pass
355363

356364
else:
357365
yield 1, 0, DALL000, type(self)
358366

367+
# Require top-level __dir__ function
368+
if not visitor.found_dir:
369+
filename = getattr(self, "filename", None)
370+
if filename and filename.endswith("__init__.py"):
371+
if visitor.members:
372+
yield 1, 0, DALL101, type(self)
373+
else:
374+
yield 1, 0, DALL100, type(self)
375+
359376
@classmethod
360377
def add_options(cls, option_manager: OptionManager) -> None: # noqa: D102 # pragma: no cover
361378

tests/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88

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

1212

1313
testing_source_a = '''

tests/test_dir_required.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from __future__ import annotations
2+
3+
import ast
4+
import inspect
5+
from typing import Any
6+
7+
from flake8_dunder_all import Plugin
8+
9+
10+
def from_source(source: str, filename: str) -> list[tuple[int, int, str, type[Any]]]:
11+
source_clean = inspect.cleandoc(source)
12+
plugin = Plugin(ast.parse(source_clean))
13+
plugin.filename = filename
14+
return list(plugin.run())
15+
16+
17+
def test_dir_required_non_init():
18+
source = """
19+
import foo
20+
"""
21+
results = from_source(source, "module.py")
22+
assert any("DALL100" in r[2] for r in results)
23+
24+
def test_dir_required_non_init_with_dir():
25+
# __dir__ defined, should not yield DALL100
26+
source_with_dir = """
27+
def __dir__():
28+
return []\n"""
29+
results = from_source(source_with_dir, "module.py")
30+
assert not any("DALL100" in r[2] for r in results)
31+
32+
33+
def test_dir_required_empty():
34+
source = """\nimport foo\n"""
35+
# No __dir__ defined but no members present, should not yield DALL101
36+
results = from_source(source, "__init__.py")
37+
assert not any("DALL101" in r[2] for r in results)
38+
39+
def test_dir_required_init():
40+
source = """\nimport foo\n\nclass Foo: ...\n"""
41+
# No __dir__ defined, should yield DALL101
42+
results = from_source(source, "__init__.py")
43+
assert any("DALL101" in r[2] for r in results)
44+
45+
def test_dir_required_init_with_dir():
46+
# __dir__ defined, should not yield DALL101
47+
source_with_dir = """\ndef __dir__():\n return []\n"""
48+
results = from_source(source_with_dir, "__init__.py")
49+
assert not any("DALL101" in r[2] for r in results)

tests/test_flake8_dunder_all.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ def test_plugin(source: str, expects: Set[str]):
137137
def test_plugin_alphabetical(source: str, expects: Set[str], dunder_all_alphabetical: AlphabeticalOptions):
138138
plugin = Plugin(ast.parse(source))
139139
plugin.dunder_all_alphabetical = dunder_all_alphabetical
140-
assert {"{}:{}: {}".format(*r) for r in plugin.run()} == expects
140+
assert {"{}:{}: {}".format(*r) for r in plugin.run() if "DALL0" in r[2]} == expects
141141

142142

143143
@pytest.mark.parametrize(
@@ -212,7 +212,7 @@ def test_plugin_alphabetical_ann_assign(
212212
):
213213
plugin = Plugin(ast.parse(source))
214214
plugin.dunder_all_alphabetical = dunder_all_alphabetical
215-
assert {"{}:{}: {}".format(*r) for r in plugin.run()} == expects
215+
assert {"{}:{}: {}".format(*r) for r in plugin.run() if "DALL0" in r[2]} == expects
216216

217217

218218
@pytest.mark.parametrize(
@@ -232,13 +232,13 @@ def test_plugin_alphabetical_not_list(source: str, dunder_all_alphabetical: Alph
232232
plugin = Plugin(ast.parse(source))
233233
plugin.dunder_all_alphabetical = dunder_all_alphabetical
234234
msg = "1:0: DALL002 __all__ not a list or tuple of strings."
235-
assert {"{}:{}: {}".format(*r) for r in plugin.run()} == {msg}
235+
assert {"{}:{}: {}".format(*r) for r in plugin.run() if "DALL0" in r[2]} == {msg}
236236

237237

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

243243

244244
@pytest.mark.parametrize(

tests/test_subprocess.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def test_subprocess(tmp_pathplus: PathPlus, monkeypatch):
2424
assert result.stderr == b''
2525
assert result.stdout == b"""\
2626
demo.py:1:1: DALL000 Module lacks __all__.
27+
demo.py:1:1: DALL100 Top-level __dir__ function definition is required.
2728
demo.py:2:1: W191 indentation contains tabs
2829
demo.py:2:1: W293 blank line contains whitespace
2930
demo.py:4:1: W191 indentation contains tabs
@@ -84,7 +85,7 @@ def test_subprocess_noqa(tmp_pathplus: PathPlus, monkeypatch):
8485
monkeypatch.delenv("COV_CORE_DATAFILE", raising=False)
8586
monkeypatch.setenv("PYTHONWARNINGS", "ignore")
8687

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

8990
with in_directory(tmp_pathplus):
9091
result = subprocess.run(

0 commit comments

Comments
 (0)