Skip to content

Commit

Permalink
add: docsig as a flake8 plugin (#368)
Browse files Browse the repository at this point in the history
Bump python version patch for flake8

Signed-off-by: Stephen Whitlock <[email protected]>
  • Loading branch information
jshwi committed Jul 2, 2024
1 parent be77b6b commit 0baac07
Show file tree
Hide file tree
Showing 9 changed files with 280 additions and 3 deletions.
33 changes: 33 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://flake8.pycqa.org/en/latest/user/configuration.html#configuration-locations>`_ ``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
***

Expand Down
1 change: 1 addition & 0 deletions changelog/368.add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
docsig as a flake8 plugin
12 changes: 12 additions & 0 deletions docs/extensions/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
:hidden:

usage/api
usage/flake8
usage/configuration
usage/messages
usage/message-control
Expand Down
4 changes: 4 additions & 0 deletions docs/usage/flake8.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Flake8
======

.. include:: ../_generated/flake8.rst
169 changes: 169 additions & 0 deletions docsig/flake8.py
Original file line number Diff line number Diff line change
@@ -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())
42 changes: 40 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 14 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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",
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"

Expand Down
6 changes: 6 additions & 0 deletions whitelist.py
Original file line number Diff line number Diff line change
@@ -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)
Expand Down

0 comments on commit 0baac07

Please sign in to comment.