Skip to content

Commit fcd6ee1

Browse files
authored
Add check for alphabetical order. (#36)
* Add check for alphabetical order. * Allow __all__ to be tuple (which brings slight benefits) * Use CliRunner from consolekit * Lint
1 parent 69ef56a commit fcd6ee1

File tree

7 files changed

+314
-29
lines changed

7 files changed

+314
-29
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: 107 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,56 @@
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 TYPE_CHECKING, 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[import]
4245

4346
# this package
4447
from flake8_dunder_all.utils import find_noqa, get_docstring_lineno, mark_text_ranges
4548

49+
if TYPE_CHECKING:
50+
# stdlib
51+
from argparse import Namespace
52+
4653
__author__: str = "Dominic Davis-Foster"
4754
__copyright__: str = "2020 Dominic Davis-Foster"
4855
__license__: str = "MIT"
4956
__version__: str = "0.4.1"
5057
__email__: str = "dominic@davis-foster.co.uk"
5158

52-
__all__ = ("Visitor", "Plugin", "check_and_add_all", "DALL000")
59+
__all__ = (
60+
"check_and_add_all",
61+
"AlphabeticalOptions",
62+
"DALL000",
63+
"DALL001",
64+
"DALL002",
65+
"Plugin",
66+
"Visitor",
67+
)
5368

5469
DALL000 = "DALL000 Module lacks __all__."
70+
DALL001 = "DALL001 __all__ not sorted alphabetically"
71+
DALL002 = "DALL002 __all__ not a list or tuple of strings."
72+
73+
74+
class AlphabeticalOptions(Enum):
75+
"""
76+
Enum of possible values for the ``--dunder-all-alphabetical`` option.
77+
78+
.. versionadded:: 0.5.0
79+
"""
80+
81+
UPPER = "upper"
82+
LOWER = "lower"
83+
IGNORE = "ignore"
84+
NONE = "none"
5585

5686

5787
class Visitor(ast.NodeVisitor):
@@ -61,30 +91,56 @@ class Visitor(ast.NodeVisitor):
6191
:param use_endlineno: Flag to indicate whether the end_lineno functionality is available.
6292
This functionality is available on Python 3.8 and above, or when the tree has been passed through
6393
:func:`flake8_dunder_all.utils.mark_text_ranges``.
94+
95+
.. versionchanged:: 0.5.0
96+
97+
Added the ``sorted_upper_first``, ``sorted_lower_first`` and ``all_lineno`` attributes.
6498
"""
6599

66100
found_all: bool #: Flag to indicate a ``__all__`` declaration has been found in the AST.
67101
last_import: int #: The lineno of the last top-level or conditional import
68102
members: Set[str] #: List of functions and classed defined in the AST
69103
use_endlineno: bool
104+
all_members: Optional[Sequence[str]] #: The value of ``__all__``.
105+
all_lineno: int #: The line number where ``__all__`` is defined.
70106

71107
def __init__(self, use_endlineno: bool = False) -> None:
72108
self.found_all = False
73109
self.members = set()
74110
self.last_import = 0
75111
self.use_endlineno = use_endlineno
112+
self.all_members = None
113+
self.all_lineno = -1
76114

77-
def visit_Name(self, node: ast.Name) -> None:
78-
"""
79-
Visit a variable.
80-
81-
:param node: The node being visited.
82-
"""
115+
def visit_Assign(self, node: ast.Assign) -> None: # noqa: D102
116+
targets = []
117+
for t in node.targets:
118+
if isinstance(t, ast.Name):
119+
targets.append(t.id)
83120

84-
if node.id == "__all__":
121+
if "__all__" in targets:
85122
self.found_all = True
86-
else:
87-
self.generic_visit(node)
123+
self.all_lineno = node.lineno
124+
self.all_members = self._parse_all(cast(ast.List, node.value))
125+
126+
def visit_AnnAssign(self, node: ast.AnnAssign) -> None: # noqa: D102
127+
if isinstance(node.target, ast.Name):
128+
if node.target.id == "__all__":
129+
self.all_lineno = node.lineno
130+
self.found_all = True
131+
self.all_members = self._parse_all(cast(ast.List, node.value))
132+
133+
@staticmethod
134+
def _parse_all(all_node: ast.List) -> Optional[Sequence[str]]:
135+
try:
136+
all_ = ast.literal_eval(all_node)
137+
except ValueError:
138+
return None
139+
140+
if not isinstance(all_, Sequence):
141+
return None
142+
143+
return all_
88144

89145
def handle_def(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef]) -> None:
90146
"""
@@ -252,6 +308,7 @@ class Plugin:
252308

253309
name: str = __name__
254310
version: str = __version__ #: The plugin version
311+
dunder_all_alphabetical: AlphabeticalOptions = AlphabeticalOptions.NONE
255312

256313
def __init__(self, tree: ast.AST):
257314
self._tree = tree
@@ -272,12 +329,50 @@ def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]:
272329
visitor.visit(self._tree)
273330

274331
if visitor.found_all:
275-
return
332+
if visitor.all_members is None:
333+
yield visitor.all_lineno, 0, DALL002, type(self)
334+
335+
elif self.dunder_all_alphabetical == AlphabeticalOptions.IGNORE:
336+
# Alphabetical, upper or lower don't matter
337+
sorted_alphabetical = natsort.natsorted(visitor.all_members, key=str.lower)
338+
if list(visitor.all_members) != sorted_alphabetical:
339+
yield visitor.all_lineno, 0, f"{DALL001}.", type(self)
340+
elif self.dunder_all_alphabetical == AlphabeticalOptions.UPPER:
341+
# Alphabetical, uppercase grouped first
342+
sorted_alphabetical = natsort.natsorted(visitor.all_members)
343+
if list(visitor.all_members) != sorted_alphabetical:
344+
yield visitor.all_lineno, 0, f"{DALL001} (uppercase first).", type(self)
345+
elif self.dunder_all_alphabetical == AlphabeticalOptions.LOWER:
346+
# Alphabetical, lowercase grouped first
347+
sorted_alphabetical = natsort.natsorted(visitor.all_members, alg=natsort.ns.LOWERCASEFIRST)
348+
if list(visitor.all_members) != sorted_alphabetical:
349+
yield visitor.all_lineno, 0, f"{DALL001} (lowercase first).", type(self)
350+
276351
elif not visitor.members:
277352
return
353+
278354
else:
279355
yield 1, 0, DALL000, type(self)
280356

357+
@classmethod
358+
def add_options(cls, option_manager: OptionManager) -> None: # noqa: D102 # pragma: no cover
359+
360+
option_manager.add_option(
361+
"--dunder-all-alphabetical",
362+
choices=[member.value for member in AlphabeticalOptions],
363+
parse_from_config=True,
364+
default=AlphabeticalOptions.NONE.value,
365+
help=(
366+
"Require entries in '__all__' to be alphabetical ([upper] or [lower]case first)."
367+
"(Default: %(default)s)"
368+
),
369+
)
370+
371+
@classmethod
372+
def parse_options(cls, options: "Namespace") -> None: # noqa: D102 # pragma: no cover
373+
# note: this sets the option on the class and not the instance
374+
cls.dunder_all_alphabetical = AlphabeticalOptions(options.dunder_all_alphabetical)
375+
281376

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

formate.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ known_third_party = [
3838
"flake8",
3939
"github",
4040
"importlib_metadata",
41+
"natsort",
4142
"pytest",
4243
"pytest_cov",
4344
"pytest_randomly",

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

tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)