From 722d5239256a28eb16a54abcd7d11a6e5f21c62a Mon Sep 17 00:00:00 2001 From: Lynn Root Date: Sun, 7 Apr 2024 10:52:40 -0700 Subject: [PATCH] Add support for other file extensions (pyi to start) --- .pre-commit-config.yaml | 1 + MANIFEST.in | 2 +- README.rst | 6 +++ docs/changelog.rst | 1 + docs/index.rst | 4 ++ src/interrogate/__init__.py | 2 +- src/interrogate/cli.py | 14 +++++ src/interrogate/coverage.py | 23 ++++++--- tests/functional/sample/full.pyi | 85 +++++++++++++++++++++++++++++++ tests/functional/test_cli.py | 1 + tests/functional/test_coverage.py | 5 +- 11 files changed, 135 insertions(+), 9 deletions(-) create mode 100644 tests/functional/sample/full.pyi diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3cb5442..a62269c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,6 +14,7 @@ repos: rev: 24.3.0 hooks: - id: black + exclude: ^(tests/functional/sample/full.pyi) - repo: https://github.com/asottile/pyupgrade rev: v3.15.2 diff --git a/MANIFEST.in b/MANIFEST.in index 11c5d64..10ebf0b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,7 +4,7 @@ graft .github # Tests include tox.ini conftest.py -recursive-include tests *.py +recursive-include tests *.py *.pyi recursive-include tests *.svg *.png recursive-include tests *.txt diff --git a/README.rst b/README.rst index f213663..1db3f99 100644 --- a/README.rst +++ b/README.rst @@ -364,6 +364,7 @@ Configure within your ``pyproject.toml`` (``interrogate`` will automatically det fail-under = 95 exclude = ["setup.py", "docs", "build"] ignore-regex = ["^get$", "^mock_.*", ".*BaseClass.*"] + ext = [] # possible values: 0 (minimal output), 1 (-v), 2 (-vv) verbose = 0 quiet = false @@ -397,6 +398,7 @@ Or configure within your ``setup.cfg`` (``interrogate`` will automatically detec fail-under = 95 exclude = setup.py,docs,build ignore-regex = ^get$,^mock_.*,.*BaseClass.* + ext = [] ; possible values: 0 (minimal output), 1 (-v), 2 (-vv) verbose = 0 quiet = false @@ -528,6 +530,10 @@ To view all options available, run ``interrogate --help``: function names to ignore. Multiple `-r/--ignore-regex` invocations supported. + --ext Include Python-like files with the given + extension (supported: ``pyi``). Multiple + `--ext` invocations supported. + -w, --whitelist-regex STR Regex identifying class, method, and function names to include. Multiple `-w/--whitelist-regex` invocations diff --git a/docs/changelog.rst b/docs/changelog.rst index ff13659..39ae202 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,7 @@ Added ^^^^^ * `tomli` dependency for Python versions < 3.11, making use of `tomllib` in the standard library with 3.11+ (`#150 `_). +* Support for ``pyi`` file extensions (and leave room for other file extensions to be added, like maybe ``ipynb``). Fixed ^^^^^ diff --git a/docs/index.rst b/docs/index.rst index 00711f9..0b48a88 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -94,6 +94,10 @@ Command Line Options Regex identifying class, method, and function names to ignore. Multiple ``-r/--ignore-regex`` invocations supported. +.. option:: --ext STR + + Include Python-like files with the given extension (supported: ``pyi``). Multiple ``--ext`` invocations supported. + .. option:: -w, --whitelist-regex STR Regex identifying class, method, and function names to include. Multiple ``-r/--ignore-regex`` invocations supported. diff --git a/src/interrogate/__init__.py b/src/interrogate/__init__.py index 8e92916..47401fa 100644 --- a/src/interrogate/__init__.py +++ b/src/interrogate/__init__.py @@ -1,7 +1,7 @@ # Copyright 2020-2021 Lynn Root """Explain yourself! Interrogate a codebase for docstring coverage.""" __author__ = "Lynn Root" -__version__ = "1.6.0" +__version__ = "1.7.0.dev0" __email__ = "lynn@lynnroot.com" __description__ = "Interrogate a codebase for docstring coverage." __uri__ = "https://interrogate.readthedocs.io" diff --git a/src/interrogate/cli.py b/src/interrogate/cli.py index b087992..a3f7ed9 100644 --- a/src/interrogate/cli.py +++ b/src/interrogate/cli.py @@ -177,6 +177,18 @@ "Multiple `-r/--ignore-regex` invocations supported." ), ) +@click.option( + "--ext", + default=(), + multiple=True, + metavar="STR", + type=click.Choice(["pyi"], case_sensitive=False), + help=( + "Include Python-like files with the given extension " + "(supported: ``pyi``). Multiple ``--ext`` invocations supported. " + "``.py`` files included by default unless excluded in ``--exclude``." + ), +) @click.option( "-w", "--whitelist-regex", @@ -310,6 +322,7 @@ def main(ctx, paths, **kwargs): .. versionadded:: 1.5.0 ``--omit-covered-files`` .. versionadded:: 1.5.0 ``--badge-style`` .. versionadded:: 1.6.0 ``--ignore-overloaded-functions`` + .. verisonadded:: 1.7.0 ``--ext`` .. versionchanged:: 1.3.1 only generate badge if results change from an existing badge. @@ -360,6 +373,7 @@ def main(ctx, paths, **kwargs): paths=paths, conf=conf, excluded=kwargs["exclude"], + extensions=kwargs["ext"], ) results = interrogate_coverage.get_coverage() diff --git a/src/interrogate/coverage.py b/src/interrogate/coverage.py index 22470a0..6134c0a 100644 --- a/src/interrogate/coverage.py +++ b/src/interrogate/coverage.py @@ -109,9 +109,12 @@ class InterrogateCoverage: """ COMMON_EXCLUDE = [".tox", ".venv", "venv", ".git", ".hg"] + VALID_EXT = [".py", ".pyi"] # someday in the future: .ipynb - def __init__(self, paths, conf=None, excluded=None): + def __init__(self, paths, conf=None, excluded=None, extensions=None): self.paths = paths + self.extensions = set(extensions or set()) + self.extensions.add(".py") self.config = conf or config.InterrogateConfig() self.excluded = excluded or () self.common_base = pathlib.Path("/") @@ -129,7 +132,8 @@ def _add_common_exclude(self): def _filter_files(self, files): """Filter files that are explicitly excluded.""" for f in files: - if not f.endswith(".py"): + has_valid_ext = any([f.endswith(ext) for ext in self.extensions]) + if not has_valid_ext: continue if self.config.ignore_init_module: basename = os.path.basename(f) @@ -144,10 +148,14 @@ def get_filenames_from_paths(self): filenames = [] for path in self.paths: if os.path.isfile(path): - if not path.endswith(".py") and not path.endswith(".pyi"): + has_valid_ext = any( + [path.endswith(ext) for ext in self.VALID_EXT] + ) + if not has_valid_ext: msg = ( - "E: Invalid file '{}'. Unable to interrogate non-Python" - " files.".format(path) + f"E: Invalid file '{path}'. Unable to interrogate " + "non-Python or Python-like files. " + f"Valid file extensions: {', '.join(self.VALID_EXT)}" ) click.echo(msg, err=True) return sys.exit(1) @@ -159,7 +167,10 @@ def get_filenames_from_paths(self): if not filenames: p = ", ".join(self.paths) - msg = f"E: No Python files found to interrogate in '{p}'." + msg = ( + f"E: No Python or Python-like files found to interrogate in " + f"'{p}'." + ) click.echo(msg, err=True) return sys.exit(1) diff --git a/tests/functional/sample/full.pyi b/tests/functional/sample/full.pyi new file mode 100644 index 0000000..3ebe6c6 --- /dev/null +++ b/tests/functional/sample/full.pyi @@ -0,0 +1,85 @@ +# Copyright 2024 Lynn Root +"""Sample stub with docs""" +import typing + +from typing import overload + + +class Foo: + """Foo class""" + + def __init__(self) -> None: + """init method of Foo class""" + + def __str__(self) -> None: + """a magic method.""" + + def _semiprivate(self) -> None: + """a semipriate method""" + + def __private(self) -> None: + """a private method""" + + def method_foo(self) -> None: + """this method does foo""" + + def get(self) -> None: + """this method gets something""" + + async def get(self) -> None: + """this async method gets something""" + + @property + def prop(self) -> None: + """this method has a get property decorator""" + + @prop.setter + def prop(self) -> None: + """this method has a set property decorator""" + + @prop.deleter + def prop(self) -> None: + """this method as a del property decorator""" + + @typing.overload + def module_overload(a: None) -> None: + """overloaded method""" + + @typing.overload + def module_overload(a: int) -> int: + """overloaded method""" + + def module_overload(a: str) -> str: + """overloaded method implementation""" + + @overload + def simple_overload(a: None) -> None: + """overloaded method""" + + @overload + def simple_overload(a: int) -> int: + """overloaded method""" + + def simple_overload(a: str) -> str: + """overloaded method implementation""" + +def top_level_func() -> None: + """A top level function""" + + def inner_func() -> None: + """A inner function""" + +class Bar: + """Bar class""" + + def method_bar(self) -> None: + """a method that does bar""" + + class InnerBar: + """an inner class""" + +class _SemiprivateClass: + """a semiprivate class""" + +class __PrivateClass: + """a private class""" diff --git a/tests/functional/test_cli.py b/tests/functional/test_cli.py index 84fec74..e65f046 100644 --- a/tests/functional/test_cli.py +++ b/tests/functional/test_cli.py @@ -101,6 +101,7 @@ def test_run_shortflags(flags, exp_result, exp_exit_code, runner): (["--ignore-nested-classes"], 52.2, 1), (["--ignore-overloaded-functions"], 51.6, 1), (["--ignore-regex", "^get$"], 51.5, 1), + (["--ext", "pyi"], 64.2, 1), (["--whitelist-regex", "^get$"], 50.0, 1), (["--exclude", os.path.join(SAMPLE_DIR, "partial.py")], 63.4, 1), (["--fail-under", "40"], 51.4, 0), diff --git a/tests/functional/test_coverage.py b/tests/functional/test_coverage.py index 660369f..214d87a 100644 --- a/tests/functional/test_coverage.py +++ b/tests/functional/test_coverage.py @@ -107,7 +107,10 @@ def test_coverage_errors(capsys): interrogate_coverage.get_coverage() captured = capsys.readouterr() - assert "E: No Python files found to interrogate in " in captured.err + assert ( + "E: No Python or Python-like files found to interrogate in " + in captured.err + ) @pytest.mark.parametrize(