Skip to content

Add check for alphabetical order. #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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, List, Set, Tuple, Type, Union
from enum import Enum
from typing import Any, Generator, List, 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 = "[email protected]"

__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
Loading