3232# stdlib
3333import ast
3434import sys
35- from typing import Any , Generator , Set , Tuple , Type , Union
35+ from enum import Enum
36+ from typing import Any , Generator , 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
4245from flake8 .style_guide import find_noqa # type: ignore
4346
4447# this package
5053__version__ : str = "0.1.8"
5154__email__ : str = "dominic@davis-foster.co.uk"
5255
53- __all__ = ["Visitor" , "Plugin" , "check_and_add_all" , "DALL000" ]
56+ __all__ = [
57+ "check_and_add_all" ,
58+ "AlphabeticalOptions" ,
59+ "DALL000" ,
60+ "DALL001" ,
61+ "DALL002" ,
62+ "Plugin" ,
63+ "Visitor" ,
64+ ]
5465
55- DALL000 = "DALL000 Module lacks __all__."
66+ DALL000 = "DALL000 Module lacks __all__"
67+ DALL001 = "DALL001 __all__ not sorted alphabetically"
68+ DALL002 = "DALL002 __all__ not a list of strings"
69+
70+
71+ class AlphabeticalOptions (Enum ):
72+ """
73+ Enum of possible values for the ``--dunder-all-alphabetical`` option.
74+
75+ .. versionadded:: 0.2.0
76+ """
77+
78+ UPPER = "upper"
79+ LOWER = "lower"
80+ IGNORE = "ignore"
81+ NONE = "none"
5682
5783
5884class Visitor (ast .NodeVisitor ):
@@ -62,30 +88,56 @@ class Visitor(ast.NodeVisitor):
6288 :param use_endlineno: Flag to indicate whether the end_lineno functionality is available.
6389 This functionality is available on Python 3.8 and above, or when the tree has been passed through
6490 :func:`flake8_dunder_all.utils.mark_text_ranges``.
91+
92+ .. versionchanged:: 0.2.0
93+
94+ Added the ``sorted_upper_first``, ``sorted_lower_first`` and ``all_lineno`` attributes.
6595 """
6696
6797 found_all : bool #: Flag to indicate a ``__all__`` declaration has been found in the AST.
6898 last_import : int #: The lineno of the last top-level import
6999 members : Set [str ] #: List of functions and classed defined in the AST
70100 use_endlineno : bool
101+ all_members : Optional [Sequence [str ]] #: The value of ``__all__``.
102+ all_lineno : int #: The line number where ``__all__`` is defined.
71103
72104 def __init__ (self , use_endlineno : bool = False ) -> None :
73105 self .found_all = False
74106 self .members = set ()
75107 self .last_import = 0
76108 self .use_endlineno = use_endlineno
109+ self .all_members = None
110+ self .all_lineno = - 1
77111
78- def visit_Name (self , node : ast .Name ):
79- """
80- Visit a variable.
81-
82- :param node: The node being visited.
83- """
112+ def visit_Assign (self , node : ast .Assign ) -> None : # noqa: D102
113+ targets = []
114+ for t in node .targets :
115+ if isinstance (t , ast .Name ):
116+ targets .append (t .id )
84117
85- if node . id == "__all__" :
118+ if "__all__" in targets :
86119 self .found_all = True
87- else :
88- self .generic_visit (node )
120+ self .all_lineno = node .lineno
121+ self .all_members = self ._parse_all (cast (ast .List , node .value ))
122+
123+ def visit_AnnAssign (self , node : ast .AnnAssign ) -> None : # noqa: D102
124+ if isinstance (node .target , ast .Name ):
125+ if node .target .id == "__all__" :
126+ self .all_lineno = node .lineno
127+ self .found_all = True
128+ self .all_members = self ._parse_all (cast (ast .List , node .value ))
129+
130+ @staticmethod
131+ def _parse_all (all_node : ast .List ) -> Optional [Sequence [str ]]:
132+ try :
133+ all_ = ast .literal_eval (all_node )
134+ except ValueError :
135+ return None
136+
137+ if not isinstance (all_ , Sequence ):
138+ return None
139+
140+ return all_
89141
90142 def handle_def (self , node : Union [ast .FunctionDef , ast .AsyncFunctionDef , ast .ClassDef ]):
91143 """
@@ -193,6 +245,7 @@ class Plugin:
193245
194246 name : str = __name__
195247 version : str = __version__ #: The plugin version
248+ dunder_all_alphabetical : AlphabeticalOptions = AlphabeticalOptions .NONE
196249
197250 def __init__ (self , tree : ast .AST ):
198251 self ._tree = tree
@@ -213,12 +266,50 @@ def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]:
213266 visitor .visit (self ._tree )
214267
215268 if visitor .found_all :
216- return
269+ if visitor .all_members is None :
270+ yield visitor .all_lineno , 0 , DALL002 , type (self )
271+
272+ elif self .dunder_all_alphabetical == AlphabeticalOptions .IGNORE :
273+ # Alphabetical, upper or lower don't matter
274+ sorted_alphabetical = natsort .natsorted (visitor .all_members , key = str .lower )
275+ if visitor .all_members != sorted_alphabetical :
276+ yield visitor .all_lineno , 0 , f"{ DALL001 } " , type (self )
277+ elif self .dunder_all_alphabetical == AlphabeticalOptions .UPPER :
278+ # Alphabetical, uppercase grouped first
279+ sorted_alphabetical = natsort .natsorted (visitor .all_members )
280+ if visitor .all_members != sorted_alphabetical :
281+ yield visitor .all_lineno , 0 , f"{ DALL001 } (uppercase first)" , type (self )
282+ elif self .dunder_all_alphabetical == AlphabeticalOptions .LOWER :
283+ # Alphabetical, lowercase grouped first
284+ sorted_alphabetical = natsort .natsorted (visitor .all_members , alg = natsort .ns .LOWERCASEFIRST )
285+ if visitor .all_members != sorted_alphabetical :
286+ yield visitor .all_lineno , 0 , f"{ DALL001 } (lowercase first)" , type (self )
287+
217288 elif not visitor .members :
218289 return
290+
219291 else :
220292 yield 1 , 0 , DALL000 , type (self )
221293
294+ @classmethod
295+ def add_options (cls , option_manager : OptionManager ) -> None : # noqa: D102 # pragma: no cover
296+
297+ option_manager .add_option (
298+ "--dunder-all-alphabetical" ,
299+ choices = [member .value for member in AlphabeticalOptions ],
300+ parse_from_config = True ,
301+ default = AlphabeticalOptions .NONE .value ,
302+ help = (
303+ "Require entries in '__all__' to be alphabetical ([upper] or [lower]case first)."
304+ "(Default: %(default)s)"
305+ ),
306+ )
307+
308+ @classmethod
309+ def parse_options (cls , options ): # noqa: D102 # pragma: no cover
310+ # note: this sets the option on the class and not the instance
311+ cls .dunder_all_alphabetical = AlphabeticalOptions (options .dunder_all_alphabetical )
312+
222313
223314def check_and_add_all (filename : PathLike , quote_type : str = '"' ) -> int :
224315 """
0 commit comments