Skip to content

Commit

Permalink
Merge pull request #172 from econchick/ext-flag
Browse files Browse the repository at this point in the history
Add support for other file extensions (pyi to start)
  • Loading branch information
econchick authored Apr 7, 2024
2 parents 1a28d80 + 722d523 commit 5461ebc
Show file tree
Hide file tree
Showing 11 changed files with 135 additions and 9 deletions.
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Added
^^^^^

* `tomli` dependency for Python versions < 3.11, making use of `tomllib` in the standard library with 3.11+ (`#150 <https://github.com/econchick/interrogate/issues/150>`_).
* Support for ``pyi`` file extensions (and leave room for other file extensions to be added, like maybe ``ipynb``).

Fixed
^^^^^
Expand Down
4 changes: 4 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/interrogate/__init__.py
Original file line number Diff line number Diff line change
@@ -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"
14 changes: 14 additions & 0 deletions src/interrogate/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -360,6 +373,7 @@ def main(ctx, paths, **kwargs):
paths=paths,
conf=conf,
excluded=kwargs["exclude"],
extensions=kwargs["ext"],
)
results = interrogate_coverage.get_coverage()

Expand Down
23 changes: 17 additions & 6 deletions src/interrogate/coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("/")
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)

Expand Down
85 changes: 85 additions & 0 deletions tests/functional/sample/full.pyi
Original file line number Diff line number Diff line change
@@ -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"""
1 change: 1 addition & 0 deletions tests/functional/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
5 changes: 4 additions & 1 deletion tests/functional/test_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 5461ebc

Please sign in to comment.