Skip to content

Commit

Permalink
Add check for alphabetical order.
Browse files Browse the repository at this point in the history
  • Loading branch information
domdfcoding committed Jan 31, 2022
1 parent 253c307 commit 5e5285f
Show file tree
Hide file tree
Showing 4 changed files with 294 additions and 21 deletions.
15 changes: 15 additions & 0 deletions doc-source/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,21 @@ Flake8 codes
.. flake8-codes:: flake8_dunder_all

DALL000
DALL001
DALL002


For the ``DALL001`` option there exists a configuration option (``dunder-all-alphabetical``)
which controls the alphabetical grouping expected of ``__all__``.
The options are:

* ``ignore`` -- ``__all__`` should be sorted alphabetically ignoring case, e.g. ``['bar', 'Baz', 'foo']``
* ``lower`` -- group lowercase names first, then uppercase names, e.g. ``['bar', 'foo', 'Baz']``
* ``upper`` -- group uppercase names first, then uppercase names, e.g. ``['Baz', 'Foo', 'bar']``

If the ``dunder-all-alphabetical`` is omitted the ``DALL001`` check is disabled.

.. versionchanged:: 0.2.0 Added the ``DALL001`` and ``DALL002`` checks.


``ensure-dunder-all`` script
Expand Down
117 changes: 104 additions & 13 deletions flake8_dunder_all/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,16 @@
# stdlib
import ast
import sys
from typing import Any, Generator, Set, Tuple, Type, Union
from enum import Enum
from typing import Any, Generator, Optional, Sequence, Set, Tuple, Type, Union, cast

# 3rd party
import natsort
from consolekit.terminal_colours import Fore
from domdf_python_tools.paths import PathPlus
from domdf_python_tools.typing import PathLike
from domdf_python_tools.utils import stderr_writer
from flake8.options.manager import OptionManager # type: ignore
from flake8.style_guide import find_noqa # type: ignore

# this package
Expand All @@ -50,9 +53,32 @@
__version__: str = "0.1.8"
__email__: str = "dominic@davis-foster.co.uk"

__all__ = ["Visitor", "Plugin", "check_and_add_all", "DALL000"]
__all__ = [
"check_and_add_all",
"AlphabeticalOptions",
"DALL000",
"DALL001",
"DALL002",
"Plugin",
"Visitor",
]

DALL000 = "DALL000 Module lacks __all__."
DALL000 = "DALL000 Module lacks __all__"
DALL001 = "DALL001 __all__ not sorted alphabetically"
DALL002 = "DALL002 __all__ not a list of strings"


class AlphabeticalOptions(Enum):
"""
Enum of possible values for the ``--dunder-all-alphabetical`` option.
.. versionadded:: 0.2.0
"""

UPPER = "upper"
LOWER = "lower"
IGNORE = "ignore"
NONE = "none"


class Visitor(ast.NodeVisitor):
Expand All @@ -62,30 +88,56 @@ class Visitor(ast.NodeVisitor):
:param use_endlineno: Flag to indicate whether the end_lineno functionality is available.
This functionality is available on Python 3.8 and above, or when the tree has been passed through
:func:`flake8_dunder_all.utils.mark_text_ranges``.
.. versionchanged:: 0.2.0
Added the ``sorted_upper_first``, ``sorted_lower_first`` and ``all_lineno`` attributes.
"""

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

def __init__(self, use_endlineno: bool = False) -> None:
self.found_all = False
self.members = set()
self.last_import = 0
self.use_endlineno = use_endlineno
self.all_members = None
self.all_lineno = -1

def visit_Name(self, node: ast.Name):
"""
Visit a variable.
:param node: The node being visited.
"""
def visit_Assign(self, node: ast.Assign) -> None: # noqa: D102
targets = []
for t in node.targets:
if isinstance(t, ast.Name):
targets.append(t.id)

if node.id == "__all__":
if "__all__" in targets:
self.found_all = True
else:
self.generic_visit(node)
self.all_lineno = node.lineno
self.all_members = self._parse_all(cast(ast.List, node.value))

def visit_AnnAssign(self, node: ast.AnnAssign) -> None: # noqa: D102
if isinstance(node.target, ast.Name):
if node.target.id == "__all__":
self.all_lineno = node.lineno
self.found_all = True
self.all_members = self._parse_all(cast(ast.List, node.value))

@staticmethod
def _parse_all(all_node: ast.List) -> Optional[Sequence[str]]:
try:
all_ = ast.literal_eval(all_node)
except ValueError:
return None

if not isinstance(all_, Sequence):
return None

return all_

def handle_def(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef]):
"""
Expand Down Expand Up @@ -193,6 +245,7 @@ class Plugin:

name: str = __name__
version: str = __version__ #: The plugin version
dunder_all_alphabetical: AlphabeticalOptions = AlphabeticalOptions.NONE

def __init__(self, tree: ast.AST):
self._tree = tree
Expand All @@ -213,12 +266,50 @@ def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]:
visitor.visit(self._tree)

if visitor.found_all:
return
if visitor.all_members is None:
yield visitor.all_lineno, 0, DALL002, type(self)

elif self.dunder_all_alphabetical == AlphabeticalOptions.IGNORE:
# Alphabetical, upper or lower don't matter
sorted_alphabetical = natsort.natsorted(visitor.all_members, key=str.lower)
if visitor.all_members != sorted_alphabetical:
yield visitor.all_lineno, 0, f"{DALL001}", type(self)
elif self.dunder_all_alphabetical == AlphabeticalOptions.UPPER:
# Alphabetical, uppercase grouped first
sorted_alphabetical = natsort.natsorted(visitor.all_members)
if visitor.all_members != sorted_alphabetical:
yield visitor.all_lineno, 0, f"{DALL001} (uppercase first)", type(self)
elif self.dunder_all_alphabetical == AlphabeticalOptions.LOWER:
# Alphabetical, lowercase grouped first
sorted_alphabetical = natsort.natsorted(visitor.all_members, alg=natsort.ns.LOWERCASEFIRST)
if visitor.all_members != sorted_alphabetical:
yield visitor.all_lineno, 0, f"{DALL001} (lowercase first)", type(self)

elif not visitor.members:
return

else:
yield 1, 0, DALL000, type(self)

@classmethod
def add_options(cls, option_manager: OptionManager) -> None: # noqa: D102 # pragma: no cover

option_manager.add_option(
"--dunder-all-alphabetical",
choices=[member.value for member in AlphabeticalOptions],
parse_from_config=True,
default=AlphabeticalOptions.NONE.value,
help=(
"Require entries in '__all__' to be alphabetical ([upper] or [lower]case first)."
"(Default: %(default)s)"
),
)

@classmethod
def parse_options(cls, options): # noqa: D102 # pragma: no cover
# note: this sets the option on the class and not the instance
cls.dunder_all_alphabetical = AlphabeticalOptions(options.dunder_all_alphabetical)


def check_and_add_all(filename: PathLike, quote_type: str = '"') -> int:
"""
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ click>=7.1.2
consolekit>=0.8.1
domdf-python-tools>=2.6.0
flake8>=3.7
natsort>=8.0.2

0 comments on commit 5e5285f

Please sign in to comment.