2929 "*.egg-info" ,
3030]
3131
32+ _ARCHIGNORE_FILE = ".archignore"
33+
3234_IGNORE_DIRECTIVE_REGEX = re .compile (
3335 r"#\s*archunit(?::|-)\s*ignore"
3436 r"(?:\([^)]*\))?"
@@ -88,9 +90,7 @@ def extract_graph(
8890 project_path = os .getcwd ()
8991
9092 project_path = os .path .abspath (project_path )
91- excludes = (
92- list (set (exclude_patterns )) if exclude_patterns is not None else list (_DEFAULT_EXCLUDE )
93- )
93+ excludes = _resolve_exclude_patterns (project_path , exclude_patterns )
9494 ignore_type_checking_imports = bool (options and options .ignore_type_checking_imports )
9595 cache_key = _build_cache_key (project_path , excludes , ignore_type_checking_imports )
9696
@@ -122,6 +122,34 @@ def _build_cache_key(
122122 )
123123
124124
125+ def _resolve_exclude_patterns (
126+ project_path : str ,
127+ exclude_patterns : list [str ] | None ,
128+ ) -> list [str ]:
129+ """Resolve exclude patterns (explicit or defaults) plus any .archignore patterns."""
130+ excludes = list (exclude_patterns ) if exclude_patterns is not None else list (_DEFAULT_EXCLUDE )
131+ excludes .extend (_load_archignore_patterns (project_path ))
132+ return excludes
133+
134+
135+ def _load_archignore_patterns (project_path : str ) -> list [str ]:
136+ """Load .archignore patterns from a project root, if present."""
137+ archignore_path = os .path .join (project_path , _ARCHIGNORE_FILE )
138+ try :
139+ with open (archignore_path , "r" , encoding = "utf-8" , errors = "replace" ) as f :
140+ lines = f .readlines ()
141+ except OSError :
142+ return []
143+
144+ patterns : list [str ] = []
145+ for line in lines :
146+ pattern = line .strip ()
147+ if not pattern or pattern .startswith ("#" ):
148+ continue
149+ patterns .append (pattern )
150+ return patterns
151+
152+
125153def _extract_graph_uncached (
126154 project_path : str ,
127155 exclude_patterns : list [str ],
@@ -160,7 +188,7 @@ def _extract_graph_uncached(
160188 if resolved and resolved != _normalize (file_path ):
161189 # Check if the resolved path is in our project
162190 if not is_external and resolved not in normalized_py_file_set :
163- is_external = True
191+ continue
164192
165193 edges .append (
166194 Edge (
@@ -182,25 +210,65 @@ def _normalize(path: str) -> str:
182210def _find_python_files (root : str , exclude : list [str ]) -> list [str ]:
183211 """Recursively find all .py files, excluding specified patterns."""
184212 py_files : list [str ] = []
213+ root = os .path .abspath (root )
185214 for dirpath , dirnames , filenames in os .walk (root ):
186215 # Filter out excluded directories in-place
187- dirnames [:] = [d for d in dirnames if not _should_exclude (d , exclude )]
216+ dirnames [:] = [
217+ d
218+ for d in dirnames
219+ if not _should_exclude_path (os .path .join (dirpath , d ), root , exclude , is_dir = True )
220+ ]
188221
189222 for filename in filenames :
190- if filename .endswith (".py" ) and not _should_exclude (filename , exclude ):
191- full_path = os .path .join (dirpath , filename )
223+ full_path = os .path .join (dirpath , filename )
224+ if filename .endswith (".py" ) and not _should_exclude_path (
225+ full_path , root , exclude , is_dir = False
226+ ):
192227 py_files .append (os .path .abspath (full_path ))
193228
194229 return py_files
195230
196231
197- def _should_exclude (name : str , patterns : list [str ]) -> bool :
198- """Check if a name matches any exclude pattern."""
232+ def _should_exclude_path (
233+ path : str ,
234+ root : str ,
235+ patterns : list [str ],
236+ * ,
237+ is_dir : bool ,
238+ ) -> bool :
239+ """Check if a path matches any exclude pattern."""
199240 import fnmatch
200241
201- for pattern in patterns :
202- if fnmatch .fnmatch (name , pattern ):
242+ rel_path = _normalize (os .path .relpath (path , root ))
243+ name = os .path .basename (path )
244+
245+ for raw_pattern in patterns :
246+ pattern = raw_pattern .strip ().replace ("\\ " , "/" )
247+ if not pattern or pattern .startswith ("#" ):
248+ continue
249+
250+ pattern = pattern .removeprefix ("./" )
251+ anchored = pattern .startswith ("/" )
252+ if anchored :
253+ pattern = pattern [1 :]
254+
255+ dir_only = pattern .endswith ("/" )
256+ if dir_only :
257+ pattern = pattern .rstrip ("/" )
258+ if not is_dir :
259+ continue
260+
261+ if not pattern :
262+ continue
263+
264+ if "/" in pattern or anchored :
265+ if fnmatch .fnmatch (rel_path , pattern ):
266+ return True
267+ if is_dir and rel_path == pattern :
268+ return True
269+ elif fnmatch .fnmatch (name , pattern ):
203270 return True
271+
204272 return False
205273
206274
0 commit comments