6969DALL000 = "DALL000 Module lacks __all__."
7070DALL001 = "DALL001 __all__ not sorted alphabetically"
7171DALL002 = "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
7476class AlphabeticalOptions (Enum ):
@@ -95,9 +97,12 @@ class Visitor(ast.NodeVisitor):
9597 .. versionchanged:: 0.5.0
9698
9799 Added the ``sorted_upper_first``, ``sorted_lower_first`` and ``all_lineno`` attributes.
100+
101+ .. versionchanged:: 0.6.0 Added the ``found_dir`` attribute.
98102 """
99103
100104 found_all : bool #: Flag to indicate a ``__all__`` declaration has been found in the AST.
105+ found_dir : bool #: Flag to indicate a top-level ``__dir__`` function has been found in the AST.
101106 last_import : int #: The lineno of the last top-level or conditional import
102107 members : Set [str ] #: List of functions and classed defined in the AST
103108 use_endlineno : bool
@@ -106,6 +111,7 @@ class Visitor(ast.NodeVisitor):
106111
107112 def __init__ (self , use_endlineno : bool = False ) -> None :
108113 self .found_all = False
114+ self .found_dir = False
109115 self .members = set ()
110116 self .last_import = 0
111117 self .use_endlineno = use_endlineno
@@ -187,6 +193,9 @@ def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
187193 # Don't generic visit
188194 self .handle_def (node )
189195
196+ if node .name == "__dir__" :
197+ self .found_dir = True
198+
190199 def visit_AsyncFunctionDef (self , node : ast .AsyncFunctionDef ) -> None :
191200 """
192201 Visit ``async def foo(): ...``.
@@ -306,14 +315,18 @@ class Plugin:
306315 A Flake8 plugin which checks to ensure modules have defined ``__all__``.
307316
308317 :param tree: The abstract syntax tree (AST) to check.
318+ :param filename: The filename being checked.
319+
320+ .. versionchanged:: 0.6.0 Added ``filename`` argument.
309321 """
310322
311323 name : str = __name__
312324 version : str = __version__ #: The plugin version
313325 dunder_all_alphabetical : AlphabeticalOptions = AlphabeticalOptions .NONE
314326
315- def __init__ (self , tree : ast .AST ):
327+ def __init__ (self , tree : ast .AST , filename : str ):
316328 self ._tree = tree
329+ self ._filename = filename
317330
318331 def run (self ) -> Generator [Tuple [int , int , str , Type [Any ]], None , None ]:
319332 """
@@ -350,12 +363,16 @@ def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]:
350363 if list (visitor .all_members ) != sorted_alphabetical :
351364 yield visitor .all_lineno , 0 , f"{ DALL001 } (lowercase first)." , type (self )
352365
353- elif not visitor .members :
354- return
355-
356- else :
366+ elif visitor .members :
357367 yield 1 , 0 , DALL000 , type (self )
358368
369+ # Require a top-level __dir__, but only when the module defines public members
370+ if visitor .members and not visitor .found_dir :
371+ if self ._filename .endswith ("__init__.py" ):
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
0 commit comments