@@ -81,6 +81,12 @@ def _is_eval_protocol_test(obj: Any) -> bool:
8181 return False
8282 # Must have pytest marks from evaluation_test
8383 marks = getattr (obj , "pytestmark" , [])
84+ # Handle pytest proxy objects (APIRemovedInV1Proxy)
85+ if not isinstance (marks , (list , tuple )):
86+ try :
87+ marks = list (marks ) if marks else []
88+ except (TypeError , AttributeError ):
89+ return False
8490 return len (marks ) > 0
8591
8692
@@ -91,6 +97,14 @@ def _extract_param_info_from_marks(obj: Any) -> tuple[bool, int, list[str]]:
9197 (has_parametrize, param_count, param_ids)
9298 """
9399 marks = getattr (obj , "pytestmark" , [])
100+
101+ # Handle pytest proxy objects (APIRemovedInV1Proxy) - same as _is_eval_protocol_test
102+ if not isinstance (marks , (list , tuple )):
103+ try :
104+ marks = list (marks ) if marks else []
105+ except (TypeError , AttributeError ):
106+ marks = []
107+
94108 has_parametrize = False
95109 total_combinations = 0
96110 all_param_ids : list [str ] = []
@@ -131,51 +145,103 @@ def _discover_tests(root: str) -> list[DiscoveredTest]:
131145
132146 discovered : list [DiscoveredTest ] = []
133147
134- # Collect all test functions from Python files
135- for file_path in _iter_python_files (root ):
148+ class CollectionPlugin :
149+ """Plugin to capture collected items without running code."""
150+
151+ def __init__ (self ):
152+ self .items = []
153+
154+ def pytest_ignore_collect (self , collection_path , config ):
155+ """Ignore problematic files before pytest tries to import them."""
156+ # Ignore specific files
157+ ignored_files = ["setup.py" , "versioneer.py" , "conf.py" , "__main__.py" ]
158+ if collection_path .name in ignored_files :
159+ return True
160+
161+ # Ignore hidden files (starting with .)
162+ if collection_path .name .startswith ("." ):
163+ return True
164+
165+ # Ignore test_discovery files
166+ if collection_path .name .startswith ("test_discovery" ):
167+ return True
168+
169+ return None
170+
171+ def pytest_collection_modifyitems (self , items ):
172+ """Hook called after collection is done."""
173+ self .items = items
174+
175+ plugin = CollectionPlugin ()
176+
177+ # Run pytest collection only (--collect-only prevents code execution)
178+ # Override python_files to collect from ANY .py file
179+ args = [
180+ abs_root ,
181+ "--collect-only" ,
182+ "-q" ,
183+ "--pythonwarnings=ignore" ,
184+ "-o" ,
185+ "python_files=*.py" , # Override to collect all .py files
186+ ]
187+
188+ try :
189+ # Suppress pytest output
190+ import io
191+ import contextlib
192+
193+ with contextlib .redirect_stdout (io .StringIO ()), contextlib .redirect_stderr (io .StringIO ()):
194+ pytest .main (args , plugins = [plugin ])
195+ except Exception :
196+ # If pytest collection fails, fall back to empty list
197+ return []
198+
199+ # Process collected items
200+ for item in plugin .items :
201+ if not hasattr (item , "obj" ):
202+ continue
203+
204+ obj = item .obj
205+ if not _is_eval_protocol_test (obj ):
206+ continue
207+
208+ origin = getattr (obj , "_origin_func" , obj )
136209 try :
137- unique_name = "ep_upload_" + re .sub (r"[^a-zA-Z0-9_]" , "_" , os .path .abspath (file_path ))
138- spec = importlib .util .spec_from_file_location (unique_name , file_path )
139- if spec and spec .loader :
140- module = importlib .util .module_from_spec (spec )
141- sys .modules [spec .name ] = module
142- spec .loader .exec_module (module ) # type: ignore[attr-defined]
143- else :
144- continue
210+ src_file = inspect .getsourcefile (origin ) or str (item .path )
211+ _ , lineno = inspect .getsourcelines (origin )
145212 except Exception :
146- continue
213+ src_file , lineno = str ( item . path ), None
147214
148- for name , obj in inspect .getmembers (module ):
149- if _is_eval_protocol_test (obj ):
150- origin = getattr (obj , "_origin_func" , obj )
151- try :
152- src_file = inspect .getsourcefile (origin ) or file_path
153- _ , lineno = inspect .getsourcelines (origin )
154- except Exception :
155- src_file , lineno = file_path , None
156-
157- # Extract parametrization info from marks
158- has_parametrize , param_count , param_ids = _extract_param_info_from_marks (obj )
159-
160- # Generate synthetic nodeids for display
161- base_nodeid = f"{ os .path .basename (file_path )} ::{ name } "
162- if has_parametrize and param_ids :
163- nodeids = [f"{ base_nodeid } [{ pid } ]" for pid in param_ids ]
164- else :
165- nodeids = [base_nodeid ]
166-
167- discovered .append (
168- DiscoveredTest (
169- module_path = module .__name__ ,
170- module_name = module .__name__ ,
171- qualname = f"{ module .__name__ } .{ name } " ,
172- file_path = os .path .abspath (src_file ),
173- lineno = lineno ,
174- has_parametrize = has_parametrize ,
175- param_count = param_count ,
176- nodeids = nodeids ,
177- )
178- )
215+ # Extract parametrization info from marks
216+ has_parametrize , param_count , param_ids = _extract_param_info_from_marks (obj )
217+
218+ # Get module name and function name
219+ module_name = (
220+ item .module .__name__
221+ if hasattr (item , "module" )
222+ else item .nodeid .split ("::" )[0 ].replace ("/" , "." ).replace (".py" , "" )
223+ )
224+ func_name = item .name .split ("[" )[0 ] if "[" in item .name else item .name
225+
226+ # Generate nodeids
227+ base_nodeid = f"{ os .path .basename (src_file )} ::{ func_name } "
228+ if param_ids :
229+ nodeids = [f"{ base_nodeid } [{ pid } ]" for pid in param_ids ]
230+ else :
231+ nodeids = [base_nodeid ]
232+
233+ discovered .append (
234+ DiscoveredTest (
235+ module_path = module_name ,
236+ module_name = module_name ,
237+ qualname = f"{ module_name } .{ func_name } " ,
238+ file_path = os .path .abspath (src_file ),
239+ lineno = lineno ,
240+ has_parametrize = has_parametrize ,
241+ param_count = param_count ,
242+ nodeids = nodeids ,
243+ )
244+ )
179245
180246 # Deduplicate by qualname (in case same test appears multiple times)
181247 by_qual : dict [str , DiscoveredTest ] = {}
0 commit comments