55import contextlib
66import itertools
77import logging
8+ import os .path
89from pathlib import Path
910import shutil
1011import sys
2021
2122class RendererWarningPrinter (logging .Handler ):
2223 def emit (self , record : logging .LogRecord ) -> None :
23- if record .levelno >= logging .WARNING :
24+ if record .levelno >= logging .WARNING : # pragma: no branch
2425 sys .stderr .write (f"Warning: { record .msg } \n " )
2526
2627
@@ -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 ): # pragma: >=3.13 cover
63+ if is_excluded (path , opts ["exclude" ], toml_path , "exclude" in cli_opts ):
64+ continue
65+ else : # pragma: <3.13 cover
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,14 @@ 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 ): # pragma: >=3.13 cover
176+ parser .add_argument (
177+ "--exclude" ,
178+ action = "append" ,
179+ metavar = "PATTERN" ,
180+ help = "exclude files that match the Unix-style glob pattern "
181+ "(multiple allowed)" ,
182+ )
160183 for plugin in parser_extensions .values ():
161184 if hasattr (plugin , "add_cli_options" ):
162185 plugin .add_cli_options (parser )
@@ -173,34 +196,63 @@ def __init__(self, path: Path):
173196def resolve_file_paths (path_strings : Iterable [str ]) -> list [None | Path ]:
174197 """Resolve pathlib.Path objects from filepath strings.
175198
176- Convert path strings to pathlib.Path objects. Resolve symlinks.
177- Check that all paths are either files, directories or stdin. If not,
178- raise InvalidPath. Resolve directory paths to a list of file paths
179- (ending with ".md").
199+ Convert path strings to pathlib.Path objects. Check that all paths
200+ are either files, directories or stdin. If not, raise InvalidPath.
201+ Resolve directory paths to a list of file paths (ending with ".md").
180202 """
181203 file_paths : list [None | Path ] = [] # Path to file or None for stdin/stdout
182204 for path_str in path_strings :
183205 if path_str == "-" :
184206 file_paths .append (None )
185207 continue
186208 path_obj = Path (path_str )
187- path_obj = _resolve_path (path_obj )
209+ path_obj = _normalize_path (path_obj )
188210 if path_obj .is_dir ():
189211 for p in path_obj .glob ("**/*.md" ):
190- p = _resolve_path (p )
191- file_paths .append (p )
192- else :
212+ if p .is_file ():
213+ p = _normalize_path (p )
214+ file_paths .append (p )
215+ elif path_obj .is_file (): # pragma: nt no cover
193216 file_paths .append (path_obj )
217+ else : # pragma: nt no cover
218+ raise InvalidPath (path_obj )
194219 return file_paths
195220
196221
197- def _resolve_path (path : Path ) -> Path :
198- """Resolve path.
222+ def is_excluded ( # pragma: >=3.13 cover
223+ path : Path | None ,
224+ patterns : list [str ],
225+ toml_path : Path | None ,
226+ excludes_from_cli : bool ,
227+ ) -> bool :
228+ if not path :
229+ return False
230+
231+ if not excludes_from_cli and toml_path :
232+ exclude_root = toml_path .parent
233+ else :
234+ exclude_root = Path .cwd ()
235+
236+ try :
237+ relative_path = path .relative_to (exclude_root )
238+ except ValueError :
239+ return False
240+
241+ return any (
242+ relative_path .full_match (pattern ) # type: ignore[attr-defined]
243+ for pattern in patterns
244+ )
245+
246+
247+ def _normalize_path (path : Path ) -> Path :
248+ """Normalize path.
199249
200- Resolve symlinks. Raise `InvalidPath` if the path does not exist.
250+ Make the path absolute, resolve any ".." sequences. Do not resolve
251+ symlinks, as it would interfere with 'exclude' patterns. Raise
252+ `InvalidPath` if the path does not exist.
201253 """
254+ path = Path (os .path .abspath (path ))
202255 try :
203- path = path .resolve () # resolve symlinks
204256 path_exists = path .exists ()
205257 except OSError : # Catch "OSError: [WinError 123]" on Windows # pragma: no cover
206258 path_exists = False
0 commit comments