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

Commit

Permalink
Add a match_path option for comparison against a full path.
Browse files Browse the repository at this point in the history
It has been noted in issue #363 that match & match_dir are
unwieldy when attempting to match against full paths.  For
unexample if you have A.py in directories B & C and you only
want to run pydocstyle on one of them.

From my own experience trying to deploy pydocstyle against a
large legacy codebase it is unworkable as it would mean the
entire codebase being converted as a big bang change. A more
nuanced approach means the codebase can be converted gradually.

This commit adds a new option, match_path, to the config &
command lines which can be used to provide more nuanced
matching.  For example the following specification:

match_path = [AB]/[ab].py
             D/e.py

This defines two regexes.  If either match a given path,
relative to the directory specified, the file will be yielded
for comparison.  The is complimentary to match & match_dir and
the three can be used together.
  • Loading branch information
heoga committed Apr 1, 2021
1 parent 837c0c2 commit 25171aa
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 5 deletions.
40 changes: 35 additions & 5 deletions src/pydocstyle/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,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 @@ -149,6 +151,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 @@ -160,14 +169,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 @@ -176,7 +193,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 @@ -283,7 +304,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 @@ -371,7 +391,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 @@ -405,7 +425,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 @@ -721,6 +741,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 @@ -743,7 +773,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 @@ -1308,6 +1308,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

0 comments on commit 25171aa

Please sign in to comment.