|
69 | 69 | DALL000 = "DALL000 Module lacks __all__." |
70 | 70 | DALL001 = "DALL001 __all__ not sorted alphabetically" |
71 | 71 | 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." |
72 | 74 |
|
73 | 75 |
|
74 | 76 | class AlphabeticalOptions(Enum): |
@@ -106,6 +108,8 @@ class Visitor(ast.NodeVisitor): |
106 | 108 |
|
107 | 109 | def __init__(self, use_endlineno: bool = False) -> None: |
108 | 110 | self.found_all = False |
| 111 | + self.found_lineno = -1 |
| 112 | + self.found_dir = False |
109 | 113 | self.members = set() |
110 | 114 | self.last_import = 0 |
111 | 115 | self.use_endlineno = use_endlineno |
@@ -177,6 +181,10 @@ def handle_def(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.Clas |
177 | 181 | if not node.name.startswith('_') and "overload" not in decorators: |
178 | 182 | self.members.add(node.name) |
179 | 183 |
|
| 184 | + if node.name == "__dir__": |
| 185 | + self.found_dir = True |
| 186 | + self.found_lineno = node.lineno |
| 187 | + |
180 | 188 | def visit_FunctionDef(self, node: ast.FunctionDef) -> None: |
181 | 189 | """ |
182 | 190 | Visit ``def foo(): ...``. |
@@ -306,14 +314,16 @@ class Plugin: |
306 | 314 | A Flake8 plugin which checks to ensure modules have defined ``__all__``. |
307 | 315 |
|
308 | 316 | :param tree: The abstract syntax tree (AST) to check. |
| 317 | + :param filename: The filename being checked. |
309 | 318 | """ |
310 | 319 |
|
311 | 320 | name: str = __name__ |
312 | 321 | version: str = __version__ #: The plugin version |
313 | 322 | dunder_all_alphabetical: AlphabeticalOptions = AlphabeticalOptions.NONE |
314 | 323 |
|
315 | | - def __init__(self, tree: ast.AST): |
| 324 | + def __init__(self, tree: ast.AST, filename: str): |
316 | 325 | self._tree = tree |
| 326 | + self._filename = filename |
317 | 327 |
|
318 | 328 | def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]: |
319 | 329 | """ |
@@ -351,11 +361,19 @@ def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]: |
351 | 361 | yield visitor.all_lineno, 0, f"{DALL001} (lowercase first).", type(self) |
352 | 362 |
|
353 | 363 | elif not visitor.members: |
354 | | - return |
| 364 | + pass |
355 | 365 |
|
356 | 366 | else: |
357 | 367 | yield 1, 0, DALL000, type(self) |
358 | 368 |
|
| 369 | + # Require top-level __dir__ function |
| 370 | + if not visitor.found_dir: |
| 371 | + if self._filename.endswith("__init__.py"): |
| 372 | + if visitor.members: |
| 373 | + yield 1, 0, DALL101, type(self) |
| 374 | + else: |
| 375 | + yield 1, 0, DALL100, type(self) |
| 376 | + |
359 | 377 | @classmethod |
360 | 378 | def add_options(cls, option_manager: OptionManager) -> None: # noqa: D102 # pragma: no cover |
361 | 379 |
|
|
0 commit comments