Skip to content

Commit e97cefe

Browse files
committed
Add check for alphabetical order.
1 parent 69ef56a commit e97cefe

File tree

4 files changed

+299
-24
lines changed

4 files changed

+299
-24
lines changed

doc-source/usage.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,21 @@ Flake8 codes
1212
.. flake8-codes:: flake8_dunder_all
1313

1414
DALL000
15+
DALL001
16+
DALL002
17+
18+
19+
For the ``DALL001`` option there exists a configuration option (``dunder-all-alphabetical``)
20+
which controls the alphabetical grouping expected of ``__all__``.
21+
The options are:
22+
23+
* ``ignore`` -- ``__all__`` should be sorted alphabetically ignoring case, e.g. ``['bar', 'Baz', 'foo']``
24+
* ``lower`` -- group lowercase names first, then uppercase names, e.g. ``['bar', 'foo', 'Baz']``
25+
* ``upper`` -- group uppercase names first, then uppercase names, e.g. ``['Baz', 'Foo', 'bar']``
26+
27+
If the ``dunder-all-alphabetical`` option is omitted the ``DALL001`` check is disabled.
28+
29+
.. versionchanged:: 0.5.0 Added the ``DALL001`` and ``DALL002`` checks.
1530

1631
.. note::
1732

flake8_dunder_all/__init__.py

Lines changed: 103 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,16 @@
3232
# stdlib
3333
import ast
3434
import 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
3840
from consolekit.terminal_colours import Fore
3941
from domdf_python_tools.paths import PathPlus
4042
from domdf_python_tools.typing import PathLike
4143
from domdf_python_tools.utils import stderr_writer
44+
from flake8.options.manager import OptionManager # type: ignore
4245

4346
# this package
4447
from flake8_dunder_all.utils import find_noqa, get_docstring_lineno, mark_text_ranges
@@ -49,9 +52,32 @@
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

5465
DALL000 = "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

5783
class 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

282373
def check_and_add_all(filename: PathLike, quote_type: str = '"', use_tuple: bool = False) -> int:
283374
"""

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ click>=7.1.2
33
consolekit>=0.8.1
44
domdf-python-tools>=2.6.0
55
flake8>=3.7
6+
natsort>=8.0.2

0 commit comments

Comments
 (0)