Skip to content

Commit 1f30316

Browse files
committed
Exclude patterns (no tests, no docs)
1 parent 995b5b9 commit 1f30316

File tree

2 files changed

+67
-14
lines changed

2 files changed

+67
-14
lines changed

src/mdformat/_cli.py

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import contextlib
66
import itertools
77
import logging
8+
import os.path
89
from pathlib import Path
910
import shutil
1011
import sys
@@ -52,12 +53,26 @@ def run(cli_args: Sequence[str]) -> int: # noqa: C901
5253
renderer_warning_printer = RendererWarningPrinter()
5354
for path in file_paths:
5455
try:
55-
toml_opts = read_toml_opts(path.parent if path else Path.cwd())
56+
toml_opts, toml_path = read_toml_opts(path.parent if path else Path.cwd())
5657
except InvalidConfError as e:
5758
print_error(str(e))
5859
return 1
5960
opts: Mapping = {**DEFAULT_OPTS, **toml_opts, **cli_opts}
6061

62+
if sys.version_info >= (3, 13):
63+
if is_excluded(path, opts["exclude"], toml_path, "exclude" in cli_opts):
64+
continue
65+
else:
66+
if "exclude" in toml_opts:
67+
print_error(
68+
"'exclude' patterns are only available on Python 3.13+.",
69+
paragraphs=[
70+
"Please remove the 'exclude' list from your .mdformat.toml"
71+
" or upgrade Python version."
72+
],
73+
)
74+
return 1
75+
6176
if path:
6277
path_str = str(path)
6378
# Unlike `path.read_text(encoding="utf-8")`, this preserves
@@ -157,6 +172,12 @@ def make_arg_parser(
157172
choices=("lf", "crlf", "keep"),
158173
help="output file line ending mode (default: lf)",
159174
)
175+
if sys.version_info >= (3, 13):
176+
parser.add_argument(
177+
"--exclude",
178+
action="append",
179+
help="exclude files that match the pattern (multiple allowed)",
180+
)
160181
for plugin in parser_extensions.values():
161182
if hasattr(plugin, "add_cli_options"):
162183
plugin.add_cli_options(parser)
@@ -173,7 +194,7 @@ def __init__(self, path: Path):
173194
def resolve_file_paths(path_strings: Iterable[str]) -> list[None | Path]:
174195
"""Resolve pathlib.Path objects from filepath strings.
175196
176-
Convert path strings to pathlib.Path objects. Resolve symlinks.
197+
Convert path strings to pathlib.Path objects.
177198
Check that all paths are either files, directories or stdin. If not,
178199
raise InvalidPath. Resolve directory paths to a list of file paths
179200
(ending with ".md").
@@ -184,23 +205,48 @@ def resolve_file_paths(path_strings: Iterable[str]) -> list[None | Path]:
184205
file_paths.append(None)
185206
continue
186207
path_obj = Path(path_str)
187-
path_obj = _resolve_path(path_obj)
208+
path_obj = _normalize_path(path_obj)
188209
if path_obj.is_dir():
189210
for p in path_obj.glob("**/*.md"):
190-
p = _resolve_path(p)
191-
file_paths.append(p)
192-
else:
211+
if p.is_file():
212+
p = _normalize_path(p)
213+
file_paths.append(p)
214+
elif path_obj.is_file():
193215
file_paths.append(path_obj)
216+
else:
217+
raise InvalidPath(path_obj)
194218
return file_paths
195219

196220

197-
def _resolve_path(path: Path) -> Path:
198-
"""Resolve path.
221+
def is_excluded(
222+
path: Path | None, patterns: list[str], toml_path: Path | None, excludes_from_cli: bool
223+
) -> bool:
224+
if not path:
225+
return False
226+
227+
if not excludes_from_cli and toml_path:
228+
exclude_root = toml_path.parent
229+
else:
230+
exclude_root = Path.cwd()
231+
232+
try:
233+
relative_path = path.relative_to(exclude_root)
234+
except ValueError:
235+
return False
236+
237+
return any(relative_path.full_match(pattern) for pattern in patterns)
238+
239+
240+
def _normalize_path(path: Path) -> Path:
241+
"""Normalize path.
199242
200-
Resolve symlinks. Raise `InvalidPath` if the path does not exist.
243+
Make the path absolute, resolve any ".." sequences.
244+
Do not resolve symlinks, as it would interfere with
245+
'exclude' patterns.
246+
Raise `InvalidPath` if the path does not exist.
201247
"""
248+
path = Path(os.path.abspath(path))
202249
try:
203-
path = path.resolve() # resolve symlinks
204250
path_exists = path.exists()
205251
except OSError: # Catch "OSError: [WinError 123]" on Windows # pragma: no cover
206252
path_exists = False

src/mdformat/_conf.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"wrap": "keep",
1111
"number": False,
1212
"end_of_line": "lf",
13+
"exclude": [],
1314
}
1415

1516

@@ -24,12 +25,12 @@ class InvalidConfError(Exception):
2425

2526

2627
@functools.lru_cache()
27-
def read_toml_opts(conf_dir: Path) -> Mapping:
28+
def read_toml_opts(conf_dir: Path) -> tuple[Mapping, Path | None]:
2829
conf_path = conf_dir / ".mdformat.toml"
2930
if not conf_path.is_file():
3031
parent_dir = conf_dir.parent
3132
if conf_dir == parent_dir:
32-
return {}
33+
return {}, None
3334
return read_toml_opts(parent_dir)
3435

3536
with open(conf_path, "rb") as f:
@@ -41,10 +42,10 @@ def read_toml_opts(conf_dir: Path) -> Mapping:
4142
_validate_keys(toml_opts, conf_path)
4243
_validate_values(toml_opts, conf_path)
4344

44-
return toml_opts
45+
return toml_opts, conf_path
4546

4647

47-
def _validate_values(opts: Mapping, conf_path: Path) -> None:
48+
def _validate_values(opts: Mapping, conf_path: Path) -> None: # noqa: C901
4849
if "wrap" in opts:
4950
wrap_value = opts["wrap"]
5051
if not (
@@ -58,6 +59,12 @@ def _validate_values(opts: Mapping, conf_path: Path) -> None:
5859
if "number" in opts:
5960
if not isinstance(opts["number"], bool):
6061
raise InvalidConfError(f"Invalid 'number' value in {conf_path}")
62+
if "exclude" in opts:
63+
if not isinstance(opts["exclude"], list):
64+
raise InvalidConfError(f"Invalid 'exclude' value in {conf_path}")
65+
for pattern in opts["exclude"]:
66+
if not isinstance(pattern, str):
67+
raise InvalidConfError(f"Invalid 'exclude' value in {conf_path}")
6168

6269

6370
def _validate_keys(opts: Mapping, conf_path: Path) -> None:

0 commit comments

Comments
 (0)