Skip to content
This repository has been archived by the owner on Nov 3, 2023. It is now read-only.

Add a match_path option for comparison against a full path. #529

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ Release Notes
**pydocstyle** version numbers follow the
`Semantic Versioning <http://semver.org/>`_ specification.

Current Development Version
---------------------------

New Features

* Add --match-path which operates over relative paths (#529)


Current Development Version
---------------------------
Expand Down
40 changes: 35 additions & 5 deletions src/pydocstyle/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,14 @@ class ConfigurationParser:
'add-ignore',
'match',
'match-dir',
'match-path',
'ignore-decorators',
)
BASE_ERROR_SELECTION_OPTIONS = ('ignore', 'select', 'convention')

DEFAULT_MATCH_RE = r'(?!test_).*\.py'
DEFAULT_MATCH_DIR_RE = r'[^\.].*'
DEFAULT_MATCH_PATH_RE = [r'[^\.].*']
DEFAULT_IGNORE_DECORATORS_RE = ''
DEFAULT_CONVENTION = conventions.pep257

Expand Down Expand Up @@ -260,6 +262,13 @@ def _get_matches(conf):
match_dir_func = re(conf.match_dir + '$').match
return match_func, match_dir_func

def _get_path_matches(conf):
"""Return a list of `match_path` regexes."""
matches = conf.match_path
if isinstance(matches, str):
matches = matches.split()
return [re(x) for x in matches]

def _get_ignore_decorators(conf):
"""Return the `ignore_decorators` as None or regex."""
return (
Expand All @@ -271,14 +280,22 @@ def _get_ignore_decorators(conf):
for root, dirs, filenames in os.walk(name):
config = self._get_config(os.path.abspath(root))
match, match_dir = _get_matches(config)
match_paths = _get_path_matches(config)
ignore_decorators = _get_ignore_decorators(config)

# Skip any dirs that do not match match_dir
dirs[:] = [d for d in dirs if match_dir(d)]

for filename in filenames:
full_path = os.path.join(root, filename)
relative_posix = os.path.normpath(
os.path.relpath(full_path, start=name)
).replace(os.path.sep, "/")
if not any(
x.match(relative_posix) for x in match_paths
):
continue
if match(filename):
full_path = os.path.join(root, filename)
yield (
full_path,
list(config.checked_codes),
Expand All @@ -287,7 +304,11 @@ def _get_ignore_decorators(conf):
else:
config = self._get_config(os.path.abspath(name))
match, _ = _get_matches(config)
match_paths = _get_path_matches(config)
ignore_decorators = _get_ignore_decorators(config)
posix = os.path.normpath(name).replace(os.path.sep, "/")
if not any(x.match(posix) for x in match_paths):
continue
if match(name):
yield (name, list(config.checked_codes), ignore_decorators)

Expand Down Expand Up @@ -394,7 +415,6 @@ def _get_config(self, node):
cli_val = getattr(self._override_by_cli, attr)
conf_val = getattr(config, attr)
final_config[attr] = cli_val if cli_val is not None else conf_val

config = CheckConfiguration(**final_config)

self._set_add_options(config.checked_codes, self._options)
Expand Down Expand Up @@ -485,7 +505,7 @@ def _merge_configuration(self, parent_config, child_options):
self._set_add_options(error_codes, child_options)

kwargs = dict(checked_codes=error_codes)
for key in ('match', 'match_dir', 'ignore_decorators'):
for key in ('match', 'match_dir', 'match_path', 'ignore_decorators'):
kwargs[key] = getattr(child_options, key) or getattr(
parent_config, key
)
Expand Down Expand Up @@ -519,7 +539,7 @@ def _create_check_config(cls, options, use_defaults=True):
checked_codes = cls._get_checked_errors(options)

kwargs = dict(checked_codes=checked_codes)
for key in ('match', 'match_dir', 'ignore_decorators'):
for key in ('match', 'match_dir', 'match_path', 'ignore_decorators'):
kwargs[key] = (
getattr(cls, f'DEFAULT_{key.upper()}_RE')
if getattr(options, key) is None and use_defaults
Expand Down Expand Up @@ -840,6 +860,16 @@ def _create_option_parser(cls):
"a dot"
).format(cls.DEFAULT_MATCH_DIR_RE),
)
option(
'--match-path',
metavar='<pattern>',
default=None,
nargs="+",
help=(
"search only paths that exactly match <pattern> regular "
"expressions. Can take multiple values."
),
)

# Decorators
option(
Expand All @@ -862,7 +892,7 @@ def _create_option_parser(cls):
# Check configuration - used by the ConfigurationParser class.
CheckConfiguration = namedtuple(
'CheckConfiguration',
('checked_codes', 'match', 'match_dir', 'ignore_decorators'),
('checked_codes', 'match', 'match_dir', 'match_path', 'ignore_decorators'),
)


Expand Down
63 changes: 63 additions & 0 deletions src/tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1393,6 +1393,69 @@ def foo():
assert code == 0


def test_config_file_nearest_match_path(env):
r"""Test that the `match-path` option is handled correctly.

env_base
+-- tox.ini
| This configuration will set `convention=pep257` and
| `match_path=A/[BC]/[bc]\.py\n A/D/bla.py`.
+-- A
+-- B
| +-- b.py
| Will violate D100,D103.
+-- C
| +-- c.py
| | Will violate D100,D103.
| +-- bla.py
| Will violate D100.
+-- D
+-- c.py
| Will violate D100,D103.
+-- bla.py
Will violate D100.

We expect the call to pydocstyle to fail, and since we run with verbose the
output should contain `A/B/b.py`, `A/C/c.py` and `A/D/bla.py` but not the
others.
"""
env.write_config(convention='pep257')
env.write_config(match_path='A/[BC]/[bc]\.py\n A/D/bla.py')

content = textwrap.dedent("""\
def foo():
pass
""")

env.makedirs(os.path.join('A', 'B'))
env.makedirs(os.path.join('A', 'C'))
env.makedirs(os.path.join('A', 'D'))
with env.open(os.path.join('A', 'B', 'b.py'), 'wt') as test:
test.write(content)

with env.open(os.path.join('A', 'C', 'c.py'), 'wt') as test:
test.write(content)

with env.open(os.path.join('A', 'C', 'bla.py'), 'wt') as test:
test.write('')

with env.open(os.path.join('A', 'D', 'c.py'), 'wt') as test:
test.write(content)

with env.open(os.path.join('A', 'D', 'bla.py'), 'wt') as test:
test.write('')

out, _, code = env.invoke(args="--verbose")

assert os.path.join("A", "B", "b.py") in out
assert os.path.join("A", "C", "c.py") in out
assert os.path.join("A", "C", "bla.py") not in out
assert os.path.join("A", "D", "c.py") not in out
assert os.path.join("A", "D", "bla.py") in out

assert code == 1


def test_syntax_error_multiple_files(env):
"""Test that a syntax error in a file doesn't prevent further checking."""
for filename in ('first.py', 'second.py'):
Expand Down