55 List ,
66 Iterable ,
77 Optional ,
8- Any
8+ Any ,
9+ Pattern
910)
1011
1112import click
12- import toml
13+ import re
1314
1415from robotidy .app import Robotidy
1516from robotidy .transformers import load_transformers
17+ from robotidy .files import read_pyproject_config , find_and_read_config , DEFAULT_EXCLUDES
1618from robotidy .utils import (
1719 GlobalFormattingConfig ,
1820 split_args_from_name_or_path ,
@@ -81,70 +83,6 @@ def convert(self, value, param, ctx):
8183 return name , args
8284
8385
84- def find_project_root (srcs : Iterable [str ]) -> Path :
85- """Return a directory containing .git, or robotidy.toml.
86- That directory will be a common parent of all files and directories
87- passed in `srcs`.
88- If no directory in the tree contains a marker that would specify it's the
89- project root, the root of the file system is returned.
90- """
91- if not srcs :
92- return Path ("/" ).resolve ()
93-
94- path_srcs = [Path (Path .cwd (), src ).resolve () for src in srcs ]
95-
96- # A list of lists of parents for each 'src'. 'src' is included as a
97- # "parent" of itself if it is a directory
98- src_parents = [
99- list (path .parents ) + ([path ] if path .is_dir () else []) for path in path_srcs
100- ]
101-
102- common_base = max (
103- set .intersection (* (set (parents ) for parents in src_parents )),
104- key = lambda path : path .parts ,
105- )
106-
107- for directory in (common_base , * common_base .parents ):
108- if (directory / ".git" ).exists ():
109- return directory
110-
111- if (directory / "robotidy.toml" ).is_file ():
112- return directory
113-
114- if (directory / "pyproject.toml" ).is_file ():
115- return directory
116-
117- return directory
118-
119-
120- def find_and_read_config (src_paths : Iterable [str ]) -> Dict [str , Any ]:
121- project_root = find_project_root (src_paths )
122- config_path = project_root / 'robotidy.toml'
123- if config_path .is_file ():
124- return read_pyproject_config (str (config_path ))
125- pyproject_path = project_root / 'pyproject.toml'
126- if pyproject_path .is_file ():
127- return read_pyproject_config (str (pyproject_path ))
128- return {}
129-
130-
131- def load_toml_file (path : str ) -> Dict [str , Any ]:
132- try :
133- config = toml .load (path )
134- click .echo (f"Loaded configuration from { path } " )
135- return config
136- except (toml .TomlDecodeError , OSError ) as e :
137- raise click .FileError (
138- filename = path , hint = f"Error reading configuration file: { e } "
139- )
140-
141-
142- def read_pyproject_config (path : str ) -> Dict [str , Any ]:
143- config = load_toml_file (path )
144- config = config .get ("tool" , {}).get ("robotidy" , {})
145- return {k .replace ('--' , '' ).replace ('-' , '_' ): v for k , v in config .items ()}
146-
147-
14886def parse_opt (opt ):
14987 while opt and opt [0 ] == '-' :
15088 opt = opt [1 :]
@@ -185,6 +123,21 @@ def read_config(ctx: click.Context, param: click.Parameter, value: Optional[str]
185123 ctx .default_map = default_map
186124
187125
126+ def validate_regex_callback (
127+ ctx : click .Context ,
128+ param : click .Parameter ,
129+ value : Optional [str ],
130+ ) -> Optional [Pattern ]:
131+ return validate_regex (value )
132+
133+
134+ def validate_regex (value : Optional [str ]) -> Optional [Pattern ]:
135+ try :
136+ return re .compile (value ) if value is not None else None
137+ except re .error :
138+ raise click .BadParameter ("Not a valid regular expression" )
139+
140+
188141def print_description (name : str ):
189142 transformers = load_transformers (None , {}, allow_disabled = True )
190143 transformer_by_names = {transformer .__class__ .__name__ : transformer for transformer in transformers }
@@ -242,6 +195,27 @@ def print_transformers_list():
242195 is_eager = True ,
243196 metavar = '[PATH(S)]'
244197)
198+ @click .option (
199+ "--exclude" ,
200+ type = str ,
201+ callback = validate_regex_callback ,
202+ help = (
203+ "A regular expression that matches files and directories that should be"
204+ " excluded on recursive searches. An empty value means no paths are excluded."
205+ " Use forward slashes for directories on all platforms."
206+ f" [default: '{ DEFAULT_EXCLUDES } ']"
207+ ),
208+ show_default = False ,
209+ )
210+ @click .option (
211+ "--extend-exclude" ,
212+ type = str ,
213+ callback = validate_regex_callback ,
214+ help = (
215+ "Like --exclude, but adds additional files and directories on top of the"
216+ " excluded ones. (Useful if you simply want to add to the default)"
217+ ),
218+ )
245219@click .option (
246220 "--config" ,
247221 type = click .Path (
@@ -363,6 +337,8 @@ def cli(
363337 transform : List [Tuple [str , List ]],
364338 configure : List [Tuple [str , List ]],
365339 src : Tuple [str , ...],
340+ exclude : Optional [Pattern ],
341+ extend_exclude : Optional [Pattern ],
366342 overwrite : bool ,
367343 diff : bool ,
368344 check : bool ,
@@ -395,6 +371,9 @@ def cli(
395371 print ("No source path provided. Run robotidy --help to see how to use robotidy" )
396372 ctx .exit (0 )
397373
374+ if exclude is None :
375+ exclude = re .compile (DEFAULT_EXCLUDES )
376+
398377 if config and verbose :
399378 click .echo (f'Loaded { config } configuration file' )
400379
@@ -408,6 +387,8 @@ def cli(
408387 transformers = transform ,
409388 transformers_config = configure ,
410389 src = src ,
390+ exclude = exclude ,
391+ extend_exclude = extend_exclude ,
411392 overwrite = overwrite ,
412393 show_diff = diff ,
413394 formatting_config = formatting_config ,
0 commit comments