3232# stdlib
3333import ast
3434import sys
35- from typing import Any , Generator , Iterator , List , Set , Tuple , Type , Union
35+ from enum import Enum
36+ from typing import Any , Generator , Iterator , List , Optional , Sequence , Set , Tuple , Type , Union , cast
3637
3738# 3rd party
39+ import natsort
3840from consolekit .terminal_colours import Fore
3941from domdf_python_tools .paths import PathPlus
4042from domdf_python_tools .typing import PathLike
4143from domdf_python_tools .utils import stderr_writer
44+ from flake8 .options .manager import OptionManager # type: ignore
4245
4346# this package
4447from flake8_dunder_all .utils import find_noqa , get_docstring_lineno , mark_text_ranges
4952__version__ : str = "0.4.1"
5053__email__ :
str = "[email protected] " 5154
52- __all__ = ("Visitor" , "Plugin" , "check_and_add_all" , "DALL000" )
55+ __all__ = (
56+ "check_and_add_all" ,
57+ "AlphabeticalOptions" ,
58+ "DALL000" ,
59+ "DALL001" ,
60+ "DALL002" ,
61+ "Plugin" ,
62+ "Visitor" ,
63+ )
5364
5465DALL000 = "DALL000 Module lacks __all__."
66+ DALL001 = "DALL001 __all__ not sorted alphabetically"
67+ DALL002 = "DALL002 __all__ not a list of strings."
68+
69+
70+ class AlphabeticalOptions (Enum ):
71+ """
72+ Enum of possible values for the ``--dunder-all-alphabetical`` option.
73+
74+ .. versionadded:: 0.5.0
75+ """
76+
77+ UPPER = "upper"
78+ LOWER = "lower"
79+ IGNORE = "ignore"
80+ NONE = "none"
5581
5682
5783class Visitor (ast .NodeVisitor ):
@@ -61,30 +87,56 @@ class Visitor(ast.NodeVisitor):
6187 :param use_endlineno: Flag to indicate whether the end_lineno functionality is available.
6288 This functionality is available on Python 3.8 and above, or when the tree has been passed through
6389 :func:`flake8_dunder_all.utils.mark_text_ranges``.
90+
91+ .. versionchanged:: 0.5.0
92+
93+ Added the ``sorted_upper_first``, ``sorted_lower_first`` and ``all_lineno`` attributes.
6494 """
6595
6696 found_all : bool #: Flag to indicate a ``__all__`` declaration has been found in the AST.
6797 last_import : int #: The lineno of the last top-level or conditional import
6898 members : Set [str ] #: List of functions and classed defined in the AST
6999 use_endlineno : bool
100+ all_members : Optional [Sequence [str ]] #: The value of ``__all__``.
101+ all_lineno : int #: The line number where ``__all__`` is defined.
70102
71103 def __init__ (self , use_endlineno : bool = False ) -> None :
72104 self .found_all = False
73105 self .members = set ()
74106 self .last_import = 0
75107 self .use_endlineno = use_endlineno
108+ self .all_members = None
109+ self .all_lineno = - 1
76110
77- def visit_Name (self , node : ast .Name ) -> None :
78- """
79- Visit a variable.
111+ def visit_Assign (self , node : ast .Assign ) -> None : # noqa: D102
112+ targets = []
113+ for t in node .targets :
114+ if isinstance (t , ast .Name ):
115+ targets .append (t .id )
80116
81- :param node: The node being visited.
82- """
83-
84- if node .id == "__all__" :
117+ if "__all__" in targets :
85118 self .found_all = True
86- else :
87- self .generic_visit (node )
119+ self .all_lineno = node .lineno
120+ self .all_members = self ._parse_all (cast (ast .List , node .value ))
121+
122+ def visit_AnnAssign (self , node : ast .AnnAssign ) -> None : # noqa: D102
123+ if isinstance (node .target , ast .Name ):
124+ if node .target .id == "__all__" :
125+ self .all_lineno = node .lineno
126+ self .found_all = True
127+ self .all_members = self ._parse_all (cast (ast .List , node .value ))
128+
129+ @staticmethod
130+ def _parse_all (all_node : ast .List ) -> Optional [Sequence [str ]]:
131+ try :
132+ all_ = ast .literal_eval (all_node )
133+ except ValueError :
134+ return None
135+
136+ if not isinstance (all_ , Sequence ):
137+ return None
138+
139+ return all_
88140
89141 def handle_def (self , node : Union [ast .FunctionDef , ast .AsyncFunctionDef , ast .ClassDef ]) -> None :
90142 """
@@ -252,6 +304,7 @@ class Plugin:
252304
253305 name : str = __name__
254306 version : str = __version__ #: The plugin version
307+ dunder_all_alphabetical : AlphabeticalOptions = AlphabeticalOptions .NONE
255308
256309 def __init__ (self , tree : ast .AST ):
257310 self ._tree = tree
@@ -272,12 +325,50 @@ def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]:
272325 visitor .visit (self ._tree )
273326
274327 if visitor .found_all :
275- return
328+ if visitor .all_members is None :
329+ yield visitor .all_lineno , 0 , DALL002 , type (self )
330+
331+ elif self .dunder_all_alphabetical == AlphabeticalOptions .IGNORE :
332+ # Alphabetical, upper or lower don't matter
333+ sorted_alphabetical = natsort .natsorted (visitor .all_members , key = str .lower )
334+ if visitor .all_members != sorted_alphabetical :
335+ yield visitor .all_lineno , 0 , f"{ DALL001 } ." , type (self )
336+ elif self .dunder_all_alphabetical == AlphabeticalOptions .UPPER :
337+ # Alphabetical, uppercase grouped first
338+ sorted_alphabetical = natsort .natsorted (visitor .all_members )
339+ if visitor .all_members != sorted_alphabetical :
340+ yield visitor .all_lineno , 0 , f"{ DALL001 } (uppercase first)." , type (self )
341+ elif self .dunder_all_alphabetical == AlphabeticalOptions .LOWER :
342+ # Alphabetical, lowercase grouped first
343+ sorted_alphabetical = natsort .natsorted (visitor .all_members , alg = natsort .ns .LOWERCASEFIRST )
344+ if visitor .all_members != sorted_alphabetical :
345+ yield visitor .all_lineno , 0 , f"{ DALL001 } (lowercase first)." , type (self )
346+
276347 elif not visitor .members :
277348 return
349+
278350 else :
279351 yield 1 , 0 , DALL000 , type (self )
280352
353+ @classmethod
354+ def add_options (cls , option_manager : OptionManager ) -> None : # noqa: D102 # pragma: no cover
355+
356+ option_manager .add_option (
357+ "--dunder-all-alphabetical" ,
358+ choices = [member .value for member in AlphabeticalOptions ],
359+ parse_from_config = True ,
360+ default = AlphabeticalOptions .NONE .value ,
361+ help = (
362+ "Require entries in '__all__' to be alphabetical ([upper] or [lower]case first)."
363+ "(Default: %(default)s)"
364+ ),
365+ )
366+
367+ @classmethod
368+ def parse_options (cls , options ): # noqa: D102 # pragma: no cover
369+ # note: this sets the option on the class and not the instance
370+ cls .dunder_all_alphabetical = AlphabeticalOptions (options .dunder_all_alphabetical )
371+
281372
282373def check_and_add_all (filename : PathLike , quote_type : str = '"' , use_tuple : bool = False ) -> int :
283374 """
0 commit comments