1+ #!python
12import codecs
23import io
34import os
45import re
56import subprocess
67import sys
7- from typing import Optional , Tuple
8+ from pathlib import Path
9+ from typing import Optional , Tuple , Iterable , List
810
911import boltons .iterutils
1012import click
1113import pydevf
12- from pathlib import Path
1314
1415CPP_PATTERNS = {
1516 '*.cpp' ,
@@ -42,15 +43,27 @@ def is_cpp(filename):
4243 return any (fnmatch (os .path .basename (filename ), p ) for p in CPP_PATTERNS )
4344
4445
45- def should_format (filename ):
46+ def should_format (filename : str , include_patterns : Iterable [ str ], exclude_patterns : Iterable [ str ] ):
4647 """
47- Return a tuple (fmt, reason) where fmt is True if the filename
48- is of a type that is supported by this tool.
48+ Return a tuple (fmt, reason) where fmt is True if the filename should be formatted.
49+
50+ :param filename: file name to verify if should be formatted or not
51+
52+ :param include_patterns: list of file patterns to be included in the formatting
53+
54+ :param exclude_patterns: list of file patterns to be excluded from formatting. Has precedence
55+ over `include_patterns`
56+
57+ :rtype: Tuple[bool, str]
4958 """
5059 from fnmatch import fnmatch
60+
61+ if any (fnmatch (filename , pattern ) for pattern in exclude_patterns ):
62+ return False , 'Excluded file'
63+
5164 filename_no_ext , ext = os .path .splitext (filename )
52- ipynb_filename = filename_no_ext + '.ipynb'
5365 # ignore .py file that has a jupytext configured notebook with the same base name
66+ ipynb_filename = filename_no_ext + '.ipynb'
5467 if ext == '.py' and os .path .isfile (ipynb_filename ):
5568 with open (ipynb_filename , 'rb' ) as f :
5669 if b'jupytext' not in f .read ():
@@ -59,12 +72,14 @@ def should_format(filename):
5972 if b'jupytext:' not in f .read ():
6073 return True , ''
6174 return False , 'Jupytext generated file'
62- if any (fnmatch (os .path .basename (filename ), p ) for p in PATTERNS ):
75+
76+ if any (fnmatch (os .path .basename (filename ), pattern ) for pattern in include_patterns ):
6377 return True , ''
78+
6479 return False , 'Unknown file type'
6580
6681
67- def find_black_config (files_or_directories ) -> Optional [Path ]:
82+ def find_pyproject_toml (files_or_directories ) -> Optional [Path ]:
6883 """
6984 Searches for a valid pyproject.toml file based on the list of files/directories given.
7085
@@ -76,11 +91,35 @@ def find_black_config(files_or_directories) -> Optional[Path]:
7691 common = Path (os .path .commonpath (files_or_directories )).resolve ()
7792 for p in ([common ] + list (common .parents )):
7893 fn = p / 'pyproject.toml'
79- if fn .is_file () and '[tool.black]' in fn . read_text ( encoding = 'UTF-8' ) :
94+ if fn .is_file ():
8095 return fn
8196 return None
8297
8398
99+ def read_exclude_patterns (pyproject_toml : Path ) -> List [str ]:
100+ import toml
101+
102+ toml_contents = toml .load (pyproject_toml )
103+ ff_options = toml_contents .get ('tool' , {}).get ('esss_fix_format' , {})
104+ excludes_option = ff_options .get ('exclude' , [])
105+ if not isinstance (excludes_option , list ):
106+ raise TypeError (
107+ f"pyproject.toml excludes option must be a list, got { type (excludes_option )} )"
108+ )
109+
110+ # Fix exclude paths based on cwd (exclude paths are defined relative to TOML file)
111+ cwd_relpath_from_toml = os .path .relpath (os .getcwd (), pyproject_toml .parent )
112+ if cwd_relpath_from_toml != '.' :
113+ excludes_option = [p .replace (cwd_relpath_from_toml + '/' , '' , 1 ) for p in excludes_option ]
114+ return excludes_option
115+
116+
117+ def has_black_config (pyproject_toml : Optional [Path ]) -> bool :
118+ if pyproject_toml is None :
119+ return False
120+ return pyproject_toml .is_file () and '[tool.black]' in pyproject_toml .read_text (encoding = 'UTF-8' )
121+
122+
84123# caches which directories have the `.clang-format` file, *in or above it*, to avoid hitting the
85124# disk too many times
86125__HAS_DOT_CLANG_FORMAT = dict ()
@@ -313,7 +352,7 @@ def _process_file(filename, check, format_code, *, verbose):
313352 return changed , errors , formatter
314353
315354
316- def run_black_on_python_files (files , check , verbose ) -> Tuple [bool , bool ]:
355+ def run_black_on_python_files (files , check , exclude_patterns , verbose ) -> Tuple [bool , bool ]:
317356 """
318357 Runs black on the given files (checking or formatting).
319358
@@ -326,7 +365,7 @@ def run_black_on_python_files(files, check, verbose) -> Tuple[bool, bool]:
326365
327366 :return: a pair (would_be_formatted, black_failed)
328367 """
329- py_files = [x for x in files if x . suffix == ' .py' and should_format ( str ( x ) )[0 ]]
368+ py_files = [x for x in files if should_format ( str ( x ), [ '* .py'], exclude_patterns )[0 ]]
330369 black_failed = False
331370 would_be_formatted = False
332371 if py_files :
@@ -367,27 +406,35 @@ def _main(files_or_directories, check, stdin, commit, pydevf_format_func, *, ver
367406 for dirname in list (dirs ):
368407 if dirname in SKIP_DIRS :
369408 dirs .remove (dirname )
370- files .extend (os .path .join (root , n ) for n in names if should_format (n ))
409+ files .extend (
410+ os .path .join (root , n ) for n in names if should_format (n , PATTERNS , [])
411+ )
371412 else :
372413 files .append (file_or_dir )
373414
374415 files = sorted (Path (x ) for x in files )
375416 errors = []
376417
377- black_config = find_black_config (files )
418+ pyproject_toml = find_pyproject_toml (files )
419+ if pyproject_toml :
420+ exclude_patterns = read_exclude_patterns (pyproject_toml )
421+ else :
422+ exclude_patterns = []
423+
378424 would_be_formatted = False
379- if black_config :
425+ if has_black_config ( pyproject_toml ) :
380426 # skip pydevf formatter
381427 pydevf_format_func = None
382- would_be_formatted , black_failed = run_black_on_python_files (files , check , verbose )
428+ would_be_formatted , black_failed = \
429+ run_black_on_python_files (files , check , exclude_patterns , verbose )
383430 if black_failed :
384431 errors .append ('Error formatting black (see console)' )
385432
386433 changed_files = []
387434 analysed_files = []
388435 for filename in files :
389436 filename = str (filename )
390- fmt , reason = should_format (filename )
437+ fmt , reason = should_format (filename , PATTERNS , exclude_patterns )
391438 if not fmt :
392439 if verbose :
393440 click .secho (click .format_filename (filename ) + ': ' + reason , fg = 'white' )
0 commit comments