5757 PlainValidator ,
5858 RootModel ,
5959 ValidationError ,
60+ ValidationInfo ,
6061 WithJsonSchema ,
6162 field_validator ,
6263 model_validator ,
6364)
6465
65- from fastsandpm .dependencies .requirements import ConcreteRequirement
66+ from fastsandpm .dependencies .requirements import ConcreteRequirement , PathRequirement
6667from fastsandpm .registries import Registries
6768from fastsandpm .versioning import LibraryVersion
6869
@@ -404,6 +405,48 @@ def parse_optional_dependencies(cls, data: Any) -> Any:
404405 data ["optional_dependencies" ] = new_opt_deps
405406 return data
406407
408+ @model_validator (mode = "after" )
409+ def _resolve_path_requirement_paths (self , info : ValidationInfo ) -> Manifest :
410+ """Resolve relative paths in PathRequirements to absolute paths.
411+
412+ Creates a new Manifest with all relative path dependencies resolved
413+ relative to the manifest file's directory.
414+
415+ Args:
416+ manifest: The parsed Manifest object.
417+ manifest_dir: The directory containing the manifest file.
418+
419+ Returns:
420+ A new Manifest with resolved path dependencies.
421+ """
422+ if isinstance (info .context , dict ) and "manifest_dir" in info .context :
423+ manifest_dir = pathlib .Path (info .context ["manifest_dir" ])
424+ else :
425+ # No manifest directory context provided (e.g., loading from bytes)
426+ # Keep relative paths as-is
427+ return self
428+
429+ def resolve_dep (dep : ConcreteRequirement ) -> ConcreteRequirement :
430+ """Resolve paths in a single dependency."""
431+ if isinstance (dep , PathRequirement ) and not dep .path .is_absolute ():
432+ resolved_path = (manifest_dir / dep .path ).resolve ()
433+ return dep .model_copy (update = {"path" : resolved_path })
434+ return dep
435+
436+ # Resolve paths in required dependencies
437+ new_deps = Dependencies ([resolve_dep (dep ) for dep in self .dependencies ])
438+
439+ # Resolve paths in optional dependencies
440+ new_opt_deps : dict [str , Dependencies ] = {}
441+ for group_name , deps in self .optional_dependencies .items ():
442+ new_opt_deps [group_name ] = Dependencies ([resolve_dep (dep ) for dep in deps ])
443+
444+ # Create new manifest with resolved paths
445+ self .dependencies = new_deps
446+ self .optional_dependencies = new_opt_deps
447+
448+ return self
449+
407450
408451#: The default manifest filename
409452MANIFEST_FILENAME = "proj.toml"
@@ -413,13 +456,14 @@ def get_manifest(path: os.PathLike) -> Manifest:
413456 """Load and parse a manifest from a repository path.
414457
415458 Looks for a `proj.toml` file in the specified directory, parses it,
416- and returns a Manifest object.
459+ and returns a Manifest object. Relative paths in path dependencies are
460+ resolved to absolute paths relative to the manifest file's directory.
417461
418462 Args:
419463 path: Path to the repository directory containing the proj.toml file.
420464
421465 Returns:
422- The parsed Manifest object.
466+ The parsed Manifest object with resolved path dependencies .
423467
424468 Raises:
425469 ManifestNotFoundError: If the proj.toml file does not exist at the path.
@@ -454,10 +498,13 @@ def get_manifest(path: os.PathLike) -> Manifest:
454498
455499 # Parse the data into a Manifest object
456500 try :
457- return Manifest .model_validate (data )
501+ manifest = Manifest .model_validate (data , context = { "manifest_dir" : path . resolve ()} )
458502 except ValidationError as e :
459503 raise ManifestParseError (path , str (e )) from e
460504
505+ # Resolve relative paths in path dependencies
506+ return manifest
507+
461508
462509def get_manifest_from_bytes (content : bytes , source : str = "<bytes>" ) -> Manifest :
463510 """Parse a manifest from raw bytes content.
0 commit comments