diff --git a/.bumpversion.cfg b/.bumpversion.cfg index e5e9e27..3a47b4c 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.4.1 +current_version = 0.5.0 commit = True tag = True diff --git a/.github/workflows/conda_ci.yml b/.github/workflows/conda_ci.yml index f600213..a020b4d 100644 --- a/.github/workflows/conda_ci.yml +++ b/.github/workflows/conda_ci.yml @@ -24,14 +24,15 @@ jobs: - name: Setup Python 🐍 uses: "actions/setup-python@v5" with: - python-version: "3.8" + python-version: "3.11" - name: Setup Conda - uses: conda-incubator/setup-miniconda@v2 + uses: conda-incubator/setup-miniconda@v2.1.1 with: activate-environment: env conda-build-version: 3.28.4 - python-version: "3.8" + miniconda-version: py311_24.1.2-0 + python-version: "3.11" miniforge-variant: Mambaforge - name: Install dependencies 🔧 @@ -57,7 +58,7 @@ jobs: - name: "Install package" run: | - $CONDA/bin/conda install -c file://$(pwd)/conda-bld flake8-dunder-all=0.4.1=py_1 -y || exit 1 + $CONDA/bin/conda install -c file://$(pwd)/conda-bld flake8-dunder-all=0.5.0=py_1 -y || exit 1 - name: "Run Tests" run: | diff --git a/.github/workflows/flake8.yml b/.github/workflows/flake8.yml index 0a8c0c3..af1b394 100644 --- a/.github/workflows/flake8.yml +++ b/.github/workflows/flake8.yml @@ -16,7 +16,7 @@ permissions: jobs: Run: name: "Flake8" - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" steps: - name: Checkout 🛎️ diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 10c6f30..bd74787 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: - os: ['ubuntu-20.04', 'windows-2019'] + os: ['ubuntu-22.04', 'windows-2019'] fail-fast: false steps: diff --git a/.github/workflows/python_ci.yml b/.github/workflows/python_ci.yml index b4655ed..c67e2c5 100644 --- a/.github/workflows/python_ci.yml +++ b/.github/workflows/python_ci.yml @@ -22,24 +22,22 @@ jobs: runs-on: "windows-2019" continue-on-error: ${{ matrix.config.experimental }} env: - USING_COVERAGE: '3.6,3.7,3.8,3.9,3.10,3.11,3.12,3.13.0-alpha.5,pypy-3.6,pypy-3.7,pypy-3.8,pypy-3.9' + USING_COVERAGE: '3.7,3.8,3.9,3.10,3.11,3.12,3.13,pypy-3.7,pypy-3.8,pypy-3.9' strategy: fail-fast: False matrix: config: - - {python-version: "3.6", testenvs: "py36-flake8{4,5},build", experimental: False} - {python-version: "3.7", testenvs: "py37-flake8{4,5},build", experimental: False} - {python-version: "3.8", testenvs: "py38-flake8{4,5,6,7},build", experimental: False} - {python-version: "3.9", testenvs: "py39-flake8{4,5,6,7},build", experimental: False} - {python-version: "3.10", testenvs: "py310-flake8{4,5,6,7},build", experimental: False} - {python-version: "3.11", testenvs: "py311-flake8{4,5,6,7},build", experimental: False} - {python-version: "3.12", testenvs: "py312-flake8{5,6,7},build", experimental: False} - - {python-version: "3.13.0-alpha.5", testenvs: "py313-dev-flake8{5,6,7},build", experimental: True} - - {python-version: "pypy-3.6", testenvs: "pypy36-flake8{4,5}", experimental: False} + - {python-version: "3.13", testenvs: "py313-dev-flake8{5,6,7},build", experimental: True} - {python-version: "pypy-3.7", testenvs: "pypy37-flake8{4,5},build", experimental: False} - {python-version: "pypy-3.8", testenvs: "pypy38-flake8{4,5,6,7},build", experimental: False} - - {python-version: "pypy-3.9", testenvs: "pypy39-flake8{4,5,6,7},build", experimental: True} + - {python-version: "pypy-3.9-v7.3.15", testenvs: "pypy39-flake8{4,5,6,7},build", experimental: True} steps: - name: Checkout 🛎️ @@ -80,3 +78,4 @@ jobs: with: name: "coverage-${{ matrix.config.python-version }}" path: .coverage + include-hidden-files: true diff --git a/.github/workflows/python_ci_linux.yml b/.github/workflows/python_ci_linux.yml index 178c720..32c826c 100644 --- a/.github/workflows/python_ci_linux.yml +++ b/.github/workflows/python_ci_linux.yml @@ -19,25 +19,23 @@ permissions: jobs: tests: - name: "ubuntu-20.04 / Python ${{ matrix.config.python-version }}" - runs-on: "ubuntu-20.04" + name: "ubuntu-22.04 / Python ${{ matrix.config.python-version }}" + runs-on: "ubuntu-22.04" continue-on-error: ${{ matrix.config.experimental }} env: - USING_COVERAGE: '3.6,3.7,3.8,3.9,3.10,3.11,3.12,3.13.0-alpha.5,pypy-3.6,pypy-3.7,pypy-3.8,pypy-3.9' + USING_COVERAGE: '3.7,3.8,3.9,3.10,3.11,3.12,3.13,pypy-3.7,pypy-3.8,pypy-3.9' strategy: fail-fast: False matrix: config: - - {python-version: "3.6", testenvs: "py36-flake8{4,5},build", experimental: False} - {python-version: "3.7", testenvs: "py37-flake8{4,5},build", experimental: False} - {python-version: "3.8", testenvs: "py38-flake8{4,5,6,7},build", experimental: False} - {python-version: "3.9", testenvs: "py39-flake8{4,5,6,7},build", experimental: False} - {python-version: "3.10", testenvs: "py310-flake8{4,5,6,7},build", experimental: False} - {python-version: "3.11", testenvs: "py311-flake8{4,5,6,7},build", experimental: False} - {python-version: "3.12", testenvs: "py312-flake8{5,6,7},build", experimental: False} - - {python-version: "3.13.0-alpha.5", testenvs: "py313-dev-flake8{5,6,7},build", experimental: True} - - {python-version: "pypy-3.6", testenvs: "pypy36-flake8{4,5},build", experimental: False} + - {python-version: "3.13", testenvs: "py313-dev-flake8{5,6,7},build", experimental: True} - {python-version: "pypy-3.7", testenvs: "pypy37-flake8{4,5},build", experimental: False} - {python-version: "pypy-3.8", testenvs: "pypy38-flake8{4,5,6,7},build", experimental: False} - {python-version: "pypy-3.9", testenvs: "pypy39-flake8{4,5,6,7},build", experimental: True} @@ -82,11 +80,12 @@ jobs: with: name: "coverage-${{ matrix.config.python-version }}" path: .coverage + include-hidden-files: true Coverage: needs: tests - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" steps: - name: Checkout 🛎️ uses: "actions/checkout@v4" @@ -124,6 +123,7 @@ jobs: with: name: "combined-coverage" path: .coverage + include-hidden-files: true - name: "Upload Combined Coverage to Coveralls" if: ${{ steps.show.outcome != 'failure' }} @@ -135,7 +135,7 @@ jobs: Deploy: needs: tests - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" steps: - name: Checkout 🛎️ uses: "actions/checkout@v4" @@ -188,14 +188,15 @@ jobs: - name: Setup Python 🐍 uses: "actions/setup-python@v5" with: - python-version: 3.8 + python-version: 3.11 - name: Setup Conda - uses: conda-incubator/setup-miniconda@v2 + uses: conda-incubator/setup-miniconda@v2.1.1 with: activate-environment: env conda-build-version: 3.28.4 - python-version: "3.8" + miniconda-version: py311_24.1.2-0 + python-version: "3.11" miniforge-variant: Mambaforge - name: Install dependencies 🔧 @@ -208,6 +209,7 @@ jobs: $CONDA/bin/conda config --set always_yes yes --set changeps1 no $CONDA/bin/conda update -n base conda $CONDA/bin/conda info -a + $CONDA/bin/conda install conda-forge::py-lief=0.14.1 $CONDA/bin/conda config --add channels conda-forge $CONDA/bin/conda config --add channels domdfcoding diff --git a/.github/workflows/python_ci_macos.yml b/.github/workflows/python_ci_macos.yml index 67e5454..7cda7bd 100644 --- a/.github/workflows/python_ci_macos.yml +++ b/.github/workflows/python_ci_macos.yml @@ -18,27 +18,26 @@ permissions: jobs: tests: - name: "macos-latest / Python ${{ matrix.config.python-version }}" - runs-on: "macos-latest" + name: "macos-${{ matrix.config.os-ver }} / Python ${{ matrix.config.python-version }}" + runs-on: "macos-${{ matrix.config.os-ver }}" continue-on-error: ${{ matrix.config.experimental }} env: - USING_COVERAGE: '3.6,3.7,3.8,3.9,3.10,3.11,3.12,3.13.0-alpha.5,pypy-3.7,pypy-3.8,pypy-3.9' + USING_COVERAGE: '3.7,3.8,3.9,3.10,3.11,3.12,3.13,pypy-3.7,pypy-3.8,pypy-3.9' strategy: fail-fast: False matrix: config: - - {python-version: "3.6", testenvs: "py36-flake8{4,5},build", experimental: False} - - {python-version: "3.7", testenvs: "py37-flake8{4,5},build", experimental: False} - - {python-version: "3.8", testenvs: "py38-flake8{4,5,6,7},build", experimental: False} - - {python-version: "3.9", testenvs: "py39-flake8{4,5,6,7},build", experimental: False} - - {python-version: "3.10", testenvs: "py310-flake8{4,5,6,7},build", experimental: False} - - {python-version: "3.11", testenvs: "py311-flake8{4,5,6,7},build", experimental: False} - - {python-version: "3.12", testenvs: "py312-flake8{5,6,7},build", experimental: False} - - {python-version: "3.13.0-alpha.5", testenvs: "py313-dev-flake8{5,6,7},build", experimental: True} - - {python-version: "pypy-3.7", testenvs: "pypy37-flake8{4,5},build", experimental: False} - - {python-version: "pypy-3.8", testenvs: "pypy38-flake8{4,5,6,7},build", experimental: False} - - {python-version: "pypy-3.9", testenvs: "pypy39-flake8{4,5,6,7},build", experimental: True} + - {python-version: "3.7", os-ver: "13", testenvs: "py37-flake8{4,5},build", experimental: False} + - {python-version: "3.8", os-ver: "14", testenvs: "py38-flake8{4,5,6,7},build", experimental: False} + - {python-version: "3.9", os-ver: "14", testenvs: "py39-flake8{4,5,6,7},build", experimental: False} + - {python-version: "3.10", os-ver: "14", testenvs: "py310-flake8{4,5,6,7},build", experimental: False} + - {python-version: "3.11", os-ver: "14", testenvs: "py311-flake8{4,5,6,7},build", experimental: False} + - {python-version: "3.12", os-ver: "14", testenvs: "py312-flake8{5,6,7},build", experimental: False} + - {python-version: "3.13", os-ver: "14", testenvs: "py313-dev-flake8{5,6,7},build", experimental: True} + - {python-version: "pypy-3.7", os-ver: "13", testenvs: "pypy37-flake8{4,5},build", experimental: False} + - {python-version: "pypy-3.8", os-ver: "14", testenvs: "pypy38-flake8{4,5,6,7},build", experimental: False} + - {python-version: "pypy-3.9", os-ver: "14", testenvs: "pypy39-flake8{4,5,6,7},build", experimental: True} steps: - name: Checkout 🛎️ @@ -79,3 +78,4 @@ jobs: with: name: "coverage-${{ matrix.config.python-version }}" path: .coverage + include-hidden-files: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4335130..96f0672 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ ci: repos: - repo: https://github.com/repo-helper/pyproject-parser - rev: v0.9.1 + rev: v0.13.0 hooks: - id: reformat-pyproject @@ -42,8 +42,8 @@ repos: exclude: ^(doc-source/conf|__pkginfo__|setup|tests/.*)\.py$ - id: bind-requirements - - repo: https://github.com/domdfcoding/flake8-dunder-all - rev: v0.3.1 + - repo: https://github.com/python-formate/flake8-dunder-all + rev: v0.4.1 hooks: - id: ensure-dunder-all files: ^flake8_dunder_all/.*\.py$ @@ -81,12 +81,12 @@ repos: - id: snippet-fmt - repo: https://github.com/python-formate/formate - rev: v0.7.0 + rev: v0.8.0 hooks: - id: formate exclude: ^(doc-source/conf|__pkginfo__|setup)\.(_)?py$ - - repo: https://github.com/domdfcoding/dep_checker + - repo: https://github.com/python-coincidence/dep_checker rev: v0.8.0 hooks: - id: dep_checker diff --git a/.readthedocs.yml b/.readthedocs.yml index e928b70..83fc025 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -13,7 +13,7 @@ python: - requirements: requirements.txt - requirements: doc-source/requirements.txt build: - os: ubuntu-20.04 + os: ubuntu-22.04 tools: python: '3.9' jobs: diff --git a/README.rst b/README.rst index 442b8b3..5ee13e9 100644 --- a/README.rst +++ b/README.rst @@ -101,7 +101,7 @@ flake8-dunder-all .. |language| image:: https://img.shields.io/github/languages/top/python-formate/flake8-dunder-all :alt: GitHub top language -.. |commits-since| image:: https://img.shields.io/github/commits-since/python-formate/flake8-dunder-all/v0.4.1 +.. |commits-since| image:: https://img.shields.io/github/commits-since/python-formate/flake8-dunder-all/v0.5.0 :target: https://github.com/python-formate/flake8-dunder-all/pulse :alt: GitHub commits since tagged version @@ -109,7 +109,7 @@ flake8-dunder-all :target: https://github.com/python-formate/flake8-dunder-all/commit/master :alt: GitHub last commit -.. |maintained| image:: https://img.shields.io/maintenance/yes/2024 +.. |maintained| image:: https://img.shields.io/maintenance/yes/2025 :alt: Maintenance .. |pypi-downloads| image:: https://img.shields.io/pypi/dm/flake8-dunder-all @@ -173,7 +173,7 @@ Sample ``.pre-commit-config.yaml``: rev: 3.8.1 hooks: - id: flake8 - additional_dependencies: [flake8-dunder-all==0.4.1] + additional_dependencies: [flake8-dunder-all==0.5.0] ``ensure-dunder-all`` script diff --git a/doc-source/conf.py b/doc-source/conf.py index d894441..4ae01b2 100644 --- a/doc-source/conf.py +++ b/doc-source/conf.py @@ -71,11 +71,32 @@ } +# Fix for pathlib issue with sphinxemoji on Python 3.9 and Sphinx 4.x +def copy_asset_files(app, exc): + # 3rd party + from domdf_python_tools.compat import importlib_resources + from sphinx.util.fileutil import copy_asset + + if exc: + return + + asset_files = ["twemoji.js", "twemoji.css"] + for path in asset_files: + path_str = os.fspath(importlib_resources.files("sphinxemoji") / path) + copy_asset(path_str, os.path.join(app.outdir, "_static")) + + def setup(app): # 3rd party from sphinx_toolbox.latex import better_header_layout + from sphinxemoji import sphinxemoji app.connect("config-inited", lambda app, config: better_header_layout(config)) + app.connect("build-finished", copy_asset_files) + app.add_js_file("https://unpkg.com/twemoji@latest/dist/twemoji.min.js") + app.add_js_file("twemoji.js") + app.add_css_file("twemoji.css") + app.add_transform(sphinxemoji.EmojiSubstitutions) needspace_amount = r"5\baselineskip" diff --git a/doc-source/index.rst b/doc-source/index.rst index 26b4ce9..68427b1 100644 --- a/doc-source/index.rst +++ b/doc-source/index.rst @@ -107,14 +107,14 @@ flake8-dunder-all :alt: GitHub top language .. |commits-since| github-shield:: - :commits-since: v0.4.1 + :commits-since: v0.5.0 :alt: GitHub commits since tagged version .. |commits-latest| github-shield:: :last-commit: :alt: GitHub last commit - .. |maintained| maintained-shield:: 2024 + .. |maintained| maintained-shield:: 2025 :alt: Maintenance .. |pypi-downloads| pypi-shield:: diff --git a/doc-source/requirements.txt b/doc-source/requirements.txt index 397c364..ccc1c24 100644 --- a/doc-source/requirements.txt +++ b/doc-source/requirements.txt @@ -12,11 +12,15 @@ sphinx-debuginfo>=0.2.2 sphinx-favicon>=0.2 sphinx-licenseinfo>=0.3.1 sphinx-notfound-page>=0.7.1 -sphinx-prompt>=1.1.0 sphinx-pyproject>=0.1.0 -sphinx-tabs>=1.1.13 sphinx-toolbox>=3.5.0 +sphinxcontrib-applehelp==1.0.4 +sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-htmlhelp==2.0.1 sphinxcontrib-httpdomain>=1.7.0 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-serializinghtml==1.1.5 sphinxemoji>=0.1.6 tabulate>=0.8.7 toctree-plus>=0.6.1 diff --git a/doc-source/usage.rst b/doc-source/usage.rst index 617c19d..e1b2ebd 100644 --- a/doc-source/usage.rst +++ b/doc-source/usage.rst @@ -12,10 +12,25 @@ 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`` option is omitted the ``DALL001`` check is disabled. + +.. versionchanged:: 0.5.0 Added the ``DALL001`` and ``DALL002`` checks. .. note:: - In version ``0.4.1`` the entry point changed from ``DALL`` to ``DAL``, due to changes in flake8 itself. + In version ``0.5.0`` the entry point changed from ``DALL`` to ``DAL``, due to changes in flake8 itself. However, the codes remain ``DALLXXX`` and should continue to work as normal. @@ -40,7 +55,7 @@ See `pre-commit `_ for instructions Sample ``.pre-commit-config.yaml``: -.. pre-commit:flake8:: 0.4.1 +.. pre-commit:flake8:: 0.5.0 Using the script as a pre-commit hook @@ -53,4 +68,4 @@ See `pre-commit `_ for instructions. Sample ``.pre-commit-config.yaml``: .. pre-commit:: - :rev: v0.4.1 + :rev: v0.5.0 diff --git a/flake8_dunder_all/__init__.py b/flake8_dunder_all/__init__.py index f9a0d95..6c9f804 100644 --- a/flake8_dunder_all/__init__.py +++ b/flake8_dunder_all/__init__.py @@ -32,26 +32,56 @@ # stdlib import ast import sys -from typing import Any, Generator, Iterator, List, Set, Tuple, Type, Union +from enum import Enum +from typing import TYPE_CHECKING, Any, Generator, Iterator, 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[import] # this package from flake8_dunder_all.utils import find_noqa, get_docstring_lineno, mark_text_ranges +if TYPE_CHECKING: + # stdlib + from argparse import Namespace + __author__: str = "Dominic Davis-Foster" __copyright__: str = "2020 Dominic Davis-Foster" __license__: str = "MIT" -__version__: str = "0.4.1" +__version__: str = "0.5.0" __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__." +DALL001 = "DALL001 __all__ not sorted alphabetically" +DALL002 = "DALL002 __all__ not a list or tuple of strings." + + +class AlphabeticalOptions(Enum): + """ + Enum of possible values for the ``--dunder-all-alphabetical`` option. + + .. versionadded:: 0.5.0 + """ + + UPPER = "upper" + LOWER = "lower" + IGNORE = "ignore" + NONE = "none" class Visitor(ast.NodeVisitor): @@ -61,30 +91,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.5.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 or conditional 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) -> None: - """ - 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]) -> None: """ @@ -252,6 +308,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 @@ -272,12 +329,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 list(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 list(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 list(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: "Namespace") -> None: # 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 = '"', use_tuple: bool = False) -> int: """ diff --git a/formate.toml b/formate.toml index 72ea626..f127306 100644 --- a/formate.toml +++ b/formate.toml @@ -6,21 +6,17 @@ noqa-reformat = 60 ellipsis-reformat = 70 squish_stubs = 80 -[config] -indent = "\t" -line_length = 115 - [hooks.yapf] priority = 30 -[hooks.isort] -priority = 50 - [hooks.yapf.kwargs] yapf_style = ".style.yapf" +[hooks.isort] +priority = 50 + [hooks.isort.kwargs] -indent = "\t\t" +indent = " " multi_line_output = 8 import_heading_stdlib = "stdlib" import_heading_thirdparty = "3rd party" @@ -42,6 +38,7 @@ known_third_party = [ "flake8", "github", "importlib_metadata", + "natsort", "pytest", "pytest_cov", "pytest_randomly", @@ -50,3 +47,7 @@ known_third_party = [ "requests", ] known_first_party = [ "flake8_dunder_all",] + +[config] +indent = " " +line_length = 115 diff --git a/pyproject.toml b/pyproject.toml index 1204cea..d2ad517 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,20 +4,19 @@ build-backend = "whey" [project] name = "flake8-dunder-all" -version = "0.4.1" +version = "0.5.0" description = "A Flake8 plugin and pre-commit hook which checks to ensure modules have defined '__all__'." readme = "README.rst" keywords = [ "flake8",] dynamic = [ "requires-python", "classifiers", "dependencies",] +[project.license] +file = "LICENSE" + [[project.authors]] name = "Dominic Davis-Foster" email = "dominic@davis-foster.co.uk" - -[project.license] -file = "LICENSE" - [project.urls] Homepage = "https://github.com/python-formate/flake8-dunder-all" "Issue Tracker" = "https://github.com/python-formate/flake8-dunder-all/issues" @@ -28,6 +27,9 @@ Documentation = "https://flake8-dunder-all.readthedocs.io/en/latest" ensure_dunder_all = "flake8_dunder_all.__main__:main" ensure-dunder-all = "flake8_dunder_all.__main__:main" +[project.entry-points."flake8.extension"] +DAL = "flake8_dunder_all:Plugin" + [tool.whey] base-classifiers = [ "Development Status :: 4 - Beta", @@ -36,7 +38,7 @@ base-classifiers = [ "Topic :: Utilities", "Typing :: Typed", ] -python-versions = [ "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12",] +python-versions = [ "3.7", "3.8", "3.9", "3.10", "3.11", "3.12",] python-implementations = [ "CPython", "PyPy",] platforms = [ "Windows", "macOS", "Linux",] license-key = "MIT" @@ -63,7 +65,6 @@ extensions = [ "sphinx.ext.mathjax", "sphinxcontrib.extras_require", "sphinx.ext.todo", - "sphinxemoji.sphinxemoji", "notfound.extension", "sphinx_copybutton", "sphinxcontrib.default_values", @@ -77,7 +78,6 @@ extensions = [ "sphinx_toolbox.more_autosummary.column_widths", "sphinx_favicon", ] -sphinxemoji_style = "twemoji" gitstamp_fmt = "%d %b %Y" templates_path = [ "_templates",] html_static_path = [ "_static",] @@ -137,13 +137,20 @@ show_error_codes = true [tool.snippet-fmt] directives = [ "code-block",] +[tool.snippet-fmt.languages.python] +reformat = true + +[tool.snippet-fmt.languages.TOML] +reformat = true + +[tool.snippet-fmt.languages.ini] + +[tool.snippet-fmt.languages.json] + [tool.mkrecipe] conda-channels = [ "conda-forge", "domdfcoding",] extras = "all" -[project.entry-points."flake8.extension"] -DAL = "flake8_dunder_all:Plugin" - [tool.dependency-dash."requirements.txt"] order = 10 @@ -154,13 +161,3 @@ include = false [tool.dependency-dash."doc-source/requirements.txt"] order = 30 include = false - -[tool.snippet-fmt.languages.python] -reformat = true - -[tool.snippet-fmt.languages.TOML] -reformat = true - -[tool.snippet-fmt.languages.ini] - -[tool.snippet-fmt.languages.json] diff --git a/repo_helper.yml b/repo_helper.yml index 6793e94..3570785 100644 --- a/repo_helper.yml +++ b/repo_helper.yml @@ -4,7 +4,7 @@ modname: flake8-dunder-all copyright_years: "2020-2022" author: "Dominic Davis-Foster" email: "dominic@davis-foster.co.uk" -version: "0.4.1" +version: "0.5.0" username: "python-formate" assignee: "domdfcoding" license: 'MIT' @@ -20,11 +20,6 @@ min_coverage: 100 # Versions to run tests for python_versions: - '3.6': - matrix_exclude: - flake8: - - 6 - - 7 '3.7': matrix_exclude: flake8: @@ -42,11 +37,6 @@ python_versions: matrix_exclude: flake8: - 4 - pypy36: - matrix_exclude: - flake8: - - 6 - - 7 pypy37: matrix_exclude: flake8: diff --git a/requirements.txt b/requirements.txt index ac873e6..6bd2469 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_flake8_dunder_all.py b/tests/test_flake8_dunder_all.py index d1c35dd..a7ede9d 100644 --- a/tests/test_flake8_dunder_all.py +++ b/tests/test_flake8_dunder_all.py @@ -7,7 +7,13 @@ # 3rd party import pytest from coincidence.regressions import AdvancedFileRegressionFixture -from common import ( +from consolekit.terminal_colours import Fore +from domdf_python_tools.paths import PathPlus + +# this package +from flake8_dunder_all import AlphabeticalOptions, Plugin, Visitor, check_and_add_all +from flake8_dunder_all.utils import mark_text_ranges +from tests.common import ( if_type_checking_else_source, if_type_checking_source, if_type_checking_try_finally_source, @@ -32,12 +38,6 @@ testing_source_m, testing_source_n ) -from consolekit.terminal_colours import Fore -from domdf_python_tools.paths import PathPlus - -# this package -from flake8_dunder_all import Visitor, check_and_add_all -from flake8_dunder_all.utils import mark_text_ranges @pytest.mark.parametrize( @@ -49,13 +49,16 @@ pytest.param(testing_source_b, {"1:0: DALL000 Module lacks __all__."}, id="function no __all__"), pytest.param(testing_source_c, {"1:0: DALL000 Module lacks __all__."}, id="class no __all__"), pytest.param( - testing_source_d, {"1:0: DALL000 Module lacks __all__."}, - id="function and class no __all__" + testing_source_d, + {"1:0: DALL000 Module lacks __all__."}, + id="function and class no __all__.", ), - pytest.param(testing_source_e, set(), id="function and class with __all__"), - pytest.param(testing_source_f, set(), id="function and class with __all__ and extra variable"), + pytest.param(testing_source_e, set(), id="function and class with __all__."), + pytest.param(testing_source_f, set(), id="function and class with __all__. and extra variable"), pytest.param( - testing_source_g, {"1:0: DALL000 Module lacks __all__."}, id="async function no __all__" + testing_source_g, + {"1:0: DALL000 Module lacks __all__."}, + id="async function no __all__", ), pytest.param(testing_source_h, set(), id="from import"), pytest.param(testing_source_i, {"1:0: DALL000 Module lacks __all__."}, id="lots of lines"), @@ -66,6 +69,176 @@ def test_plugin(source: str, expects: Set[str]): assert results(source) == expects +@pytest.mark.parametrize( + "source, dunder_all_alphabetical, expects", + [ + pytest.param( + "__all__ = ['foo', 'bar', 'Baz']", + AlphabeticalOptions.NONE, + set(), + id="NONE_wrong_order", + ), + pytest.param( + "__all__ = ['bar', 'Baz', 'foo']", + AlphabeticalOptions.NONE, + set(), + id="NONE_right_order_1", + ), + pytest.param( + "__all__ = ['Bar', 'baz', 'foo']", + AlphabeticalOptions.NONE, + set(), + id="NONE_right_order_2", + ), + pytest.param( + "__all__ = ['foo', 'bar', 'Baz']", + AlphabeticalOptions.IGNORE, + {"1:0: DALL001 __all__ not sorted alphabetically."}, + id="IGNORE_wrong_order", + ), + pytest.param( + "__all__ = ['bar', 'Baz', 'foo']", + AlphabeticalOptions.IGNORE, + set(), + id="IGNORE_right_order", + ), + pytest.param( + "__all__ = ['Baz', 'bar', 'foo']", + AlphabeticalOptions.LOWER, + {"1:0: DALL001 __all__ not sorted alphabetically (lowercase first)."}, + id="LOWER_wrong_order", + ), + pytest.param( + "__all__ = ['bar', 'foo', 'Baz']", + AlphabeticalOptions.LOWER, + set(), + id="LOWER_right_order", + ), + pytest.param( + "__all__ = ['bar', 'Baz', 'foo']", + AlphabeticalOptions.UPPER, + {"1:0: DALL001 __all__ not sorted alphabetically (uppercase first)."}, + id="UPPER_wrong_order", + ), + pytest.param( + "__all__ = ['Baz', 'bar', 'foo']", + AlphabeticalOptions.UPPER, + set(), + id="UPPER_right_order_1", + ), + pytest.param( + "__all__ = ['Baz', 'Foo', 'bar']", + AlphabeticalOptions.UPPER, + set(), + id="UPPER_right_order_2", + ), + ] + ) +def test_plugin_alphabetical(source: str, expects: Set[str], dunder_all_alphabetical: AlphabeticalOptions): + plugin = Plugin(ast.parse(source)) + plugin.dunder_all_alphabetical = dunder_all_alphabetical + assert {"{}:{}: {}".format(*r) for r in plugin.run()} == expects + + +@pytest.mark.parametrize( + "source, dunder_all_alphabetical, expects", + [ + pytest.param( + "__all__: List[str] = ['foo', 'bar', 'Baz']", + AlphabeticalOptions.NONE, + set(), + id="NONE_wrong_order", + ), + pytest.param( + "__all__: List[str] = ['bar', 'Baz', 'foo']", + AlphabeticalOptions.NONE, + set(), + id="NONE_right_order_1", + ), + pytest.param( + "__all__: List[str] = ['Bar', 'baz', 'foo']", + AlphabeticalOptions.NONE, + set(), + id="NONE_right_order_1", + ), + pytest.param( + "__all__: List[str] = ['foo', 'bar', 'Baz']", + AlphabeticalOptions.IGNORE, + {"1:0: DALL001 __all__ not sorted alphabetically."}, + id="IGNORE_wrong_order", + ), + pytest.param( + "__all__: List[str] = ['bar', 'Baz', 'foo']", + AlphabeticalOptions.IGNORE, + set(), + id="IGNORE_right_order", + ), + pytest.param( + "__all__: List[str] = ['Baz', 'bar', 'foo']", + AlphabeticalOptions.LOWER, + {"1:0: DALL001 __all__ not sorted alphabetically (lowercase first)."}, + id="LOWER_wrong_order", + ), + pytest.param( + "__all__: List[str] = ['bar', 'foo', 'Baz']", + AlphabeticalOptions.LOWER, + set(), + id="LOWER_right_order", + ), + pytest.param( + "__all__: List[str] = ['bar', 'Baz', 'foo']", + AlphabeticalOptions.UPPER, + {"1:0: DALL001 __all__ not sorted alphabetically (uppercase first)."}, + id="UPPER_wrong_order", + ), + pytest.param( + "__all__: List[str] = ['Baz', 'bar', 'foo']", + AlphabeticalOptions.UPPER, + set(), + id="UPPER_right_order_1", + ), + pytest.param( + "__all__: List[str] = ['Baz', 'Foo', 'bar']", + AlphabeticalOptions.UPPER, + set(), + id="UPPER_right_order_2", + ), + ] + ) +def test_plugin_alphabetical_ann_assign( + source: str, expects: Set[str], dunder_all_alphabetical: AlphabeticalOptions + ): + plugin = Plugin(ast.parse(source)) + plugin.dunder_all_alphabetical = dunder_all_alphabetical + assert {"{}:{}: {}".format(*r) for r in plugin.run()} == expects + + +@pytest.mark.parametrize( + "source, dunder_all_alphabetical", + [ + pytest.param("__all__ = 12345", AlphabeticalOptions.IGNORE, id="IGNORE_123"), + pytest.param("__all__ = 12345", AlphabeticalOptions.LOWER, id="LOWER_123"), + pytest.param("__all__ = 12345", AlphabeticalOptions.NONE, id="NONE_123"), + pytest.param("__all__ = 12345", AlphabeticalOptions.UPPER, id="UPPER_123"), + pytest.param("__all__ = abc", AlphabeticalOptions.IGNORE, id="IGNORE_abc"), + pytest.param("__all__ = abc", AlphabeticalOptions.LOWER, id="LOWER_abc"), + pytest.param("__all__ = abc", AlphabeticalOptions.NONE, id="NONE_abc"), + pytest.param("__all__ = abc", AlphabeticalOptions.UPPER, id="UPPER_abc"), + ] + ) +def test_plugin_alphabetical_not_list(source: str, dunder_all_alphabetical: AlphabeticalOptions): + plugin = Plugin(ast.parse(source)) + plugin.dunder_all_alphabetical = dunder_all_alphabetical + msg = "1:0: DALL002 __all__ not a list or tuple of strings." + assert {"{}:{}: {}".format(*r) for r in plugin.run()} == {msg} + + +def test_plugin_alphabetical_tuple(): + plugin = Plugin(ast.parse("__all__ = ('bar',\n'foo')")) + plugin.dunder_all_alphabetical = AlphabeticalOptions.IGNORE + assert {"{}:{}: {}".format(*r) for r in plugin.run()} == set() + + @pytest.mark.parametrize( "source, members, found_all, last_import", [ diff --git a/tests/test_main.py b/tests/test_main.py index fdd55ff..e9383a9 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -4,11 +4,14 @@ # 3rd party import pytest -from click.testing import CliRunner, Result from coincidence.regressions import AdvancedFileRegressionFixture from consolekit.terminal_colours import Fore +from consolekit.testing import CliRunner, Result from domdf_python_tools.paths import PathPlus -from test_flake8_dunder_all import ( + +# this package +from flake8_dunder_all.__main__ import main +from tests.test_flake8_dunder_all import ( mangled_source, testing_source_a, testing_source_b, @@ -25,9 +28,6 @@ testing_source_l ) -# this package -from flake8_dunder_all.__main__ import main - @pytest.mark.parametrize( "source, members, ret", diff --git a/tox.ini b/tox.ini index 39d881a..8fb1cb1 100644 --- a/tox.ini +++ b/tox.ini @@ -5,6 +5,7 @@ # * testenv # * testenv:.package # * testenv:py313-dev +# * testenv:py313 # * testenv:py312-dev # * testenv:py312 # * testenv:docs @@ -22,7 +23,6 @@ [tox] envlist = - py36-flake8{4,5} py37-flake8{4,5} py38-flake8{4,5,6,7} py39-flake8{4,5,6,7} @@ -30,7 +30,6 @@ envlist = py311-flake8{4,5,6,7} py312-flake8{5,6,7} py313-dev-flake8{5,6,7} - pypy36-flake8{4,5} pypy37-flake8{4,5} pypy38-flake8{4,5,6,7} pypy39-flake8{4,5,6,7} @@ -46,7 +45,6 @@ requires = [envlists] test = - py36-flake8{4,5} py37-flake8{4,5} py38-flake8{4,5,6,7} py39-flake8{4,5,6,7} @@ -54,7 +52,6 @@ test = py311-flake8{4,5,6,7} py312-flake8{5,6,7} py313-dev-flake8{5,6,7} - pypy36-flake8{4,5} pypy37-flake8{4,5} pypy38-flake8{4,5,6,7} pypy39-flake8{4,5,6,7} @@ -82,11 +79,14 @@ setenv = PIP_DISABLE_PIP_VERSION_CHECK=1 [testenv:py313-dev] +download = True setenv = PYTHONDEVMODE=1 PIP_DISABLE_PIP_VERSION_CHECK=1 + UNSAFE_PYO3_SKIP_VERSION_CHECK=1 [testenv:py312] +download = True setenv = PYTHONDEVMODE=1 PIP_DISABLE_PIP_VERSION_CHECK=1 @@ -104,6 +104,7 @@ setenv = PYTHONDEVMODE=1 PIP_DISABLE_PIP_VERSION_CHECK=1 PIP_PREFER_BINARY=1 + UNSAFE_PYO3_SKIP_VERSION_CHECK=1 skip_install = True changedir = {toxinidir} deps = @@ -210,7 +211,7 @@ inline-quotes = " multiline-quotes = """ docstring-quotes = """ count = True -min_python_version = 3.6.1 +min_python_version = 3.7 unused-arguments-ignore-abstract-functions = True unused-arguments-ignore-overload-functions = True unused-arguments-ignore-magic-methods = True @@ -244,14 +245,17 @@ filterwarnings = ignore:can't resolve package from __spec__ or __package__, falling back on __name__ and __path__:ImportWarning [testenv:py312-flake8{4,5,6,7}] +download = True setenv = PYTHONDEVMODE=1 PIP_DISABLE_PIP_VERSION_CHECK=1 [testenv:py313-dev-flake8{4,5,6,7}] +download = True setenv = PYTHONDEVMODE=1 PIP_DISABLE_PIP_VERSION_CHECK=1 + UNSAFE_PYO3_SKIP_VERSION_CHECK=1 [testenv:py312-flake8{4,5,6}] setenv =