From 0baac07e60283eda685828bf831abc879c88e689 Mon Sep 17 00:00:00 2001 From: Stephen Whitlock Date: Fri, 28 Jun 2024 12:29:14 +1000 Subject: [PATCH] add: docsig as a flake8 plugin (#368) Bump python version patch for flake8 Signed-off-by: Stephen Whitlock --- README.rst | 33 +++++++ changelog/368.add.md | 1 + docs/extensions/generate.py | 12 +++ docs/index.rst | 1 + docs/usage/flake8.rst | 4 + docsig/flake8.py | 169 ++++++++++++++++++++++++++++++++++++ poetry.lock | 42 ++++++++- pyproject.toml | 15 +++- whitelist.py | 6 ++ 9 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 changelog/368.add.md create mode 100644 docs/usage/flake8.rst create mode 100644 docsig/flake8.py diff --git a/README.rst b/README.rst index b75bf2741..4d5817717 100644 --- a/README.rst +++ b/README.rst @@ -134,6 +134,39 @@ Options can also be configured with the pyproject.toml file "SIG201", ] +Flake8 +****** + +``docsig`` can also be used as a ``flake8`` plugin. Install ``flake8`` and +ensure your installation has registered `docsig` + +.. code-block:: console + + $ flake8 --version + 7.1.0 (docsig: 0.56.0, mccabe: 0.7.0, pycodestyle: 2.12.0, pyflakes: 3.2.0) CPython 3.8.13 on Darwin + +And now use `flake8` to lint your files + +.. code-block:: console + + $ flake8 example.py + example.py:1:1: SIG202 includes parameters that do not exist (params-do-not-exist) 'function' + +With ``flake8`` the pyproject.toml config will still be the base config, though the +`ini files `_ ``flake8`` gets it config from will override the pyproject.toml config. +For ``flake8`` all args and config options are prefixed with ``sig`` to +avoid any potential conflicts with other plugins + +.. code-block:: ini + + [flake8] + sig-check-dunders = True + sig-check-overridden = True + sig-check-protected = True + +.. + end flake8 + API *** diff --git a/changelog/368.add.md b/changelog/368.add.md new file mode 100644 index 000000000..86e6547cb --- /dev/null +++ b/changelog/368.add.md @@ -0,0 +1 @@ +docsig as a flake8 plugin \ No newline at end of file diff --git a/docs/extensions/generate.py b/docs/extensions/generate.py index 2c6d8253c..797470d1f 100644 --- a/docs/extensions/generate.py +++ b/docs/extensions/generate.py @@ -224,6 +224,17 @@ def generate_commit_policy() -> None: build.write_text("\n".join(content), encoding="utf-8") +@extension +def generate_flake8_help() -> None: + """Generate flake8 documentation.""" + build = GENERATED / "flake8.rst" + readme = README.read_text(encoding="utf-8") + pattern = re.compile(r"Flake8\n\*{6}(.*?)end flake8", re.DOTALL) + match = pattern.search(readme) + if match: + build.write_text(match.group(1).strip()[:-2].strip(), encoding="utf-8") + + def setup(app: Sphinx) -> None: """Set up the Sphinx extension. @@ -237,3 +248,4 @@ def setup(app: Sphinx) -> None: app.connect("builder-inited", generate_tests) app.connect("builder-inited", generate_pre_commit_example) app.connect("builder-inited", generate_commit_policy) + app.connect("builder-inited", generate_flake8_help) diff --git a/docs/index.rst b/docs/index.rst index 050d21f11..6dd719e0a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,6 +6,7 @@ :hidden: usage/api + usage/flake8 usage/configuration usage/messages usage/message-control diff --git a/docs/usage/flake8.rst b/docs/usage/flake8.rst new file mode 100644 index 000000000..435295655 --- /dev/null +++ b/docs/usage/flake8.rst @@ -0,0 +1,4 @@ +Flake8 +====== + +.. include:: ../_generated/flake8.rst diff --git a/docsig/flake8.py b/docsig/flake8.py new file mode 100644 index 000000000..b5cd3e64f --- /dev/null +++ b/docsig/flake8.py @@ -0,0 +1,169 @@ +"""Flake8 implementation of docsig.""" + +import ast +import contextlib +import io +import re +import sys +import typing as t +from argparse import Namespace + +from ._main import main +from ._version import __version__ + +Flake8Error = t.Tuple[int, int, str, t.Type] + + +class Docsig: + """Flake8 implementation of docsig class. + + :param tree: Ast module, which will not be used by flake8 will + provide. + :param filename: Filename to pass to docsig. + """ + + off_by_default = False + name = __package__ + version = __version__ + options_dict: t.Dict[str, bool] = {} + + def __init__(self, tree: ast.Module, filename: str) -> None: + _tree = tree # noqa + self.filename = filename + + # won't import flake8 type + # conflicts with this module name + # might require that flake8 actually be installed, which is not a + # requirement for this package + @classmethod + def add_options(cls, parser) -> None: + """Add flake8 commandline and config options.sig_ + + :param parser: Flake8 option manager. + """ + parser.add_option( + "--sig-check-class", + action="store_true", + parse_from_config=True, + help="check class docstrings", + ) + parser.add_option( + "--sig-check-class-constructor", + action="store_true", + parse_from_config=True, + help="check __init__ methods. Note: mutually incompatible with -c", + ) + parser.add_option( + "--sig-check-dunders", + action="store_true", + parse_from_config=True, + help="check dunder methods", + ) + parser.add_option( + "--sig-check-protected-class-methods", + action="store_true", + parse_from_config=True, + help="check public methods belonging to protected classes", + ) + parser.add_option( + "--sig-check-nested", + action="store_true", + parse_from_config=True, + help="check nested functions and classes", + ) + parser.add_option( + "--sig-check-overridden", + action="store_true", + parse_from_config=True, + help="check overridden methods", + ) + parser.add_option( + "--sig-check-protected", + action="store_true", + parse_from_config=True, + help="check protected functions and classes", + ) + parser.add_option( + "--sig-check-property-returns", + action="store_true", + parse_from_config=True, + help="check property return values", + ) + parser.add_option( + "--sig-ignore-no-params", + action="store_true", + parse_from_config=True, + help="ignore docstrings where parameters are not documented", + ) + parser.add_option( + "--sig-ignore-args", + action="store_true", + parse_from_config=True, + help="ignore args prefixed with an asterisk", + ) + parser.add_option( + "--sig-ignore-kwargs", + action="store_true", + parse_from_config=True, + help="ignore kwargs prefixed with two asterisks", + ) + parser.add_option( + "--sig-ignore-typechecker", + action="store_true", + parse_from_config=True, + help="ignore checking return values", + ) + + @classmethod + def parse_options(cls, options: Namespace) -> None: + """Parse flake8 options into am instance accessible dict. + + :param options: Argparse namespace. + """ + cls.options_dict = { + "check_class": options.sig_check_class, + "check_class_constructor": options.sig_check_class_constructor, + "check_dunders": options.sig_check_dunders, + "check_protected_class_methods": ( + options.sig_check_protected_class_methods + ), + "check_nested": options.sig_check_nested, + "check_overridden": options.sig_check_overridden, + "check_protected": options.sig_check_protected, + "check_property_returns": options.sig_check_property_returns, + "ignore_no_params": options.sig_ignore_no_params, + "ignore_args": options.sig_ignore_args, + "ignore_kwargs": options.sig_ignore_kwargs, + "ignore_typechecker": options.sig_ignore_typechecker, + } + + def run(self) -> t.Generator[Flake8Error, None, None]: + """Run docsig and possibly yield a flake8 error. + + :return: Flake8 error, if there is one. + """ + buffer = io.StringIO() + with contextlib.redirect_stdout(buffer): + sys.argv = [ + __package__, + self.filename, + *[ + f"--{k.replace('_', '-')}" + for k, v in self.options_dict.items() + if v + ], + ] + main() + + results = re.split(r"^(?!\s)", buffer.getvalue(), flags=re.MULTILINE) + for result in results: + if not result: + continue + + try: + header, remainder = result.splitlines()[:2] + lineno, func_name = header.split(":", 1)[1].split(" in ", 1) + line = f"{remainder.lstrip().replace(':', '')} '{func_name}'" + yield int(lineno), 0, line, self.__class__ + except ValueError: + print(result.rstrip()) diff --git a/poetry.lock b/poetry.lock index 2aaaaa158..5a22d9874 100644 --- a/poetry.lock +++ b/poetry.lock @@ -541,6 +541,22 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] typing = ["typing-extensions (>=4.8)"] +[[package]] +name = "flake8" +version = "7.1.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.8.1" +files = [ + {file = "flake8-7.1.0-py2.py3-none-any.whl", hash = "sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a"}, + {file = "flake8-7.1.0.tar.gz", hash = "sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.12.0,<2.13.0" +pyflakes = ">=3.2.0,<3.3.0" + [[package]] name = "flynt" version = "1.0.1" @@ -1074,6 +1090,17 @@ files = [ {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, ] +[[package]] +name = "pycodestyle" +version = "2.12.0" +description = "Python style guide checker" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycodestyle-2.12.0-py2.py3-none-any.whl", hash = "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4"}, + {file = "pycodestyle-2.12.0.tar.gz", hash = "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c"}, +] + [[package]] name = "pydantic" version = "2.8.0" @@ -1215,6 +1242,17 @@ python-dotenv = ">=0.21.0" toml = ["tomli (>=2.0.1)"] yaml = ["pyyaml (>=6.0.1)"] +[[package]] +name = "pyflakes" +version = "3.2.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, + {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, +] + [[package]] name = "pygments" version = "2.18.0" @@ -1999,5 +2037,5 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" -python-versions = "^3.8" -content-hash = "9599aa5d84aad73923dc80e2fe3487760b095432c7f715618382ba8e5e85d42c" +python-versions = "^3.8.1" +content-hash = "75fc0f6b8d658d835efc0f748bf106b97d25f7bc41a745fdbdfd35f163ef3be5" diff --git a/pyproject.toml b/pyproject.toml index 782b699ca..7b548f24f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,11 @@ filename = "README.rst" replace = "rev: v{new_version}" search = "rev: v{current_version}" +[[tool.bumpversion.files]] +filename = "README.rst" +replace = "docsig: {new_version}" +search = "docsig: {current_version}" + [[tool.bumpversion.files]] filename = "SECURITY.md" @@ -59,11 +64,13 @@ fail_under = 100 omit = [ "docs/conf.py", "docsig/__main__.py", + "docsig/flake8.py", "whitelist.py" ] [tool.deptry.per_rule_ignores] DEP004 = [ + "flake8", "git", "pytest", "tomli", @@ -129,7 +136,7 @@ arcon = ">=0.4.0" astroid = "^3.0.1" click = "^8.1.7" pathspec = "^0.12.1" -python = "^3.8" +python = "^3.8.1" [tool.poetry.group.dev.dependencies] black = "^24.4.2" @@ -157,6 +164,9 @@ sphinx-copybutton = "^0.5.2" sphinx-markdown-builder = ">=0.5.5,<0.7.0" templatest = "^0.10.1" +[tool.poetry.group.flake8.dependencies] +flake8 = "^7.1.0" + [tool.poetry.group.tests.dependencies] pytest = "^8.2.0" pytest-benchmark = "^4.0.0" @@ -166,6 +176,9 @@ pytest-sugar = "^1.0.0" pytest-xdist = "^3.6.1" templatest = "^0.10.1" +[tool.poetry.plugins."flake8.extension"] +SIG = "docsig.flake8:Docsig" + [tool.poetry.scripts] docsig = "docsig.__main__:main" diff --git a/whitelist.py b/whitelist.py index 136077a46..5ec12937b 100644 --- a/whitelist.py +++ b/whitelist.py @@ -1,3 +1,9 @@ +Docsig # unused class (docsig/flake8.py:17) +off_by_default # unused variable (docsig/flake8.py:25) +_.add_options # unused method (docsig/flake8.py:38) +_.parse_options # unused method (docsig/flake8.py:117) +_.run # unused method (docsig/flake8.py:140) +_.argv # unused attribute (docsig/flake8.py:147) __call__ # unused function (tests/__init__.py:28) content # unused variable (tests/__init__.py:28) _PParamS # unused class (tests/__init__.py:59)