diff --git a/docs/release_notes.rst b/docs/release_notes.rst
index 6a1d7ae4..9f8d0eae 100644
--- a/docs/release_notes.rst
+++ b/docs/release_notes.rst
@@ -4,6 +4,13 @@ Release Notes
**pydocstyle** version numbers follow the
`Semantic Versioning `_ specification.
+Current Development Version
+---------------------------
+
+New Features
+
+* Add --match-path which operates over relative paths (#529)
+
Current Development Version
---------------------------
diff --git a/src/pydocstyle/config.py b/src/pydocstyle/config.py
index ae1e6b58..696d8be2 100644
--- a/src/pydocstyle/config.py
+++ b/src/pydocstyle/config.py
@@ -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
@@ -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 (
@@ -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),
@@ -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)
@@ -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)
@@ -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
)
@@ -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
@@ -840,6 +860,16 @@ def _create_option_parser(cls):
"a dot"
).format(cls.DEFAULT_MATCH_DIR_RE),
)
+ option(
+ '--match-path',
+ metavar='',
+ default=None,
+ nargs="+",
+ help=(
+ "search only paths that exactly match regular "
+ "expressions. Can take multiple values."
+ ),
+ )
# Decorators
option(
@@ -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'),
)
diff --git a/src/tests/test_integration.py b/src/tests/test_integration.py
index eb4994ff..fa24e31f 100644
--- a/src/tests/test_integration.py
+++ b/src/tests/test_integration.py
@@ -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'):