diff --git a/README.md b/README.md index aac0ab4..b3e36b3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,50 @@ # FastSandPM -A package management and dependency resolution tool for HDL Design and DV projects +An RTL Design and DV package manager for python tools. Manage your RTL and +design verification library dependencies by cloning, updating, and +version-controlling git repositories. [![Tests](https://github.com/RISCY-Lib/fastsandpm/actions/workflows/run-ci-tests.yml/badge.svg)](https://github.com/RISCY-Lib/fastsandpm/actions/workflows/run-ci-tests.yml) [![PyPI Latest Release](https://img.shields.io/pypi/v/fastsandpm.svg)](https://pypi.org/project/fastsandpm/) [![docs](https://readthedocs.org/projects/fastsandpm/badge)](https://fastsandpm.readthedocs.io/en/latest/index.html) See [Read-The-Docs](https://fastsandpm.readthedocs.io/en/latest/index.html) for details + +Key Features +------------ + +- **Library Management**: Clone and update RTL/DV libraries from git repositories +- **Version Pinning**: Pin libraries to specific tags, branches, or commits +- **Version Ranges**: Specify flexible version constraints (e.g., ``>=1.0.0,<2.0.0``) +- **TOML Configuration**: Organize libraries in configuration files with sub-headings +- **Local Development**: Symlink local directories for development workflows +- **Multi-Remote Support**: Automatically discover repositories across configured remotes + +Quick Start +----------- + +Installation: + +.. code-block:: bash + + # For UV Based projects + uv add fastsandpm + + # For pip-based projects + pip install fastsandpm + +Basic Usage: + + >>> import pathlib + >>> import fastsandpm + >>> manifest = fastsandpm.get_manifest("./my-project") + >>> print(manifest.package.name) + 'my-package' + >>> resolved = fastsandpm.dependencies.resolve(manifest) + >>> print(type(resolved)) + + >>> build_library(resolved, pathlib.Path("my-library")) + +This will bring in the library dependencies for a project into the specified directory. +Additionally, a 'dependencies.f' file will be created which will point to the dependencies file list in the required order. + +For more examples, see [the docs](https://fastsandpm.readthedocs.io/en/latest/usage_guide/index.html) for details. diff --git a/docs/source/index.rst b/docs/source/index.rst index 55af67c..85ced4a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,6 +1,7 @@ .. fastsandpm documentation master file fastsandpm Documentation + =========================================== An RTL Design and DV package manager for python tools. Manage your RTL and @@ -32,19 +33,15 @@ Installation: Basic Usage: -.. code-block:: python - - import pathlib - import fastsandpm - - # Get the manifest of the current project - manifest = fastsandpm.get_manifest(pathlib.Path("some/repo/path")) - - # Resolve the libraries - deps = fastsandpm.resolve_dependencies(manifest.dependencies, manifest.optional_dependencies["dev"]) - - # Update the libraries - fastsandpm.update_deps(libraries, pathlib.Path("some/library/.path")) + >>> import pathlib + >>> import fastsandpm + >>> manifest = fastsandpm.get_manifest("./my-project") + >>> print(manifest.package.name) + 'my-package' + >>> resolved = fastsandpm.dependencies.resolve(manifest) + >>> print(type(resolved)) + + >>> build_library(resolved, pathlib.Path("my-library")) This will bring in the library dependencies for a project into the specified directory. Additionally, a 'dependencies.f' file will be created which will point to the dependencies file list in the required order. diff --git a/docs/source/manifest_reference/manifest_format.rst b/docs/source/manifest_reference/manifest_format.rst index 6f4ce8f..216fffa 100644 --- a/docs/source/manifest_reference/manifest_format.rst +++ b/docs/source/manifest_reference/manifest_format.rst @@ -4,7 +4,7 @@ The Manifest Format =================== The `proj.toml` file within a package is called its *manifest* file. -It is generally written i8n the `TOML `__ format. +It is generally written in the `TOML `__ format. Specifically, it currently supports `TOML v1.0.0 `__. It contains the metadata needed for inclusion of the package into a HDL/RTL project. @@ -14,13 +14,14 @@ Every manifest contains some of the following: - :ref:`name ` - The name of the package - :ref:`version ` - The version of the package - - :ref:`description ` - The description of the package - - :ref:`authors ` - The authors of the package + - :ref:`description ` - (Optional) The description of the package + - :ref:`authors ` - (Optional) The authors of the package - :ref:`readme ` - The readme of the package + - :ref:`flist ` - The file list of the package - :ref:`[dependencies] ` - :ref:`[optional_dependencies] ` -- :ref:`[regsistries] ` +- :ref:`[registries] ` .. _package_section: @@ -38,6 +39,9 @@ The ``name`` field The package name is a unique identifier used to refer to the package. It is used when listed as a dependency in other manifests and as the default inferred name for certain other fields. +The name field is required and must be a non-empty string. +Empty strings and whitespace-only strings are not allowed and will result in a validation error. + .. _version_field: @@ -57,21 +61,69 @@ The ``description`` field The ``description`` is a short summary of the package. This field is treated as a plain-text string. +This field is optional. +If not specified, it defaults to an empty string. +However, providing a description is strongly recommended as it helps users understand the purpose and functionality of the package. + +Example TOML with description: + +.. code-block:: TOML + + [package] + name = "my-package" + version = "1.0.0" + description = "A package for HDL design and verification" + +Example TOML without description (uses default empty string): + +.. code-block:: TOML + + [package] + name = "my-package" + version = "1.0.0" + .. _authors_field: The ``authors`` field ^^^^^^^^^^^^^^^^^^^^^^ -The ``authors`` of the package. -This field is optional. -If not specified, it defaults to not including any author information. -This field can either be an array of strings, or an array of dictionaries. +The ``authors`` field specifies the authors of the package. +This field is optional. If not specified, no author information is included. + +The ``authors`` field accepts several formats: -If the ``authors`` field is an array of strings, it is treated as a list. -It is permissible to include the authors email within angled brackets at the end of the authors string. +**Single string** (with optional email in angle brackets): -If the ``authors`` field is an array of dictionaries, it is treated as a list of authors where the valid keys are ``name`` and ``email``. +.. code-block:: TOML + + [package] + authors = "John Doe" + + # Or with email: + authors = "John Doe " + +**List of strings**: + +.. code-block:: TOML + + [package] + authors = [ + "John Doe ", + "Jane Smith " + ] + +**Dictionary with name and email**: + +.. code-block:: TOML + + [package] + authors = {name = "John Doe", email = "john.doe@example.com"} + + # Email is optional: + authors = {name = "John Doe"} + +Choose the format that best suits your needs. For multiple authors, the list format is most readable. .. _readme_field: @@ -85,6 +137,42 @@ If not specified, it defaults to not including any readme information. This field is relative to the location of the manifest file. +.. _flist_field: + +The ``flist`` field +^^^^^^^^^^^^^^^^^^^ + +The ``flist`` field points to the relative location of the file list for the package. +A file list contains the paths to all source files that should be included when this package is used as a dependency in another project. + +This field is optional. +If not specified, it defaults to ``.f`` in the same directory as the manifest file. +For example, if the package name is ``my-package``, the default file list would be ``my-package.f``. +This field is relative to the location of the manifest file. + +Example TOML with explicit file list: + +.. code-block:: TOML + + [package] + name = "my-package" + version = "1.0.0" + description = "A sample package" + flist = "rtl/sources.f" + +Example TOML using the default file list (``my-package.f``): + +.. code-block:: TOML + + [package] + name = "my-package" + version = "1.0.0" + description = "A sample package" + +When a ``fastsandpm`` compatible package is used as a dependency, the specified file list is automatically included in the dependent project. +For projects that do not specify an ``flist`` field, FastSandPM will look for a file named ``.f`` at the root of the dependency directory. + + .. _dependencies_section: The ``[dependencies]`` section @@ -100,9 +188,9 @@ The dependencies section contains a group of :ref:`dependency specifiers ` field of the ``[package]`` section. +Any other project will use a file list located at the top of the dependency's directory with the same name as the dependency. For example: @@ -128,12 +216,12 @@ For example: .. code-block:: TOML - [optional_dependencies] + [optional_dependencies] - uvm = [ - {name="uvm_utils", version="^1.0.0"}, - {name="improved_uvm_ral", version=">0.1.0", git="DCC_UVM"}, - ] + uvm = [ + {name="uvm_utils", version="^1.0.0"}, + {name="improved_uvm_ral", version=">0.1.0", git="DCC_UVM"}, + ] The above creates a UVM dependency group that contains the ``uvm_utils`` and ``improved_uvm_ral`` packages. @@ -141,18 +229,48 @@ An alternative but equally valid way to write this TOML would be: .. code-block:: TOML - [optional_dependencies.uvm] + [optional_dependencies.uvm] - uvm_utils = "^1.0.0" - improved_uvm_ral = {version=">0.1.0", git="DCC_UVM"} + uvm_utils = "^1.0.0" + improved_uvm_ral = {version=">0.1.0", git="DCC_UVM"} + + +Format Equivalence +^^^^^^^^^^^^^^^^^^ + +The two formats shown above are completely equivalent. Both create the same dependency group structure. +Choose the format that is most readable for your use case: + +- **List format** (``[optional_dependencies]`` with array): Better when copying dependency specifications or when dependencies have complex configurations +- **Table format** (``[optional_dependencies.group]``): More concise and readable for simple version specifications + +The following two specifications are identical: + +.. code-block:: TOML + + # List format + [optional_dependencies] + dev = [ + {name = "pytest", version = "^7.0.0"}, + {name = "mypy", version = "^1.0.0"} + ] + +.. code-block:: TOML + + # Table format (recommended for simple cases) + [optional_dependencies.dev] + pytest = "^7.0.0" + mypy = "^1.0.0" + +Choose whichever format makes your manifest more readable and maintainable for your project's needs. .. _registries_section: -The ``[registeries]`` section +The ``[registries]`` section ----------------------------- -Additional registeries can be specified in the ``[registries]`` section. -A regsitry type exists for each :ref:`dependency specifier ` type. +Additional registries can be specified in the ``[registries]`` section. +A registry type exists for each :ref:`dependency specifier ` type. diff --git a/docs/source/manifest_reference/specifying_dependencies.rst b/docs/source/manifest_reference/specifying_dependencies.rst index edbd26c..96c817e 100644 --- a/docs/source/manifest_reference/specifying_dependencies.rst +++ b/docs/source/manifest_reference/specifying_dependencies.rst @@ -31,21 +31,21 @@ For example you can specify a dependency named ``time`` and version ``1.0.0`` as time = "1.0.0" The version string is a known as a :ref:`version specifier `. -These specifiers can be used to set a range of valid versions used for resolving dpendencies. +These specifiers can be used to set a range of valid versions used for resolving dependencies. Dependencies from Other Registries ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ It is also possible to point to a different index using the ``index`` key in the dependency. -For example you can specify a dependency named ``time`` and version ``1.0.0`` as follows: +For example you can specify a dependency named ``time`` and version ``1.0.0`` from a custom registry as follows: .. code-block:: TOML [dependencies] - time = {registry = "my-registry", version = "1.0.0"} + time = {index = "my-registry", version = "1.0.0"} -The value of ``registry`` points to a regsiter definition from the ``[registries]`` section. of the manifest. +The value of ``index`` points to a registry definition from the ``[registries]`` section of the manifest. See the :ref:`registries section documentation ` for more information. @@ -77,7 +77,7 @@ By default the upstream default branch will be used. .. note:: If when the dependencies are updated the existing local copy is dirty an error will be raised. - A user of the library can force the update by setting the ``force_update`` flag to ``true`` or ignore the error by settign ``ignore_dirty`` to ``true``. + A user of the library can force the update by setting the ``force_update`` flag to ``true`` or ignore the error by setting ``ignore_dirty`` to ``true``. Git Commit & Tag Specifiers diff --git a/docs/source/usage_guide/index.rst b/docs/source/usage_guide/index.rst index 438086d..0a1aefc 100644 --- a/docs/source/usage_guide/index.rst +++ b/docs/source/usage_guide/index.rst @@ -23,4 +23,6 @@ Or with pip: Basic Usage ----------- +.. NOTE:: + TODO: Work in progress diff --git a/src/fastsandpm/__init__.py b/src/fastsandpm/__init__.py index 9e29135..60cc299 100644 --- a/src/fastsandpm/__init__.py +++ b/src/fastsandpm/__init__.py @@ -32,19 +32,26 @@ >>> manifest = fastsandpm.get_manifest("./my-project") >>> print(manifest.package.name) 'my-package' + >>> resolved = fastsandpm.dependencies.resolve(manifest) + >>> print(type(resolved)) + + >>> build_library(resolved, pathlib.Path("my-library")) -Classes: - Manifest: The main manifest model representing a proj.toml file. - Package: Package metadata (name, version, description, authors). - ManifestNotFoundError: Raised when a manifest file cannot be found. - ManifestParseError: Raised when a manifest file cannot be parsed. +Included Classes: + - :py:class:`~manifest.Manifest`: The main manifest model representing a `proj.toml` file. + - :py:class:`~manifest.Package`: Package metadata (name, version, description, authors). + - :py:class:`~manifest.ManifestNotFoundError`: Raised when a manifest file cannot be found. + - :py:class:`~manifest.ManifestParseError`: Raised when a manifest file cannot be parsed. Functions: - get_manifest: Load and parse a manifest from a repository path. + - :py:func:`~manifest.get_manifest`: Load and parse a manifest from a repository path. + - :py:func:`~dependencies.resolve`: Resolve all dependencies for a manifest. + - :py:func:`~install.build_library`: Build a library from a resovled dependency definition. + - :py:func:`~install.library_from_manifest`: Build a library of dependencies from a manifest. Attributes: - __version__: The current version of the FastSandPM package. - __author__: The primary author of the package. + __version__ (str): The current version of the FastSandPM package. + __author__ (str): The primary author of the package. See Also: - fastsandpm.dependencies: Dependency resolution subpackage diff --git a/src/fastsandpm/_git_utils.py b/src/fastsandpm/_git_utils.py index 3f5153a..3af8284 100644 --- a/src/fastsandpm/_git_utils.py +++ b/src/fastsandpm/_git_utils.py @@ -24,36 +24,35 @@ git-based sources. Repository Operations: - clone: Clone a repository from a remote URL. - checkout: Checkout a specific commit, branch, or tag. - fetch: Fetch updates from the remote repository. + - clone: Clone a repository from a remote URL. + - checkout: Checkout a specific commit, branch, or tag. + - fetch: Fetch updates from the remote repository. Repository State: - is_dirty: Check if a repository has uncommitted changes. - is_git_repo: Check if a path is a git repository. - get_head_commit: Get the HEAD commit hash. - get_current_branch: Get the current branch name. - get_tags_at_head: Get all tags pointing to HEAD. - get_remote_url: Get the URL of a remote. + - is_dirty: Check if a repository has uncommitted changes. + - is_git_repo: Check if a path is a git repository. + - get_head_commit: Get the HEAD commit hash. + - get_current_branch: Get the current branch name. + - get_tags_at_head: Get all tags pointing to HEAD. + - get_remote_url: Get the URL of a remote. Remote Operations: - remote_exists: Check if a remote repository is accessible. - get_available_tags: Get all tags from a remote repository. - get_remote_refs: Get all refs grouped by commit hash (cached). - get_commit_for_ref: Get commit hash for a specific ref. - get_remote_file: Fetch a single file from a remote repository. + - remote_exists: Check if a remote repository is accessible. + - get_available_tags: Get all tags from a remote repository. + - get_remote_refs: Get all refs grouped by commit hash (cached). + - get_commit_for_ref: Get commit hash for a specific ref. + - get_remote_file: Fetch a single file from a remote repository. URL Parsing: - parse_github_url: Parse GitHub URLs to extract owner/repo. - parse_gitlab_url: Parse GitLab URLs to extract host/project path. + - parse_github_url: Parse GitHub URLs to extract owner/repo. + - parse_gitlab_url: Parse GitLab URLs to extract host/project path. Hosting Provider APIs: - fetch_file_from_github: Fetch a file via GitHub raw content URL. - fetch_file_from_gitlab: Fetch a file via GitLab repository API. - fetch_file_from_hosting_api: Try fetching via supported hosting APIs. + - fetch_file_from_github: Fetch a file via GitHub raw content URL. + - fetch_file_from_gitlab: Fetch a file via GitLab repository API. + - fetch_file_from_hosting_api: Try fetching via supported hosting APIs. -Note: - This module requires git to be installed and available in the system PATH. +.. note:: This module requires git to be installed and available in the system PATH. """ from __future__ import annotations diff --git a/src/fastsandpm/dependencies/__init__.py b/src/fastsandpm/dependencies/__init__.py index 4d75a5e..eeae07c 100644 --- a/src/fastsandpm/dependencies/__init__.py +++ b/src/fastsandpm/dependencies/__init__.py @@ -22,28 +22,25 @@ including requirement definitions, candidate generation, and resolution algorithms. -Modules: - requirements: Defines requirement types for different dependency sources. - candidates: Implements candidate generation from requirements. - provider: Provides the dependency resolution provider for resolvelib. +Included Classes: + - :py:class:`~requirements.ConcreteRequirement`: Union type of all concrete requirement types. + - :py:class:`~requirements.GitRequirement`: Base class for git-based requirements. + - :py:class:`~requirements.BranchGitRequirement`: Git requirement pinned to a specific branch. + - :py:class:`~requirements.CommitGitRequirement`: Git requirement pinned to a specific commit. + - :py:class:`~requirements.TaggedGitRequirement`: Git requirement pinned to a specific tag. + - :py:class:`~requirements.VersionedGitRequirement`: Git requirement with version constraints. + - :py:class:`~requirements.PackageIndexRequirement`: Requirement from a package index. + - :py:class:`~requirements.PathRequirement`: Requirement from a local filesystem path. + - :py:class:`~candidates.Candidate`: Abstract base class for dependency candidates. + - :py:class:`~candidates.PackageIndexCandidate`: Candidate from a package index registry. + - :py:class:`~candidates.PathCandidate`: Candidate from a local filesystem path. + - :py:class:`~candidates.GitCandidate`: Candidate from a git repository. -Classes: - ConcreteRequirement: Union type of all concrete requirement types. - GitRequirement: Base class for git-based requirements. - BranchGitRequirement: Git requirement pinned to a specific branch. - CommitGitRequirement: Git requirement pinned to a specific commit. - TaggedGitRequirement: Git requirement pinned to a specific tag. - VersionedGitRequirement: Git requirement with version constraints. - PackageIndexRequirement: Requirement from a package index. - PathRequirement: Requirement from a local filesystem path. - Candidate: Abstract base class for dependency candidates. - PackageIndexCandidate: Candidate from a package index registry. - PathCandidate: Candidate from a local filesystem path. - GitCandidate: Candidate from a git repository. +Included Functions: + - :py:func:`~candidates.candidate_factory`: Singledispatch function to create candidates + from requirements. -Functions: - candidate_factory: Singledispatch function to create candidates from requirements. - resolve: Resolve all dependencies for a manifest. + - :py:func:`~provider.resolve`: Resolve all dependencies for a manifest. """ from __future__ import annotations diff --git a/src/fastsandpm/dependencies/candidates.py b/src/fastsandpm/dependencies/candidates.py index a4d2ba4..d34aedf 100644 --- a/src/fastsandpm/dependencies/candidates.py +++ b/src/fastsandpm/dependencies/candidates.py @@ -23,13 +23,13 @@ concrete versions of dependencies that can satisfy requirements. Classes: - Candidate: Abstract base class for all candidate types. - PackageIndexCandidate: Candidate from a package index registry. - PathCandidate: Candidate from a local filesystem path. - GitCandidate: Candidate from a git repository. + - :py:class:`Candidate`: Abstract base class for all candidate types. + - :py:class:`PackageIndexCandidate`: Candidate from a package index registry. + - :py:class:`PathCandidate`: Candidate from a local filesystem path. + - :py:class:`GitCandidate`: Candidate from a git repository. Functions: - candidate_factory: Singledispatch function to create candidates from requirements. + - :py:func:`candidate_factory`: Singledispatch function to create candidates from requirements. """ from __future__ import annotations @@ -87,6 +87,7 @@ def satisfies(self, requirement: ConcreteRequirement) -> bool: """Check if this candidate satisfies the given requirement. A candidate satisfies a requirement when: + - The requirement name and candidate name match - If the requirement specifies a version, the candidate's version matches @@ -114,8 +115,9 @@ class PackageIndexCandidate(Candidate): Artifactory. This implementation is currently a placeholder as package index registries are not yet fully implemented. - Note: - Package index registry support is under development. + .. warning:: + + Package index registry support is under development and is not currently implemented. """ def get_manifest(self) -> Manifest | None: @@ -169,9 +171,9 @@ def satisfies(self, requirement: ConcreteRequirement) -> bool: A path candidate satisfies a requirement when the requirement name and candidate name match, and type-specific conditions are met: - * For PackageIndexRequirement: the candidate's version matches the specifier - * For PathRequirement: the candidate path ends with the requirement path - * For Git requirements: the path is a git repo and HEAD complies with + - For PackageIndexRequirement: the candidate's version matches the specifier + - For PathRequirement: the candidate path ends with the requirement path + - For Git requirements: the path is a git repo and HEAD complies with the requirement's constraints (commit, branch, tag, or version) Args: @@ -191,7 +193,7 @@ def satisfies(self, requirement: ConcreteRequirement) -> bool: req_parts = requirement.path.parts cand_parts = self.path.parts if len(req_parts) <= len(cand_parts): - return cand_parts[-len(req_parts) :] == req_parts + return cand_parts[-len(req_parts):] == req_parts return False # Handle Git requirements: check if candidate points to a git repo @@ -327,6 +329,7 @@ def satisfies(self, requirement: ConcreteRequirement) -> bool: """Check if this git candidate satisfies the given requirement. A git candidate satisfies a requirement when: + - The requirement name and candidate name match - If the requirement specifies a version, the candidate's version matches - For PackageIndexRequirement: only if no specific index is required @@ -400,9 +403,15 @@ def candidate_factory( Yields: Candidate objects that could satisfy the requirement. - Note: + .. note:: + The base implementation yields no candidates. Specialized implementations - are registered for each concrete requirement type. + are registered for each concrete requirement type through the singledispatch. + + .. note:: + + Package index registries are not yet implemented, so this currently + yields no candidates. """ yield from [] @@ -420,8 +429,7 @@ def _package_index_candidate_factory( Yields: PackageIndexCandidate objects matching the requirement. - Note: - Package index registries are not yet implemented, so this currently + .. note:: Package index registries are not yet implemented, so this currently yields no candidates. """ yield from [] diff --git a/src/fastsandpm/dependencies/provider.py b/src/fastsandpm/dependencies/provider.py index 9a1a271..eee4e5c 100644 --- a/src/fastsandpm/dependencies/provider.py +++ b/src/fastsandpm/dependencies/provider.py @@ -23,14 +23,14 @@ and dependency extraction during the resolution process. Classes: - FastSandProvider: The main dependency resolution provider. + - :py:class:`~FastSandProvider`: The main dependency resolution provider. Functions: - resolve: Convenience function to resolve dependencies for a manifest. + - :py:func:`~resolve`: Convenience function to resolve dependencies for a manifest. Type Aliases: - FastSandReqInfo: Type alias for requirement information tuples. - FastSandReporter: Type alias for the resolution reporter. + - :py:type:`~FastSandReqInfo`: FastSandReqInfo: Type alias for requirement information tuples. + - :py:type:`~FastSandReporter`: Type alias for the resolution reporter. """ from __future__ import annotations @@ -59,6 +59,9 @@ FastSandReqInfo = RequirementInformation[ConcreteRequirement, Candidate] +""".. py:type:: FastSandReqInfo: + Type alias for requirement information tuples. +""" class FastSandProvider(resolvelib.AbstractProvider[ConcreteRequirement, Candidate, str]): @@ -279,6 +282,7 @@ def narrow_requirement_selection( FastSandReporter = resolvelib.BaseReporter[ConcreteRequirement, Candidate, str] +"""Type alias for the resolution reporter.""" def resolve(manifest: Manifest) -> dict[str, Candidate]: diff --git a/src/fastsandpm/manifest.py b/src/fastsandpm/manifest.py index 4d13cb9..2ad54ed 100644 --- a/src/fastsandpm/manifest.py +++ b/src/fastsandpm/manifest.py @@ -23,18 +23,18 @@ of package metadata, dependencies, and registry configurations. Classes: - ManifestNotFoundError: Exception raised when manifest file is not found. - ManifestParseError: Exception raised when manifest parsing fails. - Package: Package metadata (name, version, description, authors). - Dependencies: Collection of package dependencies. - Manifest: The complete manifest model. + - :py:exc:`~ManifestNotFoundError`: Exception raised when manifest file is not found. + - :py:exc:`~ManifestParseError`: Exception raised when manifest parsing fails. + - :py:class:`~Package`: Package metadata (name, version, description, authors). + - :py:class:`~Dependencies`: Collection of package dependencies. + - :py:class:`~Manifest`: The complete manifest model. Functions: - get_manifest: Load and parse a manifest from a repository path. - get_manifest_from_bytes: Parse a manifest from raw bytes content. + - :py:func:`~get_manifest`: Load and parse a manifest from a repository path. + - :py:func:`~get_manifest_from_bytes`: Parse a manifest from raw bytes content. Constants: - MANIFEST_FILENAME: The default manifest filename ("proj.toml"). + - :py:const:`~MANIFEST_FILENAME`: The default manifest filename ("proj.toml"). Example: >>> from fastsandpm.manifest import get_manifest @@ -58,6 +58,7 @@ RootModel, ValidationError, WithJsonSchema, + field_validator, model_validator, ) @@ -140,6 +141,24 @@ class Package(BaseModel): readme: pathlib.Path | None = None # TODO: Field(default_factory=_find_readme) """Path to the README file relative to manifest.""" + @field_validator("name") + @classmethod + def validate_name_not_empty(cls, v: str) -> str: + """Validate that package name is not empty. + + Args: + v: The package name to validate. + + Returns: + The validated package name. + + Raises: + ValueError: If the name is empty or contains only whitespace. + """ + if not v or not v.strip(): + raise ValueError("Package name cannot be empty or whitespace-only") + return v + class Dependencies(RootModel[list[ConcreteRequirement]]): """A collection of package dependencies. @@ -157,7 +176,15 @@ class Dependencies(RootModel[list[ConcreteRequirement]]): The model validator automatically converts dictionary-style TOML dependencies into the correct dependency type based on the keys present. - Example: + Example TOML: + .. code-block:: toml + + [dependencies] + + my-lib = "1.0.0" + other_lib = {git = "https://github.com/username/repo.git", tag = "v1.0.0"} + + Example Usage: >>> deps = manifest.dependencies >>> for dep in deps: ... print(dep.name) diff --git a/src/fastsandpm/registries.py b/src/fastsandpm/registries.py index 7bf8708..c600956 100644 --- a/src/fastsandpm/registries.py +++ b/src/fastsandpm/registries.py @@ -22,14 +22,14 @@ various sources including git hosts, package indices, and local paths. Classes: - DependencyNotFoundError: Exception raised when a dependency cannot be found. - GitRegistry: Registry for resolving dependencies from git hosts. - PackageIndexRegistery: Registry for resolving dependencies from package indices. - PathRegistry: Registry for resolving dependencies from local filesystem paths. - Registries: Collection of registries used during dependency resolution. + - :py:exc:`~DependencyNotFoundError`: Exception raised when a dependency cannot be found. + - :py:class:`~GitRegistry`: Registry for resolving dependencies from git hosts. + - :py:class:`~PackageIndexRegistery`: Registry for resolving dependencies from package indices. + - :py:class:`~PathRegistry`: Registry for resolving dependencies from local filesystem paths. + - :py:class:`~Registries`: Collection of registries used during dependency resolution. Type Aliases: - ConcreteRegistry: Union type of all concrete registry types. + - :py:type:`~ConcreteRegistry`: Union type of all concrete registry types. """ from __future__ import annotations @@ -139,12 +139,12 @@ def parse_dependencies(cls, data: Any) -> Any: Returns: A list of registry dictionaries ready for model instantiation. - Examples: - Input formats supported: - - {"name": "foo", "remote": "url"} -> [{"name": "foo", ...}] - - {"foo": "url"} -> [{"name": "foo", "remote": "url"}] - - {"foo": {"remote": "url"}} -> [{"name": "foo", "remote": "url"}] - - {"foo": {"path": "./path"}} -> [{"name": "foo", "path": "./path"}] + Example Input formats supported: + + - ``{"name": "foo", "remote": "url"} -> [{"name": "foo", ...}]`` + - ``{"foo": "url"} -> [{"name": "foo", "remote": "url"}]`` + - ``{"foo": {"remote": "url"}} -> [{"name": "foo", "remote": "url"}]`` + - ``{"foo": {"path": "./path"}} -> [{"name": "foo", "path": "./path"}]`` """ if isinstance(data, dict): # Handle single dependency passed as dict with 'name' key diff --git a/src/fastsandpm/versioning/__init__.py b/src/fastsandpm/versioning/__init__.py index 4d20657..79fc529 100644 --- a/src/fastsandpm/versioning/__init__.py +++ b/src/fastsandpm/versioning/__init__.py @@ -33,19 +33,21 @@ - Comparison: ``">=1.0.0"``, ``"<2.0.0"`` - Range: ``">=1.0.0,<2.0.0"`` -Classes: - PreReleaseStage: Enum for pre-release stages (ALPHA, BETA, RELEASE_CANDIDATE). - LibraryVersion: Represents and compares semantic versions. - VersionSpecifier: Abstract base class for version specifications. - DirectVersionSpecifier: Matches a single version exactly. - CaretVersionSpecifier: Matches semver-compatible versions (^x.y.z). - ComparisonVersionSpecifier: Matches using comparison operators. - RangeVersionSpecifier: Matches versions within a range. +Included Classes: + - :py:enim`~library_version.PreReleaseStage`: Enum for pre-release stages. + - :py:class`~library_version.LibraryVersion`: Represents and compares semantic versions. + - :py:class`~specifier.VersionSpecifier`: Abstract base class for version specifications. + - :py:class`~specifier.DirectVersionSpecifier`: Matches a single version exactly. + - :py:class`~specifier.CaretVersionSpecifier`: Matches semver-compatible versions (^x.y.z). + - :py:class`~specifier.ComparisonVersionSpecifier`: Matches using comparison operators. + - :py:class`~specifier.RangeVersionSpecifier`: Matches versions within a range. -Functions: - meets_constraints: Check if a version meets specified constraints. - find_compatible_version: Find the best matching version from available options. - version_specifier_from_str: Parse a version specifier string. +Included Functions: + + - :py:func:`~specifier.meets_constraints`: Check if a version meets specified constraints. + - :py:func:`~specifier.find_compatible_version`: Find the best matching version from + a list of options. + - :py:func:`~specifier.version_specifier_from_str`: Parse a version specifier string. Example: >>> from fastsandpm.versioning import LibraryVersion, version_specifier_from_str diff --git a/src/fastsandpm/versioning/library_version.py b/src/fastsandpm/versioning/library_version.py index 4accbf5..1124fee 100644 --- a/src/fastsandpm/versioning/library_version.py +++ b/src/fastsandpm/versioning/library_version.py @@ -34,8 +34,8 @@ - rc (release-candidate): Ready for release Classes: - PreReleaseStage: Enum for pre-release stages. - LibraryVersion: Represents and compares semantic versions. + - :py:class:`~PreReleaseStage`: Enum for pre-release stages. + - :py:class:`~LibraryVersion`: Represents and compares semantic versions. Example: >>> v1 = LibraryVersion("1.2.3") @@ -103,6 +103,7 @@ class LibraryVersion: using standard comparison operators. The version format is: major.minor.patch[.pre] or major.minor.patch[-pre] + Examples: "1.0.0", "2.3.1", "1.0.0.alpha", "2.0.0-rc1" Attributes: diff --git a/src/fastsandpm/versioning/specifier.py b/src/fastsandpm/versioning/specifier.py index bf304de..7ae9bc8 100644 --- a/src/fastsandpm/versioning/specifier.py +++ b/src/fastsandpm/versioning/specifier.py @@ -29,19 +29,19 @@ - Range: ``">=1.0.0,<2.0.0"`` (multiple constraints) Classes: - VersionSpecifier: Abstract base class for all specifiers. - DirectVersionSpecifier: Matches a single exact version. - CaretVersionSpecifier: Matches semver-compatible versions. - ComparisonVersionSpecifier: Matches using comparison operators. - RangeVersionSpecifier: Matches versions within a range. + - :py:class:`~VersionSpecifier`: Abstract base class for all specifiers. + - :py:class:`~DirectVersionSpecifier`: Matches a single exact version. + - :py:class:`~CaretVersionSpecifier`: Matches semver-compatible versions. + - :py:class:`~ComparisonVersionSpecifier`: Matches using comparison operators. + - :py:class:`~RangeVersionSpecifier`: Matches versions within a range. Functions: - meets_constraints: Check if a version satisfies all constraints. - find_compatible_version: Find the latest compatible version. - version_specifier_from_str: Parse a specifier string into an object. + - :py:func:`~meets_constraints`: Check if a version satisfies all constraints. + - :py:func:`~find_compatible_version`: Find the latest compatible version. + - :py:func:`~version_specifier_from_str`: Parse a specifier string into an object. Type Aliases: - ComparisonOperator: Literal type for valid comparison operators. + - :py:type:`~ComparisonOperator`: Literal type for valid comparison operators. Example: >>> from fastsandpm.versioning.specifier import version_specifier_from_str @@ -359,7 +359,7 @@ def from_string(cls, value: str) -> Self: # Check longer operators first to avoid matching '<' when '<=' is intended for op in sorted(cls.VALID_OPERATORS, key=len, reverse=True): if value.startswith(op): - version_str = value[len(op) :].strip() + version_str = value[len(op):].strip() return cls(op, LibraryVersion(version_str)) # type: ignore[arg-type] raise ValueError(f"Invalid comparison version specifier: {value}") diff --git a/tests/manifest/test_manifest_package.py b/tests/manifest/test_manifest_package.py index f219794..ef066ab 100644 --- a/tests/manifest/test_manifest_package.py +++ b/tests/manifest/test_manifest_package.py @@ -62,13 +62,17 @@ def test_name_with_numbers(self) -> None: package = Package(name="package123", version="1.0.0", description="A test package") assert package.name == "package123" - def test_name_accepts_empty_string(self) -> None: - """Test that name currently accepts an empty string. + def test_name_rejects_empty_string(self) -> None: + """Test that name field rejects empty strings.""" + with pytest.raises(ValidationError) as exc_info: + Package(name="", version="1.0.0", description="A test package") + assert "cannot be empty" in str(exc_info.value).lower() - Note: This documents current behavior. Future validation may reject empty names. - """ - package = Package(name="", version="1.0.0", description="A test package") - assert package.name == "" + def test_name_rejects_whitespace_only(self) -> None: + """Test that name field rejects whitespace-only strings.""" + with pytest.raises(ValidationError) as exc_info: + Package(name=" ", version="1.0.0", description="A test package") + assert "cannot be empty" in str(exc_info.value).lower() class TestPackageVersion: