@@ -179,22 +179,85 @@ def _discover_tests(root: str) -> list[DiscoveredTest]:
179179 return sorted (by_qual .values (), key = lambda x : (x .file_path , x .lineno or 0 ))
180180
181181
182+ def _to_pyargs_nodeid (file_path : str , func_name : str ) -> str | None :
183+ """Attempt to build a pytest nodeid suitable for `pytest <nodeid>`.
184+
185+ Preference order:
186+ 1) Dotted package module path with double-colon: pkg.subpkg.module::func
187+ 2) Filesystem path with double-colon: path/to/module.py::func
188+
189+ Returns dotted form when package root can be inferred (directory chain with __init__.py
190+ leading up to a directory contained in sys.path). Returns None if no reasonable
191+ nodeid can be created (should be rare).
192+ """
193+ try :
194+ abs_path = os .path .abspath (file_path )
195+ dir_path , filename = os .path .split (abs_path )
196+ module_base , ext = os .path .splitext (filename )
197+ if ext != ".py" :
198+ # Not a python file
199+ return None
200+
201+ # Walk up while packages have __init__.py
202+ segments : list [str ] = [module_base ]
203+ current = dir_path
204+ package_root = None
205+ while True :
206+ if os .path .isfile (os .path .join (current , "__init__.py" )):
207+ segments .insert (0 , os .path .basename (current ))
208+ parent = os .path .dirname (current )
209+ # Stop if parent is not within current sys.path import roots
210+ if parent == current :
211+ break
212+ current = parent
213+ else :
214+ package_root = current
215+ break
216+
217+ # If we found a package chain, check that the package_root is importable (in sys.path)
218+ if package_root and any (
219+ os .path .abspath (sp ).rstrip (os .sep ) == os .path .abspath (package_root ).rstrip (os .sep ) for sp in sys .path
220+ ):
221+ dotted = "." .join (segments )
222+ return f"{ dotted } ::{ func_name } "
223+
224+ # Do not emit a dotted top-level module for non-packages; prefer path-based nodeid
225+
226+ # Fallback to relative path (if under cwd) or absolute path
227+ cwd = os .getcwd ()
228+ try :
229+ rel = os .path .relpath (abs_path , cwd )
230+ except Exception :
231+ rel = abs_path
232+ return f"{ rel } ::{ func_name } "
233+ except Exception :
234+ return None
235+
236+
182237def _parse_entry (entry : str , cwd : str ) -> tuple [str , str ]:
183- # Accept module:function or path::function
238+ # Accept module:: function, path::function, or legacy module :function
184239 entry = entry .strip ()
185240 if "::" in entry :
186- path_part , func = entry .split ("::" , 1 )
187- abs_path = os .path .abspath (os .path .join (cwd , path_part ))
188- module_name = Path (abs_path ).stem
189- return abs_path , func
241+ target , func = entry .split ("::" , 1 )
242+ # Determine if target looks like a filesystem path; otherwise treat as module path
243+ looks_like_path = (
244+ "/" in target or "\\ " in target or target .endswith (".py" ) or os .path .exists (os .path .join (cwd , target ))
245+ )
246+ if looks_like_path :
247+ abs_path = os .path .abspath (os .path .join (cwd , target ))
248+ return abs_path , func
249+ else :
250+ # Treat as module path for --pyargs style
251+ return target , func
190252 elif ":" in entry :
253+ # Legacy support: module:function → convert to module path + function
191254 module , func = entry .split (":" , 1 )
192255 return module , func
193256 else :
194- raise ValueError ("--entry must be in 'module:function' or 'path::function' format" )
257+ raise ValueError ("--entry must be in 'module:: function', 'path::function', or 'module :function' format" )
195258
196259
197- def _generate_ts_mode_code_from_entry (entry : str , cwd : str ) -> tuple [str , str , str ]:
260+ def _generate_ts_mode_code_from_entry (entry : str , cwd : str ) -> tuple [str , str , str , str ]:
198261 target , func = _parse_entry (entry , cwd )
199262
200263 # Check if target looks like a file path
@@ -217,10 +280,12 @@ def _generate_ts_mode_code_from_entry(entry: str, cwd: str) -> tuple[str, str, s
217280 sys .modules [spec .name ] = module
218281 spec .loader .exec_module (module ) # type: ignore[attr-defined]
219282 module_name = spec .name
283+ source_file_path = target
220284 else :
221285 # Treat as module path (e.g., "my_package.my_module")
222286 module_name = target
223287 module = importlib .import_module (module_name )
288+ source_file_path = getattr (module , "__file__" , "" ) or ""
224289
225290 if not hasattr (module , func ):
226291 raise ValueError (f"Function '{ func } ' not found in module '{ module_name } '" )
@@ -238,7 +303,7 @@ def _generate_ts_mode_code_from_entry(entry: str, cwd: str) -> tuple[str, str, s
238303 nodeids = [],
239304 )
240305 )
241- return code , file_name , qualname
306+ return code , file_name , qualname , os . path . abspath ( source_file_path ) if source_file_path else ""
242307
243308
244309def _generate_ts_mode_code (test : DiscoveredTest ) -> tuple [str , str ]:
@@ -440,10 +505,8 @@ def upload_command(args: argparse.Namespace) -> int:
440505 entries = [e .strip () for e in re .split (r"[,\s]+" , entries_arg ) if e .strip ()]
441506 selected_specs : list [tuple [str , str , str , str ]] = []
442507 for e in entries :
443- code , file_name , qualname = _generate_ts_mode_code_from_entry (e , root )
444- # For --entry mode, extract file path from the entry
445- file_path = e .split ("::" )[0 ] if "::" in e else ""
446- selected_specs .append ((code , file_name , qualname , file_path ))
508+ code , file_name , qualname , resolved_path = _generate_ts_mode_code_from_entry (e , root )
509+ selected_specs .append ((code , file_name , qualname , resolved_path ))
447510 else :
448511 print ("Scanning for evaluation tests..." )
449512 tests = _discover_tests (root )
@@ -496,15 +559,21 @@ def upload_command(args: argparse.Namespace) -> int:
496559 # Normalize the evaluator ID to meet Fireworks requirements
497560 evaluator_id = _normalize_evaluator_id (evaluator_id )
498561
499- # Compute entry point metadata for backend: prefer module:function, fallback to path::function
562+ # Compute entry point metadata for backend as a pytest nodeid usable with `pytest <entrypoint>`
563+ # Always prefer a path-based nodeid to work in plain pytest environments (server may not use --pyargs)
500564 func_name = qualname .split ("." )[- 1 ]
501- module_part = qualname .rsplit ("." , 1 )[0 ] if "." in qualname else ""
502- # Use pytest pyargs style: package.module:function
503- if module_part and "." in module_part :
504- entry_point = f"{ module_part } :{ func_name } "
565+ entry_point = None
566+ if source_file_path :
567+ # Use path relative to current working directory if possible
568+ abs_path = os .path .abspath (source_file_path )
569+ try :
570+ rel = os .path .relpath (abs_path , root )
571+ except Exception :
572+ rel = abs_path
573+ entry_point = f"{ rel } ::{ func_name } "
505574 else :
506- # If we cannot derive a dotted module path, don't set entry point
507- entry_point = None
575+ # Fallback: use filename from qualname only (rare)
576+ entry_point = f" { func_name } .py:: { func_name } "
508577
509578 print (f"\n Uploading evaluator '{ evaluator_id } ' for { qualname .split ('.' )[- 1 ]} ..." )
510579 try :
@@ -524,7 +593,27 @@ def upload_command(args: argparse.Namespace) -> int:
524593 # Print success message with Fireworks dashboard link
525594 print (f"\n ✅ Successfully uploaded evaluator: { evaluator_id } " )
526595 print ("📊 View in Fireworks Dashboard:" )
527- print (f" https://app.fireworks.ai/dashboard/evaluators/{ evaluator_id } " )
596+ # Map API base to app host (e.g., dev.api.fireworks.ai -> dev.app.fireworks.ai)
597+ from urllib .parse import urlparse
598+
599+ api_base = os .environ .get ("FIREWORKS_API_BASE" , "https://api.fireworks.ai" )
600+ try :
601+ parsed = urlparse (api_base )
602+ host = parsed .netloc or parsed .path # handle cases where scheme may be missing
603+ # Mapping rules:
604+ # - dev.api.fireworks.ai → dev.fireworks.ai
605+ # - *.api.fireworks.ai → *.app.fireworks.ai (default)
606+ if host .startswith ("dev.api.fireworks.ai" ):
607+ app_host = "dev.fireworks.ai"
608+ elif host .startswith ("api." ):
609+ app_host = host .replace ("api." , "app." , 1 )
610+ else :
611+ app_host = host
612+ scheme = parsed .scheme or "https"
613+ dashboard_url = f"{ scheme } ://{ app_host } /dashboard/evaluators/{ evaluator_id } "
614+ except Exception :
615+ dashboard_url = f"https://app.fireworks.ai/dashboard/evaluators/{ evaluator_id } "
616+ print (f" { dashboard_url } " )
528617 print ()
529618 except Exception as e :
530619 print (f"Failed to upload { qualname } : { e } " )
0 commit comments