diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 2162617..82c92b9 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -22,7 +22,7 @@ concurrency: cancel-in-progress: false jobs: - # Single deploy job since we're just deploying + # Single deploy job since we"re just deploying deploy: environment: name: github-pages @@ -36,13 +36,13 @@ jobs: - name: Setup mdBook uses: peaceiris/actions-mdbook@v2 with: - mdbook-version: '0.4.40' + mdbook-version: "0.4.40" - name: Generate book run: mdbook build docs - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: - path: './docs/book' + path: "./docs/book" - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 2bea00f..02af6da 100644 --- a/.gitignore +++ b/.gitignore @@ -90,7 +90,7 @@ ipython_config.py # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not +# having no cross-platform support, pipenv may install dependencies that don"t work, or not # install all needed dependencies. #Pipfile.lock diff --git a/.python-version b/.python-version index 24ee5b1..04e2079 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.13 +3.12.8 diff --git a/README.md b/README.md index 5399d99..652b5b0 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ The tables below show the status for features and operating systems. **cpp-dev** is designed as a tool around the following external tools: - Package Management: Conan2 -- Build System: Ninja +- Build System: CMake, Ninja - Toolchain: LLVM-based (clang, clang-format, clang-coverage, clang-sanitizer, clang-tidy) - Test framework: gtest and gmock - Code coverage: lcov diff --git a/conan/recipes/googletest/conanfile.py b/conan/recipes/googletest/conanfile.py index bd6e4be..f763790 100644 --- a/conan/recipes/googletest/conanfile.py +++ b/conan/recipes/googletest/conanfile.py @@ -18,7 +18,7 @@ class GTestConan(ConanFile): name = "gtest" user = "official" channel = "cppdev" - description = "Google's C++ test framework" + description = "Google"s C++ test framework" license = "BSD-3-Clause" url = "https://github.com/conan-io/conan-center-index" homepage = "https://github.com/google/googletest" @@ -62,7 +62,7 @@ def _minimum_compilers_version(self): "clang": "3.3" if Version(self.version) < "1.11.0" else "5", "apple-clang": "5.0" if Version(self.version) < "1.11.0" else "9.1", }, - # Sinse 1.13.0, gtest requires C++14 and Google's Foundational C++ Support Policy + # Sinse 1.13.0, gtest requires C++14 and Google"s Foundational C++ Support Policy # https://github.com/google/oss-policies-info/blob/603a042ce2ee8f165fac46721a651d796ce59cb6/foundational-cxx-support-matrix.md "14": { "Visual Studio": "15", diff --git a/docs/src/ch01_introduction.md b/docs/src/ch01_introduction.md index 5de8aea..e02b428 100644 --- a/docs/src/ch01_introduction.md +++ b/docs/src/ch01_introduction.md @@ -11,7 +11,7 @@ The setup of a new C++ project typically involves several decisions and, consequ - Static code analysis - Sanitizers -Additionally, sharing of useful components with other developers and projects is harder compared to other languages' ecosystems due to the huge range of configuration possibilities. +Additionally, sharing of useful components with other developers and projects is harder compared to other languages" ecosystems due to the huge range of configuration possibilities. The project **cpp-dev** (short: `cpd`) is an attempt to bridge the *perceived* gap to other languages by providing a tool to accomplish the goals mentioned above. @@ -45,7 +45,7 @@ Therefore, all **cpp-dev** packages may also be used outside of **cpp-dev** as w A workflow using **cpp-dev** could look like: -* **Initialize project**: `cpd init [--std c++17] [--version ]` +* **Initialize project**: `cpd init [--std c++20] [--version ]` * **Add external dependency**: `cpd add-dep @` * **Update external dependencies**: `cpd update-dep [@]` * **Build**: `cpd build [release | debug]` diff --git a/mypy.ini b/mypy.ini index 4bb0597..f425af5 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,3 +1,3 @@ [mypy] exclude = conan -ignore_missing_imports = True +ignore_missing_imports = True \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 33e1a72..7352246 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "cpp-dev" version = "0.1.0" description = "C++ development tooing" readme = "README.md" -requires-python = ">=3.13" +requires-python = ">=3.12" dependencies = [ "conan>=2.11.0", "conan-server>=2.11.0", @@ -28,3 +28,6 @@ cpd = "cpp_dev.ui.cli:main" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" + +[tool.pytest.ini_options] +markers = ["conan_remote"] diff --git a/ruff.toml b/ruff.toml index a98c3b7..a439311 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,7 +1,7 @@ exclude = ["conan"] indent-width = 4 line-length = 120 -target-version = "py313" +target-version = "py312" [lint] ignore = [ @@ -15,6 +15,7 @@ ignore = [ "EM101", "EM102", "G004", + "ISC001", "PLR2004", "TC001", "TRY003", diff --git a/src/cpp_dev/common/os.py b/src/cpp_dev/common/os_detection.py similarity index 100% rename from src/cpp_dev/common/os.py rename to src/cpp_dev/common/os_detection.py diff --git a/src/cpp_dev/common/process.py b/src/cpp_dev/common/process.py index f82278e..75f714d 100644 --- a/src/cpp_dev/common/process.py +++ b/src/cpp_dev/common/process.py @@ -17,7 +17,7 @@ def run_command(command: str, *args: str) -> tuple[int, str, str]: This function blocks until the command has finished. """ logging.debug(f"Running command: {command} {args}") - result = subprocess.run([command, *args], check=True, capture_output=True) # noqa: S603 + result = subprocess.run([command, *args], check=False, capture_output=True) # noqa: S603 logging.debug(f"Command return code: {result.returncode}") diff --git a/src/cpp_dev/common/types.py b/src/cpp_dev/common/types.py index 0c4d1ab..338051f 100644 --- a/src/cpp_dev/common/types.py +++ b/src/cpp_dev/common/types.py @@ -3,88 +3,10 @@ # This work is licensed under the terms of the BSD-3-Clause license. # For a copy, see . - -from __future__ import annotations - -from dataclasses import dataclass from typing import Literal -from pydantic import RootModel, model_validator - ############################################################################### # Public API ### ############################################################################### -CppStandard = Literal["c++11", "c++14", "c++17", "c++20", "c++23"] - - -@dataclass -class SemanticVersionParts: - """The semantic version components.""" - - major: int - minor: int - patch: int - - def __lt__(self, other: SemanticVersionParts) -> bool: - return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch) - - -class SemanticVersion(RootModel): - """A semantic version string restricted to the .. format. - - For details on semantic versioning, see https://semver.org/. - """ - - root: str - - @staticmethod - def is_valid(raw: str) -> bool: - """Check if a string is a valid semantic version.""" - components = raw.split(".") - if len(components) != 3: - return False - try: - major, minor, patch = tuple(map(int, components)) - except ValueError: - return False - return major >= 0 and minor >= 0 and patch >= 0 - - @staticmethod - def from_parts(major: int, minor: int, patch: int) -> SemanticVersion: - """Create a semantic version from its components.""" - return SemanticVersion(root=f"{major}.{minor}.{patch}") - - @model_validator(mode="after") - def validate_version(self) -> SemanticVersion: - """Validate the semantic version string as part of pydantic.""" - if not self.is_valid(self.root): - raise ValueError( - f"Invalid semantic version string: got {self.root}, expected ..." - "Each version component must be positive.", - ) - return self - - @property - def parts(self) -> SemanticVersionParts: - """Return the components of the semantic version.""" - major, minor, patch = tuple(map(int, self.root.split("."))) - return SemanticVersionParts(major, minor, patch) - - def __eq__(self, other: object) -> bool: - """Check if two semantic versions are equal.""" - if not isinstance(other, SemanticVersion): - return NotImplemented - return self.root == other.root - - def __lt__(self, other: SemanticVersion) -> bool: - """Compare two semantic versions.""" - return self.parts < other.parts - - def __hash__(self) -> int: - """Hash the semantic version string.""" - return hash(self.root) - - def __str__(self) -> str: - """Return the semantic version string.""" - return self.root +CppStandard = Literal["c++20"] diff --git a/src/cpp_dev/common/utils.py b/src/cpp_dev/common/utils.py index 987ca02..90bf8d6 100644 --- a/src/cpp_dev/common/utils.py +++ b/src/cpp_dev/common/utils.py @@ -39,7 +39,7 @@ def ensure_dir_exists(path: Path) -> Path: def create_tmp_dir(base: Path | None = None) -> Generator[Path]: """Create a temporary directory and yields its path. - The base directory can be specified. If not provided, the system's default temporary directory is used. + The base directory can be specified. If not provided, the system"s default temporary directory is used. """ with TemporaryDirectory(dir=base) as tmp_dir: yield Path(tmp_dir) diff --git a/src/cpp_dev/common/version.py b/src/cpp_dev/common/version.py new file mode 100644 index 0000000..b1a558c --- /dev/null +++ b/src/cpp_dev/common/version.py @@ -0,0 +1,134 @@ +# Copyright (c) 2024 Andi Hellmund. All rights reserved. + +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + + +from __future__ import annotations + +from pydantic import RootModel, model_validator + +############################################################################### +# Public API ### +############################################################################### + + +class SemanticVersion(RootModel): + """A semantic version string restricted to the .. format. + + For details on semantic versioning, see https://semver.org/. + """ + + root: str + + @staticmethod + def from_parts(major: int, minor: int, patch: int) -> SemanticVersion: + """Create a semantic version from its components.""" + return SemanticVersion(root=f"{major}.{minor}.{patch}") + + @model_validator(mode="after") + def validate_version(self) -> SemanticVersion: + """Validate the semantic version string as part of pydantic.""" + components = self.root.split(".") + if len(components) < 3: + raise ValueError( + f"Invalid semantic version string: got {self.root}, expect format ..." + ) + try: + major, minor, patch = tuple(map(int, components)) + except ValueError as err: + raise ValueError( + f"Invalid semantic version string: got {self.root}, expect each part to be a number." + ) from err + if major < 0 or minor < 0 or patch < 0: + raise ValueError(f"Invalid semantic version string: got {self.root}, expect each part to be positive.") + + self._major = major + self._minor = minor + self._patch = patch + + return self + + @property + def major(self) -> int: + """Return the major version.""" + return self._major + + @property + def minor(self) -> int: + """Return the minor version.""" + return self._minor + + @property + def patch(self) -> int: + """Return the patch version.""" + return self._patch + + def __eq__(self, other: object) -> bool: + """Check if two semantic versions are equal.""" + if not isinstance(other, SemanticVersion): + return NotImplemented + return self.root == other.root + + def __lt__(self, other: object) -> bool: + """Compare two semantic versions.""" + if not isinstance(other, SemanticVersion): + return NotImplemented + return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch) + + def __hash__(self) -> int: + """Hash the semantic version string.""" + return hash(self.root) + + def __str__(self) -> str: + """Return the semantic version string.""" + return self.root + + +class SemanticVersionWithOptionalParts: + """A semantic version string with optional parts. + + Valid formats are "", ".", and "..". + """ + + @staticmethod + def from_semantic_version(version: SemanticVersion) -> SemanticVersionWithOptionalParts: + """Create a SemanticVersionWithOptionalParts from a SemanticVersion.""" + return SemanticVersionWithOptionalParts(version.major, version.minor, version.patch) + + def __init__(self, major: int, minor: int | None = None, patch: int | None = None) -> None: + if minor is None and patch is not None: + raise ValueError("Cannot specify a patch version without a minor version.") + + self._major = major + self._minor = minor + self._patch = patch + + @property + def major(self) -> int: + """Return the major version.""" + return self._major + + @property + def minor(self) -> int | None: + """Return the minor version.""" + return self._minor + + @property + def patch(self) -> int | None: + """Return the patch version.""" + return self._patch + + def __str__(self) -> str: + return ( + f"{self.major}.{self.minor}.{self.patch}" + if self.patch is not None + else f"{self.major}.{self.minor}" + if self.minor is not None + else str(self.major) + ) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, SemanticVersionWithOptionalParts): + return NotImplemented + return self.major == other.major and self.minor == other.minor and self.patch == other.patch diff --git a/src/cpp_dev/conan/command_wrapper.py b/src/cpp_dev/conan/command_wrapper.py deleted file mode 100644 index fa67354..0000000 --- a/src/cpp_dev/conan/command_wrapper.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (c) 2024 Andi Hellmund. All rights reserved. - -# This work is licensed under the terms of the BSD-3-Clause license. -# For a copy, see . - -import json -from collections.abc import Mapping -from pathlib import Path -from typing import Literal - -from pydantic import BaseModel, RootModel - -from cpp_dev.common.process import run_command, run_command_assert_success - -from .types import ConanPackageReference - -############################################################################### -# Public API ### -############################################################################### - - -def conan_config_install(conan_config_dir: Path) -> None: - """Run 'conan config install'.""" - run_command("conan", "config", "install", str(conan_config_dir)) - - -def conan_remote_login(remote: str, user: str, password: str) -> None: - """Run 'conan remote login'.""" - run_command_assert_success( - "conan", - "remote", - "login", - remote, - user, - "-p", - password, - ) - -class ConanRemoteListResult(RootModel): - root: Mapping[str, Mapping[str, dict]] - -def conan_list(remote: str, name: str) -> Mapping[ConanPackageReference, dict]: - stdout, _ = run_command_assert_success( - "conan", - "list", - "--json", - f"--remote={remote}", - f"{name}/", - ) - return json.loads(stdout)[remote] - -def conan_graph_buildorder(conanfile_path: Path, profile: str) -> list[str]: - stdout, _ = run_command_assert_success( - "conan", - "graph", - "buildorder", - str(conanfile_path), - "-pr:a", profile, - "--json", - "--order-by", "recipe", - ) \ No newline at end of file diff --git a/src/cpp_dev/conan/package.py b/src/cpp_dev/conan/package.py deleted file mode 100644 index 3ae2c24..0000000 --- a/src/cpp_dev/conan/package.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (c) 2024 Andi Hellmund. All rights reserved. - -# This work is licensed under the terms of the BSD-3-Clause license. -# For a copy, see . - -from pathlib import Path - -from cpp_dev.common.types import SemanticVersion -from cpp_dev.common.utils import create_tmp_dir -from cpp_dev.project.dependency.types import PackageDependency -from cpp_dev.tool.init import get_conan_home_dir - -from .command_wrapper import conan_list -from .setup import CONAN_REMOTE -from .types import ConanPackageReference -from .utils import conan_env, create_conanfile - -############################################################################### -# Public API ### -############################################################################### - -def get_available_versions(repository: str, name: str) -> list[SemanticVersion]: - """Retrieve available versions for a package represented by repository (aka. Conan user) and name. - - Result: - The versions get sorted in reverse order such that the latest version is first in the list. - """ - with conan_env(get_conan_home_dir()): - package_references = _retrieve_conan_package_references(repository, name) - available_versions = sorted([ref.version for ref in package_references], reverse=True) - return available_versions - - -def compute_dependency_graph(package_refs: list[PackageDependency]) -> None: - """Retrieve the dependency graph for the given package dependencies.""" - with conan_env(get_conan_home_dir()): - with create_tmp_dir() as tmp_dir: - conanfile_path = create_conanfile(tmp_dir, package_refs) - - - - -############################################################################### -# Implementation ### -############################################################################### - -def _retrieve_conan_package_references(repository: str, name: str) -> list[ConanPackageReference]: - package_data = conan_list(CONAN_REMOTE, name) - package_references = [ - ref - for ref in package_data.keys() - if ref.user == repository - ] - return package_references - - - diff --git a/src/cpp_dev/conan/__init__.py b/src/cpp_dev/dependency/__init__.py similarity index 100% rename from src/cpp_dev/conan/__init__.py rename to src/cpp_dev/dependency/__init__.py diff --git a/src/cpp_dev/project/dependency/__init__.py b/src/cpp_dev/dependency/conan/__init__.py similarity index 100% rename from src/cpp_dev/project/dependency/__init__.py rename to src/cpp_dev/dependency/conan/__init__.py diff --git a/src/cpp_dev/dependency/conan/command_wrapper.py b/src/cpp_dev/dependency/conan/command_wrapper.py new file mode 100644 index 0000000..0925589 --- /dev/null +++ b/src/cpp_dev/dependency/conan/command_wrapper.py @@ -0,0 +1,169 @@ +# Copyright (c) 2024 Andi Hellmund. All rights reserved. + +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + +import re +from collections.abc import Mapping +from pathlib import Path +from typing import Literal + +from pydantic import BaseModel, RootModel + +from cpp_dev.common.process import run_command, run_command_assert_success + +from .types import ConanPackageReferenceWithSemanticVersion + +############################################################################### +# Public API ### +############################################################################### + +class ConanCommandException(Exception): + """Exception for raising issues during Conan command execution.""" + def __init__(self, command: str, msg: str) -> None: + self._command = command + self._msg = msg + super().__init__(f"{self._command} failed: {self._msg}") + + +ConanSettingName = Literal["compiler", "compiler.cppstd"] +ConanSettings = dict[ConanSettingName, object] + + +############################ +### Conan Config Install ### +############################ +def conan_config_install(conan_config_dir: Path) -> None: + """Run "conan config install".""" + run_command("conan", "config", "install", str(conan_config_dir)) + + +########################## +### Conan Remote Login ### +########################## +def conan_remote_login(remote: str, user: str, password: str) -> None: + """Run "conan remote login".""" + run_command_assert_success( + "conan", + "remote", + "login", + remote, + user, + "-p", + password, + ) + +### Conan List +class ConanListResult(RootModel): + root: Mapping[str, Mapping[ConanPackageReferenceWithSemanticVersion, dict]] + +def conan_list(remote: str, name: str) -> Mapping[ConanPackageReferenceWithSemanticVersion, dict]: + stdout, _ = run_command_assert_success( + "conan", + "list", + "-f", "json", + f"--remote={remote}", + f"{name}/", + ) + parsed_data = ConanListResult.model_validate_json(stdout) + return parsed_data.root[remote] + + +############################### +### Conan Graph Build-Order ### +############################### +class ConanPackageInfo(BaseModel): + settings: ConanSettings | None = None + +class ConanPackageAttributes(BaseModel): + info: ConanPackageInfo + +class ConanRecipeAttributes(BaseModel): + ref: str + depends: list[str] + packages: list[list[ConanPackageAttributes]] + +class ConanGraphBuildOrder(BaseModel): + order: list[list[ConanRecipeAttributes]] + + +COMMAND_GRAPH_BUILDORDER = "graph-buildorder" + +def _handle_package_resolution_error(stderr: str) -> None: + regex_unable_to_find = re.compile(r"Unable to find '([^']+)'") + match = regex_unable_to_find.search(stderr) + if match: + raise ConanCommandException( + command=COMMAND_GRAPH_BUILDORDER, + msg=f"unable to find package '{match.group(1)}'", + ) + +def _handle_package_version_conflict(stderr: str) -> None: + regex_version_conflict = re.compile(r"Version conflict: Conflict between ([^ ]+) and ([^ ]+) in the graph") + match = regex_version_conflict.search(stderr) + if match: + raise ConanCommandException( + command=COMMAND_GRAPH_BUILDORDER, + msg=f"version conflict between '{match.group(1)}' and '{match.group(2)}'", + ) + +def _handle_graph_buildorder_error(stderr: str) -> None: + _handle_package_resolution_error(stderr) + _handle_package_version_conflict(stderr) + + raise ConanCommandException( + command=COMMAND_GRAPH_BUILDORDER, + msg="generic error", + ) + +def conan_graph_buildorder(conanfile_path: Path, profile: str, settings: ConanSettings) -> ConanGraphBuildOrder: + """Run "conan graph buildorder".""" + command = [ + "conan", + "graph", + "build-order", + str(conanfile_path), + "-pr:a", profile, + "-f", "json", + "--order-by", "recipe", + ] + for key, value in settings.items(): + command.extend(["-s:a", f"{key}={value}"]) + rc, stdout, stderr = run_command( + *command + ) + if rc != 0: + _handle_graph_buildorder_error(stderr) + + return ConanGraphBuildOrder.model_validate_json(stdout) + + +#################### +### Conan Create ### +#################### + +def conan_create(package_dir: Path, profile: str, settings: ConanSettings) -> None: + """Run "conan create".""" + command = [ + "conan", + "create", + str(package_dir), + "-pr:a", profile, + ] + for key, value in settings.items(): + command.extend(["-s:a", f"{key}={value}"]) + run_command_assert_success( + *command + ) + +#################### +### Conan Upload ### +#################### +def conan_upload(ref: ConanPackageReferenceWithSemanticVersion, remote: str) -> None: + """Run "conan upload".""" + run_command_assert_success( + "conan", + "upload", + "-r", remote, + str(ref), + ) \ No newline at end of file diff --git a/src/cpp_dev/dependency/conan/config/profiles/ubuntu-24.04-x86_64 b/src/cpp_dev/dependency/conan/config/profiles/ubuntu-24.04-x86_64 new file mode 100644 index 0000000..1d7254a --- /dev/null +++ b/src/cpp_dev/dependency/conan/config/profiles/ubuntu-24.04-x86_64 @@ -0,0 +1,5 @@ +[settings] +arch=x86_64 +build_type=Release +os=Linux +os.distro=Ubuntu-24.04 diff --git a/src/cpp_dev/conan/config/remotes.json b/src/cpp_dev/dependency/conan/config/remotes.json similarity index 100% rename from src/cpp_dev/conan/config/remotes.json rename to src/cpp_dev/dependency/conan/config/remotes.json diff --git a/src/cpp_dev/conan/config/settings.yml b/src/cpp_dev/dependency/conan/config/settings.yml similarity index 78% rename from src/cpp_dev/conan/config/settings.yml rename to src/cpp_dev/dependency/conan/config/settings.yml index 66b39ad..4fcc89b 100644 --- a/src/cpp_dev/conan/config/settings.yml +++ b/src/cpp_dev/dependency/conan/config/settings.yml @@ -5,10 +5,10 @@ arch: [x86_64] compiler: gcc: version: ["13"] - cppstd: [None, 11, 14, 17, 20, 23] + cppstd: [None, 20] libcxx: [libstdc++, libstdc++11] clang: version: ["19"] libcxx: [None, libstdc++, libstdc++11, libc++, c++_shared, c++_static] - cppstd: [None, 11, 14, 17, 20, 23] + cppstd: [None, 20] build_type: [None, Debug, Release, RelWithDebInfo, MinSizeRel] \ No newline at end of file diff --git a/src/cpp_dev/dependency/conan/provider.py b/src/cpp_dev/dependency/conan/provider.py new file mode 100644 index 0000000..53f8852 --- /dev/null +++ b/src/cpp_dev/dependency/conan/provider.py @@ -0,0 +1,81 @@ +# Copyright (c) 2024 Andi Hellmund. All rights reserved. + +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from itertools import chain +from pathlib import Path + +from cpp_dev.common.types import CppStandard +from cpp_dev.common.utils import create_tmp_dir +from cpp_dev.common.version import SemanticVersion +from cpp_dev.dependency.conan.command_wrapper import (ConanRecipeAttributes, + ConanSettings, + conan_graph_buildorder, + conan_list) +from cpp_dev.dependency.conan.setup import CONAN_REMOTE +from cpp_dev.dependency.conan.types import \ + ConanPackageReferenceWithSemanticVersion +from cpp_dev.dependency.conan.utils import conan_env, create_conanfile +from cpp_dev.dependency.provider import (DependencyIdentifier, + DependencyProvider) +from cpp_dev.dependency.specifier import DependencySpecifier +from cpp_dev.dependency.types import DependencySpecifierParts + +############################################################################### +# Public API ### +############################################################################### + +class ConanDependencyProvider(DependencyProvider): + + def __init__(self, conan_home_dir: Path, profile: str, settings: dict[ConanSetting, object] | None = None) -> None: + self._conan_home_dir = conan_home_dir + self._profile = profile + self._settings = settings + + def fetch_versions(self, repository: str, name: str) -> list[SemanticVersion]: + with conan_env(self._conan_home_dir): + package_references = _retrieve_conan_package_references(repository, name) + available_versions = sorted([ref.version for ref in package_references], reverse=True) + return available_versions + + def collect_dependency_hull(self, deps: list[DependencySpecifier]) -> list[DependencyIdentifier]: + with conan_env(self._conan_home_dir): + with create_tmp_dir() as tmp_dir: + conanfile_path = create_conanfile(tmp_dir, deps) + conan_settings = self._settings if self._settings else {} + build_order = conan_graph_buildorder(conanfile_path, self._profile, conan_settings) + return _construct_depenencies(build_order.order) + + + def install_dependencies(self, deps: list[DependencySpecifier]) -> list[DependencySpecifier]: + ... # Implementation using Conan package manager + + +############################################################################### +# Implementation ### +############################################################################### + +def _retrieve_conan_package_references(repository: str, name: str) -> list[ConanPackageReferenceWithSemanticVersion]: + package_data = conan_list(CONAN_REMOTE, name) + package_references = [ + ref + for ref in package_data.keys() + if ref.user == repository + ] + return package_references + +def _construct_depenencies(build_order: list[list[ConanRecipeAttributes]]) -> set[DependencyIdentifier]: + all_packages = [element for sublist in build_order for element in sublist] + dependencies = set() + for attributes in all_packages: + ref = ConanPackageReferenceWithSemanticVersion.from_raw_string_with_revision(attributes.ref) + dependencies.add( + DependencyIdentifier(repository=ref.user, name=ref.name, version=ref.version) + ) + return dependencies + \ No newline at end of file diff --git a/src/cpp_dev/conan/setup.py b/src/cpp_dev/dependency/conan/setup.py similarity index 93% rename from src/cpp_dev/conan/setup.py rename to src/cpp_dev/dependency/conan/setup.py index 40ee5ca..b03de14 100644 --- a/src/cpp_dev/conan/setup.py +++ b/src/cpp_dev/dependency/conan/setup.py @@ -30,10 +30,8 @@ def get_conan_config_source_dir() -> Path: return Path(__file__).parent / "config" - -def initialize_conan(conan_home: Path) -> None: +def initialize_conan(conan_home: Path, conan_config_dir: Path) -> None: """Initialize Conan to use the given home directory.""" with conan_env(conan_home): - conan_config_dir = get_conan_config_source_dir() conan_config_install(conan_config_dir) conan_remote_login(CONAN_REMOTE, DEFAULT_CONAN_USER, DEFAULT_CONAN_USER_PWD) diff --git a/src/cpp_dev/conan/types.py b/src/cpp_dev/dependency/conan/types.py similarity index 69% rename from src/cpp_dev/conan/types.py rename to src/cpp_dev/dependency/conan/types.py index 3ef5a7e..b958235 100644 --- a/src/cpp_dev/conan/types.py +++ b/src/cpp_dev/dependency/conan/types.py @@ -9,20 +9,33 @@ from pydantic import RootModel, model_validator -from cpp_dev.common.types import SemanticVersion +from cpp_dev.common.version import SemanticVersion ############################################################################### # Public API ### ############################################################################### + +class ConanPackageReferenceWithVersionRanges(str): + """A generic Conan package reference supporting version ranges. + + This package reference has the format: name/[version_ranges]@user/channel. + """ + -class ConanPackageReference(RootModel): +class ConanPackageReferenceWithSemanticVersion(RootModel): """A Conan package reference in the format name/version@user/channel.""" root: str + @staticmethod + def from_raw_string_with_revision(raw_string: str) -> ConanPackageReferenceWithSemanticVersion: + REVISION_MARKER = "#" + raw_string, revision = raw_string.rsplit(REVISION_MARKER, 1) + return ConanPackageReferenceWithSemanticVersion(raw_string) + @model_validator(mode="after") - def validate_reference(self) -> ConanPackageReference: + def validate_reference(self) -> ConanPackageReferenceWithSemanticVersion: CONAN_REFERENCE_PATTERN = r"(?P[a-zA-Z0-9_]+)/(?P\d+\.\d+\.\d+)@(?P[a-zA-Z0-9_]+)/(?P[a-zA-Z0-9_]+)" match = re.match(CONAN_REFERENCE_PATTERN, self.root) if not match: @@ -33,6 +46,8 @@ def validate_reference(self) -> ConanPackageReference: self._user = match.group("user") self._channel = match.group("channel") + return self + @property def name(self) -> str: return self._name @@ -53,4 +68,5 @@ def __hash__(self) -> int: return hash(self.root) def __str__(self) -> str: - return f"{self._name}/{self._version}@{self._user}/{self._channel}" \ No newline at end of file + return f"{self._name}/{self._version}@{self._user}/{self._channel}" + \ No newline at end of file diff --git a/src/cpp_dev/conan/utils.py b/src/cpp_dev/dependency/conan/utils.py similarity index 57% rename from src/cpp_dev/conan/utils.py rename to src/cpp_dev/dependency/conan/utils.py index e577146..0cb38fc 100644 --- a/src/cpp_dev/conan/utils.py +++ b/src/cpp_dev/dependency/conan/utils.py @@ -7,9 +7,12 @@ from contextlib import contextmanager from pathlib import Path -from cpp_dev.common.types import SemanticVersion from cpp_dev.common.utils import updated_env -from cpp_dev.project.dependency.types import PackageDependency +from cpp_dev.common.version import SemanticVersion +from cpp_dev.dependency.conan.types import ( + ConanPackageReferenceWithSemanticVersion, + ConanPackageReferenceWithVersionRanges) +from cpp_dev.dependency.specifier import DependencySpecifier ############################################################################### # Public API ### @@ -26,7 +29,7 @@ def conan_env(conan_home: Path) -> Generator[None]: yield -def create_conanfile(tmp_dir: Path, package_refs: list[PackageDependency]) -> Path: +def create_conanfile(tmp_dir: Path, package_refs: list[DependencySpecifier]) -> Path: """Create a conanfile.txt with the given package dependencies.""" conanfile_path = tmp_dir / "conanfile.txt" content = "[requires]\n" @@ -35,15 +38,21 @@ def create_conanfile(tmp_dir: Path, package_refs: list[PackageDependency]) -> Pa conanfile_path.write_text(content) return conanfile_path -def compose_conan_package_reference(ref: PackageDependency) -> str: +def compose_conan_package_reference(ref: DependencySpecifier) -> ConanPackageReferenceWithVersionRanges: """Compose a Conan package reference from a package dependency.""" - return f"{ref.parts.name}/{_get_conan_package_version(ref)}@{ref.parts.repository}/{DEFAULT_CONAN_CHANNEL}" + return ConanPackageReferenceWithVersionRanges(f"{ref.name}/{_get_conan_package_version(ref)}@{ref.repository}/{DEFAULT_CONAN_CHANNEL}") -def _get_conan_package_version(ref: PackageDependency) -> str: - if isinstance(ref.parts.version_spec, SemanticVersion): - return str(ref.parts.version_spec) - elif isinstance(ref.parts.version_spec, list): - bound_str = " ".join([f"{bound.operand.value}{bound.version}" for bound in ref.parts.version_spec]) + +############################################################################### +# Implementation ### +############################################################################### + +def _get_conan_package_version(ref: DependencySpecifier) -> str: + if isinstance(ref.version_spec, SemanticVersion): + return str(ref.version_spec) + elif isinstance(ref.version_spec, list): + bound_str = " ".join([f"{bound.operand.value}{bound.version}" for bound in ref.version_spec]) return f"[{bound_str}]" else: - raise ValueError("Unsupported version specification: latest") \ No newline at end of file + raise ValueError("Unsupported version specification: latest") + diff --git a/src/cpp_dev/dependency/provider.py b/src/cpp_dev/dependency/provider.py new file mode 100644 index 0000000..91259dc --- /dev/null +++ b/src/cpp_dev/dependency/provider.py @@ -0,0 +1,102 @@ +# Copyright (c) 2024 Andi Hellmund. All rights reserved. + +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass + +from cpp_dev.common.version import SemanticVersion + +from .specifier import DependencySpecifier + +############################################################################### +# Public API ### +############################################################################### + + +class DependencyError(Exception): + """Exception for raising issues during dependency resolution or installation.""" + + +@dataclass +class DependencyIdentifier: + """Attributes of a dependency.""" + + repository: str + name: str + version: SemanticVersion + + @staticmethod + def from_str(id_str: str) -> DependencyIdentifier: + """Create a dependency identifier from a dependency string. + + Args: + id_str (str): The dependency string in the format "//". + + """ + parts = id_str.split("/") + if len(parts) < 3: + raise ValueError(f"Invalid dependency id string: {id_str}") + return DependencyIdentifier(parts[0], parts[1], SemanticVersion(parts[2])) + + def __hash__(self) -> int: + return hash((self.repository, self.name, self.version)) + + def __str__(self) -> str: + return f"{self.repository}/{self.name}/{self.version}" + + +class DependencyProvider(ABC): + """Abstract base class for dependency providers. + + A dependency provider is responsible for resolving, downloading and installing dependencies. + A dependency is a software package containing libraries, headers and executables. + Each dependency is identified by a dependency string + """ + + @abstractmethod + def fetch_versions(self, repository: str, name: str) -> list[SemanticVersion]: + """Fetch available versions for a dependency represented by repository and name. + + Args: + repository (str): The repository of the dependency. + name (str): The name of the dependency. + + Result: + The list of available versions sorted in reverse order such that the latest version is first. + + """ + + @abstractmethod + def collect_dependency_hull(self, deps: list[DependencySpecifier]) -> set[DependencyIdentifier]: + """Collect the dependency hull for a list of dependencies. + + Args: + deps (list[DependencySpecifier]): The list of dependencies to collect the dependency hull for. + + Return: + The list of dependencies that form the dependency hull (i.e. including transitive dependencies) + for the input list of dependencies. + + Raise: + DependencyError: If an error occurs during dependency resolution. + + """ + + @abstractmethod + def install_dependencies(self, deps: list[DependencySpecifier]) -> list[DependencySpecifier]: + """Install the dependencies represented by the input list. + + Args: + deps (list[DependencySpecifier]): The list of dependencies to install. + + Return: + The list of successfully installed dependencies (including transitive dependencies). + + Raise: + DependencyError: If an error occurs during dependency rinstallation. + + """ diff --git a/src/cpp_dev/project/dependency/types.py b/src/cpp_dev/dependency/specifier.py similarity index 62% rename from src/cpp_dev/project/dependency/types.py rename to src/cpp_dev/dependency/specifier.py index 3d0d40f..713124a 100644 --- a/src/cpp_dev/project/dependency/types.py +++ b/src/cpp_dev/dependency/specifier.py @@ -3,35 +3,36 @@ # This work is licensed under the terms of the BSD-3-Clause license. # For a copy, see . + from __future__ import annotations from pydantic import RootModel, model_validator -from cpp_dev.common.types import SemanticVersion +from cpp_dev.common.version import SemanticVersion -from .parser import DependencyParserError, parse_dependency_string -from .parts import PackageDependencyParts +from .specifier_parser import DependencyParserError, parse_dependency_string +from .types import DependencySpecifierParts, VersionSpecType ############################################################################### # Public API ### ############################################################################### -class PackageDependency(RootModel): - """A package dependency string. +class DependencySpecifier(RootModel): + """A package dependency specifier. - Each package dependency is a string in the format '/[]'. + Each package dependency is a string in the format "/[]". The parameter is the user or organization that owns the dependency. - The default value for is 'official'. + The default value for is "official". The of the dependency is mandatory. - The supports an exact version, lower/upper bounds, intervals or 'latest'. + The supports an exact version, lower/upper bounds, intervals or "latest". The default value for is latest. - The exact version is specified as '..', while lower/upper bounds and intervals - use the format '< | <= | > | >= [.[.]]' with minor and parts parts being optional. + The exact version is specified as "..", while lower/upper bounds and intervals + use the format "< | <= | > | >= [.[.]]" with minor and parts parts being optional. """ @staticmethod - def from_parts(parts: PackageDependencyParts) -> PackageDependency: + def from_parts(parts: DependencySpecifierParts) -> DependencySpecifier: """Create a package dependency from its parts.""" repository_str = f"{parts.repository}/" if parts.repository is not None else "" version_spec = "latest" @@ -40,12 +41,12 @@ def from_parts(parts: PackageDependencyParts) -> PackageDependency: elif isinstance(parts.version_spec, list): bounds_str = [f"{bound.operand.value}{bound.version}" for bound in parts.version_spec] version_spec = ",".join(bounds_str) - return PackageDependency(f"{repository_str}{parts.name}[{version_spec}]") + return DependencySpecifier(f"{repository_str}{parts.name}[{version_spec}]") root: str @model_validator(mode="after") - def validate_version(self) -> PackageDependency: + def validate_version(self) -> DependencySpecifier: """Validate the package dependency str as part of pydantic.""" try: self._parts = parse_dependency_string(self.root) @@ -56,19 +57,23 @@ def validate_version(self) -> PackageDependency: return self @property - def parts(self) -> PackageDependencyParts: - """Return the parts of the package dependency. + def repository(self) -> str | None: + """Return the repository of the package dependency.""" + return self._parts.repository + + @property + def name(self) -> str: + """Return the name of the package dependency.""" + return self._parts.name - The parts contain: - o Repository - o Name - o Version Specs - """ - return self._parts + @property + def version_spec(self) -> VersionSpecType: + """Return the version spec of the package dependency.""" + return self._parts.version_spec def __eq__(self, other: object) -> bool: """Check if two semantic versions are equal.""" - if not isinstance(other, PackageDependency): + if not isinstance(other, DependencySpecifier): return NotImplemented return self.root == other.root diff --git a/src/cpp_dev/project/dependency/parser.py b/src/cpp_dev/dependency/specifier_parser.py similarity index 96% rename from src/cpp_dev/project/dependency/parser.py rename to src/cpp_dev/dependency/specifier_parser.py index ee56bcc..0de1e51 100644 --- a/src/cpp_dev/project/dependency/parser.py +++ b/src/cpp_dev/dependency/specifier_parser.py @@ -9,12 +9,11 @@ from dataclasses import dataclass from enum import Enum -from cpp_dev.common.types import SemanticVersion from cpp_dev.common.utils import assert_is_not_none +from cpp_dev.common.version import SemanticVersion, SemanticVersionWithOptionalParts -from .parts import ( - PackageDependencyParts, - SemanticVersionWithOptionalParts, +from .types import ( + DependencySpecifierParts, VersionSpecBound, VersionSpecBoundOperand, VersionSpecType, @@ -30,7 +29,7 @@ class DependencyParserError(Exception): """Exception for raising issues during dependency parsing.""" -def parse_dependency_string(dep_str: str) -> PackageDependencyParts: +def parse_dependency_string(dep_str: str) -> DependencySpecifierParts: """Parse a package dependency string into its components. It raises a DependencyParserError in case of an invalid format or syntax error. @@ -156,7 +155,7 @@ def _consume(self) -> None: self._pos += 1 -def _parse_spec(tokens: _TokenProvider) -> PackageDependencyParts: +def _parse_spec(tokens: _TokenProvider) -> DependencySpecifierParts: """Parse the package dependency. Grammar rule: @@ -165,7 +164,7 @@ def _parse_spec(tokens: _TokenProvider) -> PackageDependencyParts: repository, name = _parse_repository_and_name(tokens) version_spec = _parse_version_spec(tokens) tokens.assert_eof() - return PackageDependencyParts(repository, name, version_spec) + return DependencySpecifierParts(repository, name, version_spec) def _parse_repository_and_name(tokens: _TokenProvider) -> tuple[str | None, str]: diff --git a/src/cpp_dev/project/dependency/parts.py b/src/cpp_dev/dependency/types.py similarity index 55% rename from src/cpp_dev/project/dependency/parts.py rename to src/cpp_dev/dependency/types.py index 145d3b1..0ef91dd 100644 --- a/src/cpp_dev/project/dependency/parts.py +++ b/src/cpp_dev/dependency/types.py @@ -9,48 +9,13 @@ from enum import Enum from typing import Literal -from cpp_dev.common.types import SemanticVersion +from cpp_dev.common.version import SemanticVersion, SemanticVersionWithOptionalParts ############################################################################### # Public API ### ############################################################################### -class SemanticVersionWithOptionalParts: - """A semantic version string with optional parts. - - Valid formats are '', '.', and '..'. - """ - - @staticmethod - def from_semantic_version(version: SemanticVersion) -> SemanticVersionWithOptionalParts: - """Create a SemanticVersionWithOptionalParts from a SemanticVersion.""" - parts = version.parts - return SemanticVersionWithOptionalParts(parts.major, parts.minor, parts.patch) - - def __init__(self, major: int, minor: int | None = None, patch: int | None = None) -> None: - if minor is None and patch is not None: - raise ValueError("Cannot specify a patch version without a minor version.") - - self.major = major - self.minor = minor - self.patch = patch - - def __str__(self) -> str: - return ( - f"{self.major}.{self.minor}.{self.patch}" - if self.patch is not None - else f"{self.major}.{self.minor}" - if self.minor is not None - else str(self.major) - ) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, SemanticVersionWithOptionalParts): - return NotImplemented - return self.major == other.major and self.minor == other.minor and self.patch == other.patch - - class VersionSpecBoundOperand(Enum): """An enumeration of version spec bound operands.""" @@ -90,7 +55,7 @@ def __eq__(self, other: object) -> bool: @dataclass -class PackageDependencyParts: +class DependencySpecifierParts: """The result of parsing a package dependency string.""" repository: str | None diff --git a/src/cpp_dev/project/__init__.py b/src/cpp_dev/project/__init__.py index e69de29..2642f50 100644 --- a/src/cpp_dev/project/__init__.py +++ b/src/cpp_dev/project/__init__.py @@ -0,0 +1,8 @@ +# Copyright (c) 2024 Andi Hellmund. All rights reserved. + +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + +from .core import Project, setup_project + +__all__ = ["Project", "setup_project"] diff --git a/src/cpp_dev/project/config.py b/src/cpp_dev/project/config.py index 0405c8f..1d78651 100644 --- a/src/cpp_dev/project/config.py +++ b/src/cpp_dev/project/config.py @@ -6,17 +6,54 @@ from copy import deepcopy from pathlib import Path +from typing import Literal import yaml +from pydantic import BaseModel -from .constants import compose_project_config_file -from .dependency.types import PackageDependency -from .types import DependencyType, ProjectConfig +from cpp_dev.common.types import CppStandard +from cpp_dev.common.version import SemanticVersion +from cpp_dev.dependency.specifier import DependencySpecifier + +from .path_composition import compose_project_config_file ############################################################################### # Public API ### ############################################################################### +DependencyType = Literal["runtime", "dev", "cpd"] + + +class ProjectConfig(BaseModel): + """A project configuration for a cpp-dev project.""" + + name: str + version: SemanticVersion + std: CppStandard + + author: str | None + license: str | None + description: str | None + + # Public dependencies used by the project + dependencies: list[DependencySpecifier] + + # Development dependencies used by the project while developing + dev_dependencies: list[DependencySpecifier] + + # Cpp-Dev dependencies used by the tool itself. These dependencies can only be updated but not removed. + cpd_dependencies: list[DependencySpecifier] + + def get_dependencies(self, dep_type: DependencyType) -> list[DependencySpecifier]: + """Return the dependency list by type.""" + if dep_type == "runtime": + return self.dependencies + if dep_type == "dev": + return self.dev_dependencies + if dep_type == "cpd": + return self.cpd_dependencies + raise ValueError(f"Invalid dependency type requested: {dep_type}") + def create_project_config( project_dir: Path, @@ -43,7 +80,7 @@ def store_project_config(project_dir: Path, config: ProjectConfig) -> None: def update_dependencies( project_config: ProjectConfig, - deps: list[PackageDependency], + deps: list[DependencySpecifier], dep_type: DependencyType, ) -> ProjectConfig: """Update the dependency in the project configuration.""" @@ -52,20 +89,33 @@ def update_dependencies( return updated_config +def validate_dependencies(project_config: ProjectConfig) -> None: + """Check that a dependency identified by its name (without repository) exists only once. + + Dependencies are in general identified by a name and their repository, e.g. official/cpd. + It is, however, possible to add a dependency with the same name from different repositories, e.g. + official/cpd and user/cpd. In this case, it is assumed that both dependencies are the same which + is not supported in C++ because it might lead to undefined behavior. + """ + seen_names = set() + for dep in project_config.dependencies + project_config.dev_dependencies + project_config.cpd_dependencies: + if dep.name in seen_names: + raise ValueError(f"Dependency '{dep.name}' is defined multiple times.") + seen_names.add(dep.name) + + ############################################################################### # Implementation ### ############################################################################### def _update_or_add_dependency_entries( - existing_deps: list[PackageDependency], - new_deps: list[PackageDependency], + existing_deps: list[DependencySpecifier], + new_deps: list[DependencySpecifier], ) -> None: - repo_and_name_to_index_mapping = { - (entry.parts.repository, entry.parts.name): idx for idx, entry in enumerate(existing_deps) - } + repo_and_name_to_index_mapping = {(entry.repository, entry.name): idx for idx, entry in enumerate(existing_deps)} for dep in new_deps: - key = (dep.parts.repository, dep.parts.name) + key = (dep.repository, dep.name) if key in repo_and_name_to_index_mapping: idx = repo_and_name_to_index_mapping[key] existing_deps[idx] = dep diff --git a/src/cpp_dev/project/core.py b/src/cpp_dev/project/core.py new file mode 100644 index 0000000..4e72989 --- /dev/null +++ b/src/cpp_dev/project/core.py @@ -0,0 +1,200 @@ +# Copyright (c) 2024 Andi Hellmund. All rights reserved. + +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + +from pathlib import Path +from textwrap import dedent + +from cpp_dev.common.version import SemanticVersionWithOptionalParts +from cpp_dev.dependency.provider import DependencyIdentifier, DependencyProvider +from cpp_dev.dependency.specifier import DependencySpecifier +from cpp_dev.dependency.types import DependencySpecifierParts, VersionSpecBound, VersionSpecBoundOperand + +from .config import ( + DependencyType, + ProjectConfig, + create_project_config, + load_project_config, + store_project_config, + update_dependencies, + validate_dependencies, +) +from .lockfile import create_initial_lock_file +from .path_composition import compose_include_file, compose_source_file + +############################################################################### +# Public API ### +############################################################################### + + +class Project: + """Main interface for managing cpp-dev projects.""" + + def __init__(self, project_dir: Path, dependency_provider: DependencyProvider) -> None: + self._project_dir = project_dir + self._dependency_provider = dependency_provider + + @property + def project_dir(self) -> Path: + """Return the path to the project directory.""" + return self._project_dir + + def add_package_dependency(self, deps: list[DependencySpecifier], dep_type: DependencyType) -> None: + """Add package dependencies to the project for the given type.""" + refined_deps = _refine_package_dependencies(self._dependency_provider, deps) + project_config = load_project_config(self.project_dir) + update_dependencies(project_config, refined_deps, dep_type) + validate_dependencies(project_config) + dependency_hull = _obtain_dependency_hull(project_config, self._dependency_provider) + _write_lock_file(self.project_dir, dependency_hull) + store_project_config(self.project_dir, project_config) + + +def setup_project( + project_config: ProjectConfig, + dependency_provider: DependencyProvider, + parent_dir: Path | None = None, +) -> Project: + """Create a new cpp-dev project in the specified parent directory. + + The path to the new project directory is returned. + """ + project_dir = _validate_project_dir(parent_dir, project_config.name) + create_project_config(project_dir, project_config) + create_initial_lock_file(project_dir) + _create_project_files(project_dir, project_config.name) + + project = Project(project_dir, dependency_provider) + _add_default_cpd_dependencies(project) + + return project + + +############################################################################### +# Implementation ### +############################################################################### + + +def _validate_project_dir(parent_dir: Path | None, name: str) -> Path: + """Check and validate if the project directory does not yet exist.""" + if parent_dir is None: + parent_dir = Path.cwd() + project_dir = parent_dir / name + if project_dir.exists(): + raise ValueError(f"Project directory {project_dir} already exists.") + project_dir.mkdir(parents=True) + return project_dir + + +def _create_project_files(project_dir: Path, name: str) -> None: + """Create the necessary project files for the cpp-dev package.""" + _create_library_include_file(project_dir, name) + _create_library_source_file(project_dir, name) + _create_library_test_file(project_dir, name) + + +def _create_library_include_file(project_dir: Path, name: str) -> None: + include_file = compose_include_file(project_dir, name, f"{name}.hpp") + include_file.parent.mkdir(parents=True) + include_file.write_text( + dedent( + f"""\ + #pragma once + + namespace {name} {{ + int api(); + }} + """, + ), + ) + + +def _create_library_source_file(project_dir: Path, name: str) -> None: + source_file = compose_source_file(project_dir, f"{name}.cpp") + source_file.parent.mkdir(parents=True) + source_file.write_text( + dedent( + f"""\ + #include "{name}/{name}.hpp" + + int {name}::api() {{ + return 42; + }} + """, + ), + ) + + +def _create_library_test_file(project_dir: Path, name: str) -> None: + test_file = compose_source_file(project_dir, f"{name}.test.cpp") + test_file.write_text( + dedent( + f"""\ + #include + #include "{name}/{name}.hpp" + + TEST({name}, api) {{ + EXPECT_EQ({name}::api(), 42); + }} + """, + ), + ) + + +def _add_default_cpd_dependencies(project: Project) -> None: + project.add_package_dependency( + [ + DependencySpecifier("llvm"), + DependencySpecifier("gtest"), + ], + "cpd", + ) + + +DEFAULT_REPOSITORY = "official" + + +def _refine_package_dependencies( + dep_provider: DependencyProvider, deps: list[DependencySpecifier] +) -> list[DependencySpecifier]: + """Refine the package dependencies in case of defaults were chosen. + + The refinement includes (in order): + o Default repository "official" + o Latest resolved version in case of "latest" + + This step is performed to assure that package dependencies with "latest" do not get an older version + than the latest one at the time of resolution. This is important in case a versions gets removed. + """ + updated_deps = [] + for dep in deps: + repository = dep.repository if dep.repository is not None else DEFAULT_REPOSITORY + version_spec = dep.version_spec + if dep.version_spec == "latest": + available_versions = dep_provider.fetch_versions(repository, dep.name) + if len(available_versions) == 0: + raise ValueError(f"No available versions for package {dep.name} at repository {dep.repository}.") + version_spec = [ + VersionSpecBound( + operand=VersionSpecBoundOperand.GREATER_THAN_OR_EQUAL, + version=SemanticVersionWithOptionalParts.from_semantic_version(available_versions[0]), + ) + ] + updated_deps.append( + DependencySpecifier.from_parts(DependencySpecifierParts(repository, dep.name, version_spec)) + ) + return updated_deps + + +def _obtain_dependency_hull( + project_config: ProjectConfig, dep_provider: DependencyProvider +) -> set[DependencyIdentifier]: + """Obtain the dependency hull for the given project configuration.""" + return dep_provider.collect_dependency_hull( + project_config.dependencies + project_config.dev_dependencies + project_config.cpd_dependencies + ) + + +def _write_lock_file(_project_dir: Path, _dependency_hull: set[DependencyIdentifier]) -> None: + """Write the lock file for the given dependency hull.""" diff --git a/src/cpp_dev/project/dependency/utils.py b/src/cpp_dev/project/dependency/utils.py deleted file mode 100644 index c0cb3f8..0000000 --- a/src/cpp_dev/project/dependency/utils.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) 2024 Andi Hellmund. All rights reserved. - -# This work is licensed under the terms of the BSD-3-Clause license. -# For a copy, see . - -from copy import deepcopy - -from cpp_dev.conan.package import get_available_versions - -from .parts import SemanticVersionWithOptionalParts, VersionSpecBound, VersionSpecBoundOperand -from .types import PackageDependency - -############################################################################### -# Public API ### -############################################################################### - - -DEFAULT_REPOSITORY = "official" - - -def refine_package_dependencies(deps: list[PackageDependency]) -> list[PackageDependency]: - """Refine the package dependencies in case of defaults were chosen. - - The refinement includes (in order): - o Default repository 'official' - o Latest resolved version in case of 'latest' - - This step is performed to assure that package dependencies with 'latest' do not get an older version - than the latest one at the time of resolution. This is important in case a versions gets removed. - """ - updated_deps = [] - for dep in deps: - parts = deepcopy(dep.parts) - if parts.repository is None: - parts.repository = DEFAULT_REPOSITORY - if parts.version_spec == "latest": - available_versions = get_available_versions(parts.repository, parts.name) - if len(available_versions) == 0: - raise ValueError(f"No available versions for package {parts.name} at repository {parts.repository}.") - parts.version_spec = [ - VersionSpecBound( - operand=VersionSpecBoundOperand.GREATER_THAN_OR_EQUAL, - version=SemanticVersionWithOptionalParts.from_semantic_version(available_versions[0]), - ) - ] - updated_deps.append(PackageDependency.from_parts(parts)) - return updated_deps diff --git a/src/cpp_dev/project/lockfile.py b/src/cpp_dev/project/lockfile.py index 8280231..ecc332e 100644 --- a/src/cpp_dev/project/lockfile.py +++ b/src/cpp_dev/project/lockfile.py @@ -9,8 +9,8 @@ import yaml from pydantic import BaseModel -from cpp_dev.common.types import SemanticVersion -from cpp_dev.project.constants import compose_project_lock_file +from cpp_dev.common.version import SemanticVersion +from cpp_dev.project.path_composition import compose_project_lock_file ############################################################################### # Public API ### diff --git a/src/cpp_dev/project/management.py b/src/cpp_dev/project/management.py deleted file mode 100644 index 3b031d3..0000000 --- a/src/cpp_dev/project/management.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright (c) 2024 Andi Hellmund. All rights reserved. - -# This work is licensed under the terms of the BSD-3-Clause license. -# For a copy, see . - -from pathlib import Path -from textwrap import dedent -from typing import get_args - -from cpp_dev.conan.package import compute_dependency_graph - -from .config import create_project_config, load_project_config, update_dependencies -from .constants import compose_include_file, compose_source_file -from .dependency.types import PackageDependency -from .dependency.utils import refine_package_dependencies -from .lockfile import create_initial_lock_file -from .types import DependencyType, ProjectConfig - -############################################################################### -# Public API ### -############################################################################### - - -def setup_project( - project_config: ProjectConfig, - parent_dir: Path | None = None, -) -> Path: - """Create a new cpp-dev project in the specified parent directory. - - The path to the new project directory is returned. - """ - project_dir = _validate_project_dir(parent_dir, project_config.name) - create_project_config(project_dir, project_config) - create_initial_lock_file(project_dir) - _add_default_cpd_dependencies(project_dir) - _create_project_files(project_dir, project_config.name) - return project_dir - - -def add_package_dependency(project_dir: Path, deps: list[PackageDependency], dep_type: DependencyType) -> None: - """Add package dependencies to the project for the given type.""" - refined_deps = refine_package_dependencies(deps) - project_config = load_project_config(project_dir) - updated_config = update_dependencies(project_config, refined_deps, dep_type) - _collect_dependency_graph(updated_config) - - -############################################################################### -# Implementation ### -############################################################################### - - -def _validate_project_dir(parent_dir: Path | None, name: str) -> Path: - """Check and validate if the project directory does not yet exist.""" - if parent_dir is None: - parent_dir = Path.cwd() - project_dir = parent_dir / name - if project_dir.exists(): - raise ValueError(f"Project directory {project_dir} already exists.") - project_dir.mkdir(parents=True) - return project_dir - - -def _add_default_cpd_dependencies(project_dir: Path) -> None: - add_package_dependency(project_dir, [PackageDependency("llvm"), PackageDependency("gtest")], "cpd") - - -def _collect_dependency_graph(project_config: ProjectConfig) -> None: - all_package_deps = [ - dep for dep_type in get_args(DependencyType) for dep in project_config.get_dependencies(dep_type) - ] - compute_dependency_graph(all_package_deps) - - -def _create_project_files(project_dir: Path, name: str) -> None: - """Create the necessary project files for the cpp-dev package.""" - _create_library_include_file(project_dir, name) - _create_library_source_file(project_dir, name) - _create_library_test_file(project_dir, name) - - -def _create_library_include_file(project_dir: Path, name: str) -> None: - include_file = compose_include_file(project_dir, name, f"{name}.hpp") - include_file.parent.mkdir(parents=True) - include_file.write_text( - dedent( - f"""\ - #pragma once - - namespace {name} {{ - int api(); - }} - """, - ), - ) - - -def _create_library_source_file(project_dir: Path, name: str) -> None: - source_file = compose_source_file(project_dir, f"{name}.cpp") - source_file.parent.mkdir(parents=True) - source_file.write_text( - dedent( - f"""\ - #include "{name}/{name}.hpp" - - int {name}::api() {{ - return 42; - }} - """, - ), - ) - - -def _create_library_test_file(project_dir: Path, name: str) -> None: - test_file = compose_source_file(project_dir, f"{name}.test.cpp") - test_file.write_text( - dedent( - f"""\ - #include - #include "{name}/{name}.hpp" - - TEST({name}, api) {{ - EXPECT_EQ({name}::api(), 42); - }} - """, - ), - ) diff --git a/src/cpp_dev/project/constants.py b/src/cpp_dev/project/path_composition.py similarity index 89% rename from src/cpp_dev/project/constants.py rename to src/cpp_dev/project/path_composition.py index ae8e3ce..08856a7 100644 --- a/src/cpp_dev/project/constants.py +++ b/src/cpp_dev/project/path_composition.py @@ -11,12 +11,12 @@ def compose_project_config_file(project_dir: Path) -> Path: - """Compose the path to the project's configuration file.""" + """Compose the path to the project"s configuration file.""" return project_dir / "cpp-dev.yaml" def compose_project_lock_file(project_dir: Path) -> Path: - """Compose the path to the project's lock file.""" + """Compose the path to the project"s lock file.""" return project_dir / "cpp-dev.lock" diff --git a/src/cpp_dev/project/types.py b/src/cpp_dev/project/types.py deleted file mode 100644 index caadd54..0000000 --- a/src/cpp_dev/project/types.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (c) 2024 Andi Hellmund. All rights reserved. - -# This work is licensed under the terms of the BSD-3-Clause license. -# For a copy, see . - -from typing import Literal - -from pydantic import BaseModel - -from cpp_dev.common.types import CppStandard, SemanticVersion - -from .dependency.types import PackageDependency - -############################################################################### -# Public API ### -############################################################################### - -DependencyType = Literal["runtime", "dev", "cpd"] - - -class ProjectConfig(BaseModel): - """A project configuration for a cpp-dev project.""" - - name: str - version: SemanticVersion - std: CppStandard - - author: str | None - license: str | None - description: str | None - - # Public dependencies used by the project - dependencies: list[PackageDependency] - - # Development dependencies used by the project while developing - dev_dependencies: list[PackageDependency] - - # Cpp-Dev dependencies used by the tool itself. These dependencies can only be updated but not removed. - cpd_dependencies: list[PackageDependency] - - def get_dependencies(self, dep_type: DependencyType) -> list[PackageDependency]: - """Return the dependency list by type.""" - if dep_type == "runtime": - return self.dependencies - if dep_type == "dev": - return self.dev_dependencies - if dep_type == "cpd": - return self.cpd_dependencies - raise ValueError(f"Invalid dependency type requested: {dep_type}") diff --git a/src/cpp_dev/tool/init.py b/src/cpp_dev/tool/init.py index f2a7f3b..b2018b9 100644 --- a/src/cpp_dev/tool/init.py +++ b/src/cpp_dev/tool/init.py @@ -9,7 +9,7 @@ from filelock import FileLock, Timeout from cpp_dev.common.utils import ensure_dir_exists -from cpp_dev.conan.setup import initialize_conan +from cpp_dev.dependency.conan.setup import get_conan_config_source_dir, initialize_conan from cpp_dev.tool.version import get_cpd_version_from_code, read_version_file, write_version_file ############################################################################### @@ -91,7 +91,7 @@ def _initialize_cpd(cpd_dir: Path) -> None: def _initialize_conan(cpd_dir: Path) -> None: conan_dir = _compose_conan_home(cpd_dir) ensure_dir_exists(conan_dir) - initialize_conan(conan_dir) + initialize_conan(conan_dir, get_conan_config_source_dir()) write_version_file(cpd_dir, get_cpd_version_from_code()) diff --git a/src/cpp_dev/tool/version.py b/src/cpp_dev/tool/version.py index d100842..81163aa 100644 --- a/src/cpp_dev/tool/version.py +++ b/src/cpp_dev/tool/version.py @@ -5,7 +5,7 @@ from pathlib import Path -from cpp_dev.common.types import SemanticVersion +from cpp_dev.common.version import SemanticVersion ############################################################################### # Public API ### diff --git a/src/cpp_dev/ui/cli.py b/src/cpp_dev/ui/cli.py index 735e23d..087fafe 100644 --- a/src/cpp_dev/ui/cli.py +++ b/src/cpp_dev/ui/cli.py @@ -9,7 +9,7 @@ import typed_argparse as tap -from cpp_dev.common.os import assert_supported_os +from cpp_dev.common.os_detection import assert_supported_os from .mgmt import VersionArgs, command_version from .project import ( diff --git a/src/cpp_dev/ui/mgmt.py b/src/cpp_dev/ui/mgmt.py index b1936c3..f2a9867 100644 --- a/src/cpp_dev/ui/mgmt.py +++ b/src/cpp_dev/ui/mgmt.py @@ -13,7 +13,7 @@ class VersionArgs(tap.TypedArgs): - """Arguments for the 'cpd version' command.""" + """Arguments for the "cpd version" command.""" def command_version(_: VersionArgs) -> None: diff --git a/src/cpp_dev/ui/project.py b/src/cpp_dev/ui/project.py index 5c46474..3e9ad58 100644 --- a/src/cpp_dev/ui/project.py +++ b/src/cpp_dev/ui/project.py @@ -25,7 +25,7 @@ def _validate_project_name(name: str) -> str: class NewProjectArgs(tap.TypedArgs): - """Arguments for the 'cpd new' command.""" + """Arguments for the "cpd new" command.""" name: str = tap.arg(help="The name of the project.", positional=True, type=_validate_project_name) version: SemanticVersion = tap.arg(help="The version of the project.") @@ -41,18 +41,18 @@ class NewProjectArgs(tap.TypedArgs): class AddDependencyArgs(tap.TypedArgs): - """Arguments for the 'cpd add' command.""" + """Arguments for the "cpd add" command.""" dependency_spec: list[str] = tap.arg( - help="""The dependency to add to the project. The format is '/[]'. + help="""The dependency to add to the project. The format is "/[]". The parameter is the user or organization that owns the dependency. - This parameter is optional in which case an 'official' repository is assumed. + This parameter is optional in which case an "official" repository is assumed. The parameter is the name of the dependency. - The parameter supports an exact version, lower/upper bounds, intervals or 'latest'. + The parameter supports an exact version, lower/upper bounds, intervals or "latest". This parameter is optional in which case the latest version is used. - The exact version is specified as '..', while lower/upper bounds and intervals - use the format '< | <= | > | >= [.[.]]' with minor and parts parts being optional. - Multiple range specs can be combined with ',' forming a logical AND conjunction. + The exact version is specified as "..", while lower/upper bounds and intervals + use the format "< | <= | > | >= [.[.]]" with minor and parts parts being optional. + Multiple range specs can be combined with "," forming a logical AND conjunction. Examples: boost, boost[latest], boost[1.0.0], boost[>=1.0], boost[<2.0], official/boost[>=1.5,<2.0] """, positional=True, @@ -60,27 +60,27 @@ class AddDependencyArgs(tap.TypedArgs): class BuildArgs(tap.TypedArgs): - """Arguments for the 'cpd build' command.""" + """Arguments for the "cpd build" command.""" class ExecutionArgs(tap.TypedArgs): - """Arguments for the 'cpd execute' command.""" + """Arguments for the "cpd execute" command.""" class TestArgs(tap.TypedArgs): - """Arguments for the 'cpd test' command.""" + """Arguments for the "cpd test" command.""" class CheckArgs(tap.TypedArgs): - """Arguments for the 'cpd check' command.""" + """Arguments for the "cpd check" command.""" class FormatArgs(tap.TypedArgs): - """Arguments for the 'cpd format' command.""" + """Arguments for the "cpd format" command.""" class PackageArgs(tap.TypedArgs): - """Arguments for the 'cpd package' command.""" + """Arguments for the "cpd package" command.""" def command_new_project(args: NewProjectArgs) -> None: diff --git a/src/tests/cpp_dev/conan/__init__.py b/src/tests/__init__.py similarity index 100% rename from src/tests/cpp_dev/conan/__init__.py rename to src/tests/__init__.py diff --git a/src/tests/cpp_dev/project/dependency/__init__.py b/src/tests/cpp_dev/__init__.py similarity index 100% rename from src/tests/cpp_dev/project/dependency/__init__.py rename to src/tests/cpp_dev/__init__.py diff --git a/src/tests/cpp_dev/common/test_os.py b/src/tests/cpp_dev/common/test_os.py deleted file mode 100644 index 95867d5..0000000 --- a/src/tests/cpp_dev/common/test_os.py +++ /dev/null @@ -1,52 +0,0 @@ -# This work is licensed under the terms of the BSD-3-Clause license. -# For a copy, see . - -from unittest.mock import patch - -import pytest - -from cpp_dev.common.os import OperatingSystemType, assert_supported_os, detect_os - - -@pytest.mark.parametrize( - ("os_data", "expected_os_type"), - [ - (("ubuntu", "24.04"), OperatingSystemType.Ubuntu2404), - (("centos", "0"), OperatingSystemType.Unsupported), - ], -) -def test_detect_os(os_data: tuple[str, str], expected_os_type: OperatingSystemType) -> None: - os_id, os_version = os_data - with ( - patch("cpp_dev.common.os.distro.id", return_value=os_id), - patch("cpp_dev.common.os.distro.version", return_value=os_version), - ): - operating_system = detect_os() - assert operating_system.type == expected_os_type - - -@pytest.mark.parametrize( - ("os_data"), - [("ubuntu", "24.04")], -) -def test_assert_supported_os_ok(os_data: tuple[str, str]) -> None: - os_id, os_version = os_data - with ( - patch("cpp_dev.common.os.distro.id", return_value=os_id), - patch("cpp_dev.common.os.distro.version", return_value=os_version), - ): - assert_supported_os() - - -@pytest.mark.parametrize( - ("os_data"), - [("centos", "7")], -) -def test_assert_supported_os_not_ok(os_data: tuple[str, str]) -> None: - os_id, os_version = os_data - with ( - patch("cpp_dev.common.os.distro.id", return_value=os_id), - patch("cpp_dev.common.os.distro.version", return_value=os_version), - pytest.raises(RuntimeError), - ): - assert_supported_os() diff --git a/src/tests/cpp_dev/common/test_os_detection.py b/src/tests/cpp_dev/common/test_os_detection.py new file mode 100644 index 0000000..d582c4c --- /dev/null +++ b/src/tests/cpp_dev/common/test_os_detection.py @@ -0,0 +1,53 @@ +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + +from collections.abc import Generator +from contextlib import contextmanager +from unittest.mock import patch + +import pytest + +from cpp_dev.common.os_detection import OperatingSystemType, assert_supported_os, detect_os + + +@contextmanager +def patch_os_detection_module(os_id: str, os_version: str) -> Generator[None]: + with ( + patch("cpp_dev.common.os_detection.distro.id", return_value=os_id), + patch("cpp_dev.common.os_detection.distro.version", return_value=os_version), + ): + yield + + +@pytest.mark.parametrize( + ("os_id", "os_version", "expected_os_type"), + [ + ("ubuntu", "24.04", OperatingSystemType.Ubuntu2404), + ("centos", "0", OperatingSystemType.Unsupported), + ], +) +def test_detect_os(os_id: str, os_version: str, expected_os_type: OperatingSystemType) -> None: + with patch_os_detection_module(os_id, os_version): + operating_system = detect_os() + assert operating_system.type == expected_os_type + + +@pytest.mark.parametrize( + ("os_id", "os_version"), + [("ubuntu", "24.04")], +) +def test_assert_supported_os_ok(os_id: str, os_version: str) -> None: + with patch_os_detection_module(os_id, os_version): + assert_supported_os() + + +@pytest.mark.parametrize( + ("os_id", "os_version"), + [("centos", "7")], +) +def test_assert_supported_os_not_ok(os_id: str, os_version: str) -> None: + with ( + patch_os_detection_module(os_id, os_version), + pytest.raises(RuntimeError, match="Unsupported operating system"), + ): + assert_supported_os() diff --git a/src/tests/cpp_dev/common/test_types.py b/src/tests/cpp_dev/common/test_types.py deleted file mode 100644 index 725d1bb..0000000 --- a/src/tests/cpp_dev/common/test_types.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (c) 2024 Andi Hellmund. All rights reserved. - -# This work is licensed under the terms of the BSD-3-Clause license. -# For a copy, see . - - -import pytest - -from cpp_dev.common.types import SemanticVersion, SemanticVersionParts - - -def test_semantic_version_ok() -> None: - SemanticVersion("1.2.3") - SemanticVersion("10.20.30") - - -@pytest.mark.parametrize("version", ["1.2", "abc", "a.b.c", "1.2.3.4"]) -def test_semantic_version_fail(version: str) -> None: - with pytest.raises(ValueError, match="Invalid semantic version"): - SemanticVersion(version) - - -def test_semantic_version_from_parts() -> None: - version = SemanticVersion.from_parts(1, 2, 3) - assert version.root == "1.2.3" - - -def test_semantic_version_parts() -> None: - version = SemanticVersion("1.2.3") - assert version.parts == SemanticVersionParts(1, 2, 3) diff --git a/src/tests/cpp_dev/common/test_version.py b/src/tests/cpp_dev/common/test_version.py new file mode 100644 index 0000000..d0dc748 --- /dev/null +++ b/src/tests/cpp_dev/common/test_version.py @@ -0,0 +1,56 @@ +# Copyright (c) 2024 Andi Hellmund. All rights reserved. + +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + + +import pytest + +from cpp_dev.common.version import SemanticVersion, SemanticVersionWithOptionalParts + + +def test_semantic_version_ok() -> None: + version = SemanticVersion("1.2.3") + assert version.major == 1 + assert version.minor == 2 + assert version.patch == 3 + + version = SemanticVersion("10.20.30") + assert version.major == 10 + assert version.minor == 20 + assert version.patch == 30 + + +@pytest.mark.parametrize("version", ["1.2", "abc", "a.b.c", "1.2.3.4", "1.-2.3", "1.2.abc"]) +def test_semantic_version_fail(version: str) -> None: + with pytest.raises(ValueError, match="Invalid semantic version"): + SemanticVersion(version) + + +def test_semantic_version_from_parts() -> None: + version = SemanticVersion.from_parts(1, 2, 3) + assert version.major == 1 + assert version.minor == 2 + assert version.patch == 3 + + +def test_semantic_version_with_optional_parts_ok() -> None: + version = SemanticVersionWithOptionalParts(1, 2, 3) + assert version.major == 1 + assert version.minor == 2 + assert version.patch == 3 + + version = SemanticVersionWithOptionalParts(1, 2, None) + assert version.major == 1 + assert version.minor == 2 + assert version.patch is None + + version = SemanticVersionWithOptionalParts(1, None, None) + assert version.major == 1 + assert version.minor is None + assert version.patch is None + + +def test_semantic_version_with_optional_parts_fail() -> None: + with pytest.raises(ValueError, match="Cannot specify a patch version without a minor version"): + SemanticVersionWithOptionalParts(1, None, 3) diff --git a/src/tests/cpp_dev/conan/test_command_wrapper.py b/src/tests/cpp_dev/conan/test_command_wrapper.py deleted file mode 100644 index 25faea5..0000000 --- a/src/tests/cpp_dev/conan/test_command_wrapper.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright (c) 2024 Andi Hellmund. All rights reserved. - -# This work is licensed under the terms of the BSD-3-Clause license. -# For a copy, see . - -import json -from unittest.mock import patch - -from cpp_dev.conan.command_wrapper import (conan_graph_buildorder, conan_list, - conan_remote_login) - - -def test_conan_list() -> None: - with patch("cpp_dev.conan.command_wrapper.run_command_assert_success") as mock_run_command: - mock_run_command.return_value = (json.dumps({"official": {}}), None) - conan_list("official", "cpd") - mock_run_command.assert_called_once_with( - "conan", - "list", - "--json", - "--remote=official", - "cpd/", - ) - -def test_conan_remote_login() -> None: - with patch("cpp_dev.conan.command_wrapper.run_command_assert_success") as mock_run_command: - conan_remote_login("official", "user", "password") - mock_run_command.assert_called_once_with( - "conan", - "remote", - "login", - "official", - "user", - "-p", - "password", - ) - -def test_conan_graph_buildorder() -> None: - with patch("cpp_dev.conan.command_wrapper.run_command_assert_success") as mock_run_command: - mock_run_command.return_value = (json.dumps({}), None) - conan_graph_buildorder("conanfile.txt", "profile") - mock_run_command.assert_called_once_with( - "conan", - "graph", - "buildorder", - "conanfile.txt", - "-pr:a", - "profile", - "--json", - "--order-by", - "recipe", - ) \ No newline at end of file diff --git a/src/tests/cpp_dev/conan/test_package.py b/src/tests/cpp_dev/conan/test_package.py deleted file mode 100644 index 343f33f..0000000 --- a/src/tests/cpp_dev/conan/test_package.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (c) 2024 Andi Hellmund. All rights reserved. - -# This work is licensed under the terms of the BSD-3-Clause license. -# For a copy, see . - -from unittest.mock import patch - -from cpp_dev.common.types import SemanticVersion -from cpp_dev.conan.package import get_available_versions -from cpp_dev.conan.types import ConanPackageReference - - -def test_get_available_versions() -> None: - with patch("cpp_dev.conan.package.conan_list", return_value={ - ConanPackageReference("cpd/1.0.0@official/cppdev"): {}, - ConanPackageReference("cpd/2.0.0@custom/cppdev"): {}, - ConanPackageReference("cpd/3.0.0@official/cppdev"): {}, - }): - assert get_available_versions("official", "cpd") == [ - SemanticVersion("3.0.0"), - SemanticVersion("1.0.0"), - ] \ No newline at end of file diff --git a/src/tests/cpp_dev/conan/test_types.py b/src/tests/cpp_dev/conan/test_types.py deleted file mode 100644 index e179108..0000000 --- a/src/tests/cpp_dev/conan/test_types.py +++ /dev/null @@ -1,22 +0,0 @@ -import pytest - -from cpp_dev.common.types import SemanticVersion -from cpp_dev.conan.types import ConanPackageReference - - -@pytest.mark.parametrize("invalid_ref", [ - "", - "invalid_reference", - "mypackage/@myuser/stable", -]) -def test_conan_package_reference_invalid(invalid_ref): - with pytest.raises(ValueError, match="Invalid Conan package reference:"): - ConanPackageReference(invalid_ref) - -def test_conan_package_reference_valid() -> None: - package_ref = ConanPackageReference("name/1.2.3@user/channel") - - assert package_ref.name == "name" - assert package_ref.version == SemanticVersion("1.2.3") - assert package_ref.user == "user" - assert package_ref.channel == "channel" diff --git a/src/tests/cpp_dev/conftest.py b/src/tests/cpp_dev/conftest.py new file mode 100644 index 0000000..0371913 --- /dev/null +++ b/src/tests/cpp_dev/conftest.py @@ -0,0 +1,30 @@ +# Copyright (c) 2024 Andi Hellmund. All rights reserved. + +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + +import socket + +import pytest + + +############################################################################### +# Public API ### +############################################################################### +@pytest.fixture +def unused_http_port() -> int: + """Return an unused HTTP port in a pre-defined range.""" + for port in range(50000, 50100): + if not _is_port_in_use(port): + return port + raise RuntimeError("No unused HTTP port found") + + +############################################################################### +# Implementation ### +############################################################################### + + +def _is_port_in_use(port: int) -> bool: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + return s.connect_ex(("localhost", port)) == 0 diff --git a/src/tests/cpp_dev/dependency/__init__.py b/src/tests/cpp_dev/dependency/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/cpp_dev/dependency/conan/__init__.py b/src/tests/cpp_dev/dependency/conan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/cpp_dev/dependency/conan/test_command_wrapper.py b/src/tests/cpp_dev/dependency/conan/test_command_wrapper.py new file mode 100644 index 0000000..60e9411 --- /dev/null +++ b/src/tests/cpp_dev/dependency/conan/test_command_wrapper.py @@ -0,0 +1,144 @@ +# Copyright (c) 2024 Andi Hellmund. All rights reserved. + +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + +from collections.abc import Generator +from pathlib import Path +from textwrap import dedent +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from cpp_dev.dependency.conan.command_wrapper import (ConanCommandException, + ConanSettings, + conan_create, + conan_graph_buildorder, + conan_list, + conan_remote_login, + conan_upload) +from cpp_dev.dependency.conan.setup import CONAN_REMOTE +from cpp_dev.dependency.conan.types import \ + ConanPackageReferenceWithSemanticVersion + +from .utils.env import ConanTestEnv, ConanTestPackage, create_conan_test_env +from .utils.server import launch_conan_test_server + +MockType = MagicMock | AsyncMock + +@pytest.fixture +def patched_run_command_assert_success() -> Generator[MockType]: + with patch("cpp_dev.dependency.conan.command_wrapper.run_command_assert_success") as mock_run_command: + yield mock_run_command + +def test_conan_create(patched_run_command_assert_success: MockType) -> None: + # todo: this test currently uses a mock, but wil later be changed to test with a real server. + conan_create(Path("package_dir"), "profile", {"compiler": "test", "compiler.cppstd": "c++20"}) + patched_run_command_assert_success.assert_called_once_with( + "conan", + "create", + "package_dir", + "-pr:a", "profile", + "-s:a", "compiler=test", + "-s:a", "compiler.cppstd=c++20", + ) + +def test_conan_upload(patched_run_command_assert_success: MockType) -> None: + # todo: this test currently uses a mock, but wil later be changed to test with a real server. + package_ref = ConanPackageReferenceWithSemanticVersion("cpd/1.0.0@official/cppdev") + conan_upload(package_ref, CONAN_REMOTE) + patched_run_command_assert_success.assert_called_once_with( + "conan", + "upload", + "-r", CONAN_REMOTE, + str(package_ref), + ) + + +@pytest.fixture +def conan_test_environment(tmp_path: Path, unused_http_port: int) -> Generator[ConanTestEnv]: + with launch_conan_test_server(tmp_path, unused_http_port) as server: + TEST_PACKAGES = [ + ConanTestPackage( + ref=ConanPackageReferenceWithSemanticVersion("dep/1.0.0@official/cppdev"), + dependencies=[], + cpp_standard="c++20", + ), + ConanTestPackage( + ref=ConanPackageReferenceWithSemanticVersion("dep/2.0.0@official/cppdev"), + dependencies=[], + cpp_standard="c++20", + ), + ConanTestPackage( + ref=ConanPackageReferenceWithSemanticVersion("cpd/1.0.0@official/cppdev"), + dependencies=[ConanPackageReferenceWithSemanticVersion("dep/1.0.0@official/cppdev")], + cpp_standard="c++20", + ), + ConanTestPackage( + ref=ConanPackageReferenceWithSemanticVersion("cpd1/1.0.0@official/cppdev"), + dependencies=[ConanPackageReferenceWithSemanticVersion("dep/2.0.0@official/cppdev")], + cpp_standard="c++20", + ), + ] + with create_conan_test_env(tmp_path / "conan", server.http_port, TEST_PACKAGES) as conan_test_env: + yield conan_test_env + +@pytest.mark.conan_remote +def test_conan_remote_login(conan_test_environment: ConanTestEnv) -> None: + conan_remote_login(CONAN_REMOTE, conan_test_environment.server.user, conan_test_environment.server.password) + + +@pytest.mark.conan_remote +@pytest.mark.usefixtures("conan_test_environment") +def test_conan_list() -> None: + result = conan_list(CONAN_REMOTE, "cpd") + assert len(result) == 1 + assert ConanPackageReferenceWithSemanticVersion("cpd/1.0.0@official/cppdev") in result + + +@pytest.mark.conan_remote +def test_conan_graph_buildorder(tmp_path: Path, conan_test_environment: ConanTestEnv) -> None: + conanfile_path = tmp_path / "conanfile.txt" + conanfile_path.write_text(dedent(""" + [requires] + cpd/1.0.0@official/cppdev + """) + ) + graph_build_order = conan_graph_buildorder(conanfile_path, conan_test_environment.profile, conan_test_environment.construct_conan_settings()) + assert len(graph_build_order.order) == 2 + assert len(graph_build_order.order[0]) == 1 + dep_recipe = graph_build_order.order[0][0] + assert dep_recipe.ref.startswith("dep/1.0.0@official/cppdev") + + assert len(graph_build_order.order[1]) == 1 + cpd_recipe = graph_build_order.order[1][0] + assert cpd_recipe.ref.startswith("cpd/1.0.0@official/cppdev") + assert len(cpd_recipe.depends) == 1 + assert cpd_recipe.depends[0].startswith("dep/1.0.0@official/cppdev") + + +@pytest.mark.conan_remote +def test_conan_graph_buildorder_dependency_does_not_exist(tmp_path: Path, conan_test_environment: ConanTestEnv) -> None: + conanfile_path = tmp_path / "conanfile.txt" + conanfile_path.write_text(dedent(""" + [requires] + cpd/0.0.0@official/cppdev + """) + ) + + with pytest.raises(ConanCommandException, match="unable to find package") as e: + conan_graph_buildorder(conanfile_path, conan_test_environment.profile, conan_test_environment.construct_conan_settings()) + + +@pytest.mark.conan_remote +def test_conan_graph_buildorder_multiple_dependencies(tmp_path: Path, conan_test_environment: ConanTestEnv) -> None: + conanfile_path = tmp_path / "conanfile.txt" + conanfile_path.write_text(dedent(""" + [requires] + cpd/[>=0.0.0]@official/cppdev + cpd1/[<2.0.0]@official/cppdev + """) + ) + + with pytest.raises(ConanCommandException, match="version conflict") as e: + conan_graph_buildorder(conanfile_path, conan_test_environment.profile, conan_test_environment.construct_conan_settings()) \ No newline at end of file diff --git a/src/tests/cpp_dev/dependency/conan/test_provider.py b/src/tests/cpp_dev/dependency/conan/test_provider.py new file mode 100644 index 0000000..77471dc --- /dev/null +++ b/src/tests/cpp_dev/dependency/conan/test_provider.py @@ -0,0 +1,81 @@ +# Copyright (c) 2024 Andi Hellmund. All rights reserved. + +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + +from collections.abc import Generator +from dataclasses import dataclass +from pathlib import Path + +import pytest + +from cpp_dev.common.types import CppStandard +from cpp_dev.common.version import SemanticVersion +from cpp_dev.dependency.conan.command_wrapper import ConanSettings +from cpp_dev.dependency.conan.provider import ConanDependencyProvider +from cpp_dev.dependency.conan.types import \ + ConanPackageReferenceWithSemanticVersion +from cpp_dev.dependency.provider import DependencyIdentifier +from cpp_dev.dependency.specifier import DependencySpecifier +from tests.cpp_dev.dependency.conan.utils.env import (ConanTestEnv, + ConanTestPackage, + create_conan_test_env) + + +@pytest.fixture +def conan_test_environment(tmp_path: Path, unused_http_port: int) -> Generator[ConanTestEnv]: + TEST_PACKAGES = [ + ConanTestPackage( + ref=ConanPackageReferenceWithSemanticVersion("subdep/1.0.0@official/cppdev"), + dependencies=[], + cpp_standard="c++20", + ), + ConanTestPackage( + ref=ConanPackageReferenceWithSemanticVersion("dep/1.0.0@official/cppdev"), + dependencies=[ + ConanPackageReferenceWithSemanticVersion("subdep/1.0.0@official/cppdev") + ], + cpp_standard="c++20", + ), + ConanTestPackage( + ref=ConanPackageReferenceWithSemanticVersion("cpd/1.0.0@official/cppdev"), + dependencies=[], + cpp_standard="c++20", + ), + ConanTestPackage( + ref=ConanPackageReferenceWithSemanticVersion("cpd/2.0.0@custom/cppdev"), + dependencies=[], + cpp_standard="c++20", + ), + ConanTestPackage( + ref=ConanPackageReferenceWithSemanticVersion("cpd/3.0.0@official/cppdev"), + dependencies=[ + ConanPackageReferenceWithSemanticVersion("dep/1.0.0@official/cppdev") + ], + cpp_standard="c++20", + ), + ] + with create_conan_test_env(tmp_path, unused_http_port, TEST_PACKAGES) as conan_test_env: + yield conan_test_env + + +@pytest.mark.conan_remote +def test_get_available_versions(conan_test_environment: ConanTestEnv) -> None: + provider = ConanDependencyProvider(conan_test_environment.conan_home_dir, conan_test_environment.profile, conan_test_environment.construct_conan_settings()) + assert provider.fetch_versions("official", "cpd") == [ + SemanticVersion("3.0.0"), + SemanticVersion("1.0.0"), + ] + + +@pytest.mark.conan_remote +def test_collect_dependency_hull(conan_test_environment: ConanTestEnv) -> None: + provider = ConanDependencyProvider(conan_test_environment.conan_home_dir, conan_test_environment.profile, conan_test_environment.construct_conan_settings()) + deps = [ + DependencySpecifier("official/cpd[>=3.0.0]"), + ] + dependencies = provider.collect_dependency_hull(deps) + assert len(dependencies) == 3 + assert DependencyIdentifier.from_str("official/cpd/3.0.0") in dependencies + assert DependencyIdentifier.from_str("official/dep/1.0.0") in dependencies + assert DependencyIdentifier.from_str("official/subdep/1.0.0") in dependencies \ No newline at end of file diff --git a/src/tests/cpp_dev/conan/test_setup.py b/src/tests/cpp_dev/dependency/conan/test_setup.py similarity index 55% rename from src/tests/cpp_dev/conan/test_setup.py rename to src/tests/cpp_dev/dependency/conan/test_setup.py index 690d28a..37bf7ef 100644 --- a/src/tests/cpp_dev/conan/test_setup.py +++ b/src/tests/cpp_dev/dependency/conan/test_setup.py @@ -7,14 +7,15 @@ from pathlib import Path from unittest.mock import patch -from cpp_dev.conan.setup import initialize_conan +from cpp_dev.dependency.conan.setup import (get_conan_config_source_dir, + initialize_conan) def test_initialize_conan(tmp_path: Path) -> None: - with patch("cpp_dev.conan.setup.conan_remote_login") as mock: - initialize_conan(tmp_path) + with patch("cpp_dev.dependency.conan.setup.conan_remote_login") as mock: + initialize_conan(tmp_path, get_conan_config_source_dir()) mock.assert_called_once() assert (tmp_path / "remotes.json").exists() assert (tmp_path / "settings.yml").exists() - assert (tmp_path / "profiles" / "ubuntu-24.04").exists() + assert (tmp_path / "profiles" / "ubuntu-24.04-x86_64").exists() diff --git a/src/tests/cpp_dev/dependency/conan/test_types.py b/src/tests/cpp_dev/dependency/conan/test_types.py new file mode 100644 index 0000000..e5c4a73 --- /dev/null +++ b/src/tests/cpp_dev/dependency/conan/test_types.py @@ -0,0 +1,32 @@ +import pytest + +from cpp_dev.common.version import SemanticVersion +from cpp_dev.dependency.conan.types import \ + ConanPackageReferenceWithSemanticVersion + + +@pytest.mark.parametrize("invalid_ref", [ + "", + "invalid_reference", + "mypackage/@myuser/stable", +]) +def test_conan_package_reference_invalid(invalid_ref): + with pytest.raises(ValueError, match="Invalid Conan package reference:"): + ConanPackageReferenceWithSemanticVersion(invalid_ref) + +def test_conan_package_reference_valid() -> None: + package_ref = ConanPackageReferenceWithSemanticVersion("name/1.2.3@user/channel") + + assert package_ref.name == "name" + assert package_ref.version == SemanticVersion("1.2.3") + assert package_ref.user == "user" + assert package_ref.channel == "channel" + + +def test_conan_package_reference_from_raw_string_with_revision() -> None: + package_ref = ConanPackageReferenceWithSemanticVersion.from_raw_string_with_revision("name/1.2.3@user/channel#revision") + + assert package_ref.name == "name" + assert package_ref.version == SemanticVersion("1.2.3") + assert package_ref.user == "user" + assert package_ref.channel == "channel" \ No newline at end of file diff --git a/src/tests/cpp_dev/conan/test_utils.py b/src/tests/cpp_dev/dependency/conan/test_utils.py similarity index 74% rename from src/tests/cpp_dev/conan/test_utils.py rename to src/tests/cpp_dev/dependency/conan/test_utils.py index 0fc5257..15db48a 100644 --- a/src/tests/cpp_dev/conan/test_utils.py +++ b/src/tests/cpp_dev/dependency/conan/test_utils.py @@ -8,10 +8,10 @@ import pytest -from cpp_dev.conan.utils import (CONAN_HOME_ENV_VAR, - compose_conan_package_reference, conan_env, - create_conanfile) -from cpp_dev.project.dependency.types import PackageDependency +from cpp_dev.dependency.conan.utils import (CONAN_HOME_ENV_VAR, + compose_conan_package_reference, + conan_env, create_conanfile) +from cpp_dev.dependency.specifier import DependencySpecifier def test_conan_env() -> None: @@ -25,9 +25,9 @@ def test_conan_env() -> None: def test_create_conanfile(tmp_path: Path) -> None: package_deps = [ - PackageDependency("official/cpd[1.0.0]"), - PackageDependency("custom/other[>2.0.0]"), - PackageDependency("custom/other2[>=3.0.0,<4.0.0]"), + DependencySpecifier("official/cpd[1.0.0]"), + DependencySpecifier("custom/other[>2.0.0]"), + DependencySpecifier("custom/other2[>=3.0.0,<4.0.0]"), ] conanfile_path = create_conanfile(tmp_path, package_deps) assert conanfile_path.exists() @@ -45,5 +45,5 @@ def test_create_conanfile(tmp_path: Path) -> None: ("custom/other2[>=3.0.0,<4.0.0]", "other2/[>=3.0.0 <4.0.0]@custom/cppdev"), ]) def test_compose_conan_package_reference(ref: str, expected_conan_ref) -> None: - conan_ref = compose_conan_package_reference(PackageDependency(ref)) + conan_ref = compose_conan_package_reference(DependencySpecifier(ref)) assert conan_ref == expected_conan_ref \ No newline at end of file diff --git a/src/tests/cpp_dev/dependency/conan/utils/__init__.py b/src/tests/cpp_dev/dependency/conan/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/cpp_dev/dependency/conan/utils/env.py b/src/tests/cpp_dev/dependency/conan/utils/env.py new file mode 100644 index 0000000..9b69757 --- /dev/null +++ b/src/tests/cpp_dev/dependency/conan/utils/env.py @@ -0,0 +1,205 @@ +# Copyright (c) 2024 Andi Hellmund. All rights reserved. + +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + +import json +from collections.abc import Generator, Mapping +from contextlib import contextmanager +from dataclasses import dataclass +from pathlib import Path +from textwrap import dedent + +from cpp_dev.common.types import CppStandard +from cpp_dev.common.utils import ensure_dir_exists +from cpp_dev.dependency.conan.command_wrapper import (ConanSettings, + conan_create, + conan_upload) +from cpp_dev.dependency.conan.setup import CONAN_REMOTE, initialize_conan +from cpp_dev.dependency.conan.types import \ + ConanPackageReferenceWithSemanticVersion +from cpp_dev.dependency.conan.utils import conan_env +from tests.cpp_dev.dependency.conan.utils.server import ( + ConanServer, launch_conan_test_server) + +############################################################################### +# Public API ### +############################################################################### + +FileSpec = Mapping[Path, str] + +@dataclass +class ConanTestPackage: + ref: ConanPackageReferenceWithSemanticVersion + dependencies: list[ConanPackageReferenceWithSemanticVersion] + cpp_standard: CppStandard + bin_files: FileSpec | None = None + lib_files: FileSpec | None = None + include_files: FileSpec | None = None + + +class ConanTestEnv: + """A Conan environment for testing.""" + + def __init__(self, conan_home_dir: Path, profile: str, server: ConanServer, compiler: str, cppstd: CppStandard) -> None: + self._conan_home_dir = conan_home_dir + self._package_dir = conan_home_dir / ".conan_package_creation" + ensure_dir_exists(self._package_dir) + self._profile = profile + self._server = server + self._compiler = compiler + self._cppstd = cppstd + + def create_and_upload_packages(self, packages: list[ConanTestPackage]) -> None: + """Create and upload a Conan package for testing.""" + settings = self.construct_conan_settings() + for package in packages: + _create_and_upload_conan_package(self._package_dir, package, self._profile, settings) + + @property + def conan_home_dir(self) -> Path: + """Return the base directory of the Conan environment.""" + return self._conan_home_dir + + @property + def profile(self) -> str: + """Return the profile used for testing.""" + return self._profile + + @property + def server(self) -> ConanServer: + """Return the server used for testing.""" + return self._server + + @property + def compiler(self) -> str: + """Return the compiler used for testing.""" + return self._compiler + + @property + def cppstd(self) -> CppStandard: + """Return the C++ standard used for testing.""" + return self._cppstd + + def construct_conan_settings(self) -> ConanSettings: + """Construct the additional Conan settings for the Conan commands.""" + return { + "compiler": self._compiler, + "compiler.cppstd": self._cppstd, + } + + +@contextmanager +def create_conan_test_env(base_dir: Path, server_http_port: int, packages: list[ConanTestPackage]) -> Generator[ConanTestEnv]: + """Create a Conan environment for testing.""" + + # create the home for the conan server + conan_server_dir = base_dir / "server" + ensure_dir_exists(conan_server_dir) + with launch_conan_test_server(conan_server_dir, server_http_port) as server: + # create the home for the conan client + conan_home_dir = base_dir / "conan_home" + ensure_dir_exists(conan_home_dir) + source_config_path, attributes = _create_conan_source_config(conan_home_dir, server_http_port) + initialize_conan(conan_home_dir, source_config_path) + + with conan_env(conan_home_dir): + conan_test_env = ConanTestEnv(conan_home_dir, attributes.profile, server, attributes.compiler, attributes.cppstd) + conan_test_env.create_and_upload_packages(packages) + yield conan_test_env + + +############################################################################### +# Implementation ### +############################################################################### + +@dataclass +class ConanSourceConfigAttributes: + profile: str + compiler: str + cppstd: CppStandard + +def _create_conan_source_config(conan_dir: Path, server_http_port: int) -> tuple[Path, ConanSourceConfigAttributes]: + source_config_path = conan_dir / ".source_config" + ensure_dir_exists(source_config_path) + + PROFILE_NAME = "test" + test_profile_path = source_config_path / "profiles" / PROFILE_NAME + ensure_dir_exists(test_profile_path.parent) + test_profile_path.write_text(dedent( + """ + [settings] + arch=test + build_type=Test + os=Linux + os.distro=test + """ + )) + + COMPILER = "test" + CPPSTD = "c++20" + settings_path = source_config_path / "settings.yml" + settings_path.write_text(dedent( + f""" + os: + Linux: + distro: ["test"] + arch: [test] + compiler: + {COMPILER}: + cppstd: [{CPPSTD}] + build_type: [Test] + """ + )) + + remotes_path = source_config_path / "remotes.json" + remotes_path.write_text(json.dumps({ + "remotes": [ + { + "name": CONAN_REMOTE, + "url": f"http://localhost:{server_http_port}/", + "verify_ssl": False, + } + ] + })) + + return source_config_path, ConanSourceConfigAttributes( + profile=PROFILE_NAME, + compiler=COMPILER, + cppstd=CPPSTD, + ) + + +def _create_and_upload_conan_package(base_dir: Path, package: ConanTestPackage, profile: str, settings: ConanSettings) -> None: + package_dir = base_dir / f"{package.ref.name}_{package.ref.version}" + ensure_dir_exists(package_dir) + + conanfile_path = package_dir / "conanfile.py" + + requirements = ",".join([f"\"{dep}\"" for dep in package.dependencies]) + conanfile_path.write_text(dedent( + """ + from conan import ConanFile + + class TestConan(ConanFile): + name = "{name}" + user = "{user}" + channel = "cppdev" + version = "{version}" + + settings = "compiler" + + {requirements} + + def configure(self): + self.settings.rm_safe("compiler.libcxx") + + """.format( + name=package.ref.name, + user=package.ref.user, + version=package.ref.version, + requirements=f"requires = {requirements}" if package.dependencies else "", + ) + )) + conan_create(package_dir, profile, settings) + conan_upload(package.ref, CONAN_REMOTE) \ No newline at end of file diff --git a/src/tests/cpp_dev/dependency/conan/utils/server.py b/src/tests/cpp_dev/dependency/conan/utils/server.py new file mode 100644 index 0000000..4579db6 --- /dev/null +++ b/src/tests/cpp_dev/dependency/conan/utils/server.py @@ -0,0 +1,113 @@ +# Copyright (c) 2024 Andi Hellmund. All rights reserved. + +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + +import subprocess +import time +from collections.abc import Generator +from contextlib import contextmanager +from dataclasses import dataclass +from pathlib import Path +from textwrap import dedent + +import requests + +from cpp_dev.common.utils import ensure_dir_exists +from cpp_dev.dependency.conan.setup import (DEFAULT_CONAN_USER, + DEFAULT_CONAN_USER_PWD) + +############################################################################### +# Public API ### +############################################################################### + +@dataclass +class ConanServer: + hostname: str + http_port: int + user: str + password: str + + def compose_url(self) -> str: + return f"http://{self.hostname}:{self.http_port}" + +@contextmanager +def launch_conan_test_server(server_dir: Path, http_port: int) -> Generator[ConanServer]: + """Launch a Conan server for testing.""" + connection_params = _create_conan_server_config(server_dir, http_port) + process = _launch_conan_server(server_dir) + _wait_for_server_start(connection_params) + try: + yield connection_params + finally: + _stop_process(process) + + +############################################################################### +# Implementation ### +############################################################################### + +_CONAN_SERVER_CONFIG = dedent(""" + [server] + jwt_secret: IJKhyoioUINMXCRTytrR + jwt_expire_minutes: 120 + + ssl_enabled: False + port: {http_port} + + host_name: localhost + + disk_storage_path: {disk_storage_path} + + [write_permissions] + */*@*/*: {default_user} + + [read_permissions] + */*@*/*: {default_user} + + [users] + {default_user}: {default_password} +""") + +def _create_conan_server_config(server_dir: Path, http_port: int) -> ConanServer: + ensure_dir_exists(server_dir) + server_config = server_dir / "server.conf" + server_storage_path = server_dir / "data" + server_config.write_text(_CONAN_SERVER_CONFIG.format( + http_port=http_port, + disk_storage_path=server_storage_path, + default_user=DEFAULT_CONAN_USER, + default_password=DEFAULT_CONAN_USER_PWD, + )) + return ConanServer( + hostname="localhost", + http_port=http_port, + user=DEFAULT_CONAN_USER, + password=DEFAULT_CONAN_USER_PWD + ) + +def _launch_conan_server(server_dir: Path) -> subprocess.Popen: + process = subprocess.Popen( + args=[ + "conan_server", + "--server_dir", + str(server_dir) + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=False, + ) + return process # Read the server output to ensure it"s started + +def _wait_for_server_start(conan_server: ConanServer) -> None: + for _ in range(10): + try: + response = requests.get(f"{conan_server.compose_url()}/v1/ping") + response.raise_for_status() + return + except requests.exceptions.RequestException: + time.sleep(0.1) + raise RuntimeError("Failed to start Conan server") + +def _stop_process(process: subprocess.Popen) -> None: + process.terminate() \ No newline at end of file diff --git a/src/tests/cpp_dev/dependency/conan/utils/test_env.py b/src/tests/cpp_dev/dependency/conan/utils/test_env.py new file mode 100644 index 0000000..0dbc59b --- /dev/null +++ b/src/tests/cpp_dev/dependency/conan/utils/test_env.py @@ -0,0 +1,33 @@ +# Copyright (c) 2024 Andi Hellmund. All rights reserved. + +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + +from pathlib import Path + +from cpp_dev.dependency.conan.command_wrapper import conan_list +from cpp_dev.dependency.conan.setup import CONAN_REMOTE +from cpp_dev.dependency.conan.types import \ + ConanPackageReferenceWithSemanticVersion +from tests.cpp_dev.dependency.conan.utils.server import \ + launch_conan_test_server + +from .env import ConanTestPackage, create_conan_test_env + +TEST_PACKAGES = [ + ConanTestPackage( + ref=ConanPackageReferenceWithSemanticVersion("dep/1.0.0@official/cppdev"), + dependencies=[], + cpp_standard="c++20", + ), + ConanTestPackage( + ref=ConanPackageReferenceWithSemanticVersion("test/1.0.0@official/cppdev"), + dependencies=[ConanPackageReferenceWithSemanticVersion("dep/1.0.0@official/cppdev")], + cpp_standard="c++20", + ), +] + +def test_create_conan_env(tmp_path: Path, unused_http_port: int) -> None: + with create_conan_test_env(tmp_path, unused_http_port, TEST_PACKAGES) as conan_env: + result = conan_list(CONAN_REMOTE, "test") + assert ConanPackageReferenceWithSemanticVersion("test/1.0.0@official/cppdev") in result \ No newline at end of file diff --git a/src/tests/cpp_dev/dependency/conan/utils/test_server.py b/src/tests/cpp_dev/dependency/conan/utils/test_server.py new file mode 100644 index 0000000..5752db1 --- /dev/null +++ b/src/tests/cpp_dev/dependency/conan/utils/test_server.py @@ -0,0 +1,16 @@ +# Copyright (c) 2024 Andi Hellmund. All rights reserved. + +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + +from pathlib import Path + +import requests + +from .server import launch_conan_test_server + + +def test_launch_conan_server(tmp_path: Path, unused_http_port: int) -> None: + with launch_conan_test_server(tmp_path / "server", unused_http_port) as conan_server: + response = requests.get(conan_server.compose_url() + "/v1/ping", timeout=2) + assert response.status_code == 200 \ No newline at end of file diff --git a/src/tests/cpp_dev/dependency/test_provider.py b/src/tests/cpp_dev/dependency/test_provider.py new file mode 100644 index 0000000..4ea617d --- /dev/null +++ b/src/tests/cpp_dev/dependency/test_provider.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024 Andi Hellmund. All rights reserved. + +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + +import pytest + +from cpp_dev.common.version import SemanticVersion +from cpp_dev.dependency.provider import DependencyIdentifier + + +def test_dependency_identifier_ok() -> None: + dep_str = "repo/name/1.2.3" + dep_id = DependencyIdentifier.from_str(dep_str) + + assert dep_id.repository == "repo" + assert dep_id.name == "name" + assert dep_id.version == SemanticVersion("1.2.3") + + assert str(dep_id) == dep_str + + +@pytest.mark.parametrize("dep_id_str", ["repo/name", "repo/name/1.2", "repo/name/1.2.3.4"]) +def test_dependency_identifier_fail(dep_id_str: str) -> None: + with pytest.raises(ValueError): # noqa: PT011 + DependencyIdentifier.from_str(dep_id_str) diff --git a/src/tests/cpp_dev/dependency/test_specifier.py b/src/tests/cpp_dev/dependency/test_specifier.py new file mode 100644 index 0000000..2143106 --- /dev/null +++ b/src/tests/cpp_dev/dependency/test_specifier.py @@ -0,0 +1,45 @@ +# Copyright (c) 2024 Andi Hellmund. All rights reserved. + +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + + +from cpp_dev.common.version import SemanticVersionWithOptionalParts +from cpp_dev.dependency.specifier import DependencySpecifier +from cpp_dev.dependency.types import DependencySpecifierParts, VersionSpecBound, VersionSpecBoundOperand + + +def test_dependency_specifier() -> None: + specifier = DependencySpecifier("official/cpd[>1.0.0]") + assert specifier.repository == "official" + assert specifier.name == "cpd" + assert isinstance(specifier.version_spec, list) + assert len(specifier.version_spec) == 1 + + assert specifier.version_spec[0] == VersionSpecBound( + operand=VersionSpecBoundOperand.GREATER_THAN, + version=SemanticVersionWithOptionalParts(1, 0, 0), + ) + + +def test_dependency_specifier_from_parts() -> None: + parts = DependencySpecifierParts( + "official", + "cpd", + [ + VersionSpecBound( + operand=VersionSpecBoundOperand.GREATER_THAN, + version=SemanticVersionWithOptionalParts(1, 0, 0), + ) + ], + ) + dependency = DependencySpecifier.from_parts(parts) + assert dependency.root == "official/cpd[>1.0.0]" + + +def test_dependency_specifier_parts_roundtrip() -> None: + specifier = DependencySpecifier("official/cpd[>1.0.0]") + roundtrip_specifier = DependencySpecifier.from_parts( + DependencySpecifierParts(specifier.repository, specifier.name, specifier.version_spec) + ) + assert specifier == roundtrip_specifier diff --git a/src/tests/cpp_dev/project/dependency/test_parser.py b/src/tests/cpp_dev/dependency/test_specifier_parser.py similarity index 71% rename from src/tests/cpp_dev/project/dependency/test_parser.py rename to src/tests/cpp_dev/dependency/test_specifier_parser.py index 5089416..f5514bb 100644 --- a/src/tests/cpp_dev/project/dependency/test_parser.py +++ b/src/tests/cpp_dev/dependency/test_specifier_parser.py @@ -5,29 +5,24 @@ import pytest -from cpp_dev.common.types import SemanticVersion -from cpp_dev.project.dependency.parser import DependencyParserError, parse_dependency_string -from cpp_dev.project.dependency.parts import ( - PackageDependencyParts, - SemanticVersionWithOptionalParts, - VersionSpecBound, - VersionSpecBoundOperand, -) +from cpp_dev.common.version import SemanticVersion, SemanticVersionWithOptionalParts +from cpp_dev.dependency.specifier_parser import DependencyParserError, parse_dependency_string +from cpp_dev.dependency.types import DependencySpecifierParts, VersionSpecBound, VersionSpecBoundOperand @pytest.mark.parametrize( ("dep_str", "expected"), [ - ("cpd", PackageDependencyParts(repository=None, name="cpd", version_spec="latest")), - ("repo/cpd", PackageDependencyParts(repository="repo", name="cpd", version_spec="latest")), - ("repo/cpd[latest]", PackageDependencyParts(repository="repo", name="cpd", version_spec="latest")), + ("cpd", DependencySpecifierParts(repository=None, name="cpd", version_spec="latest")), + ("repo/cpd", DependencySpecifierParts(repository="repo", name="cpd", version_spec="latest")), + ("repo/cpd[latest]", DependencySpecifierParts(repository="repo", name="cpd", version_spec="latest")), ( "cpd[1.2.3]", - PackageDependencyParts(repository=None, name="cpd", version_spec=SemanticVersion("1.2.3")), + DependencySpecifierParts(repository=None, name="cpd", version_spec=SemanticVersion("1.2.3")), ), ( "cpd[>1.0.0]", - PackageDependencyParts( + DependencySpecifierParts( repository=None, name="cpd", version_spec=[ @@ -40,7 +35,7 @@ ), ( "cpd[>=1.0]", - PackageDependencyParts( + DependencySpecifierParts( repository=None, name="cpd", version_spec=[ @@ -53,7 +48,7 @@ ), ( "cpd[<1]", - PackageDependencyParts( + DependencySpecifierParts( repository=None, name="cpd", version_spec=[ @@ -66,7 +61,7 @@ ), ( "cpd[>1,<=2.0]", - PackageDependencyParts( + DependencySpecifierParts( repository=None, name="cpd", version_spec=[ @@ -83,7 +78,7 @@ ), ], ) -def test_dependency_parser_ok(dep_str: str, expected: PackageDependencyParts) -> None: +def test_dependency_specifier_parser_ok(dep_str: str, expected: DependencySpecifierParts) -> None: assert parse_dependency_string(dep_str) == expected @@ -105,6 +100,6 @@ def test_dependency_parser_ok(dep_str: str, expected: PackageDependencyParts) -> "cpd[1.x.0]", ], ) -def test_dependency_parser_fail(dep_str: str) -> None: +def test_dependency_specifier_parser_fail(dep_str: str) -> None: with pytest.raises(DependencyParserError): parse_dependency_string(dep_str) diff --git a/src/tests/cpp_dev/project/dependency/test_types.py b/src/tests/cpp_dev/project/dependency/test_types.py deleted file mode 100644 index f4ef452..0000000 --- a/src/tests/cpp_dev/project/dependency/test_types.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) 2024 Andi Hellmund. All rights reserved. - -# This work is licensed under the terms of the BSD-3-Clause license. -# For a copy, see . - - -from cpp_dev.project.dependency.parts import ( - PackageDependencyParts, - SemanticVersionWithOptionalParts, - VersionSpecBound, - VersionSpecBoundOperand, -) -from cpp_dev.project.dependency.types import PackageDependency - - -def test_package_dependency() -> None: - parts = PackageDependency("official/cpd[>1.0.0]").parts - assert parts.repository == "official" - assert parts.name == "cpd" - assert isinstance(parts.version_spec, list) - assert len(parts.version_spec) == 1 - - assert parts.version_spec[0] == VersionSpecBound( - operand=VersionSpecBoundOperand.GREATER_THAN, - version=SemanticVersionWithOptionalParts(1, 0, 0), - ) - - -def test_package_dependency_from_parts() -> None: - parts = PackageDependencyParts( - "official", - "cpd", - [ - VersionSpecBound( - operand=VersionSpecBoundOperand.GREATER_THAN, - version=SemanticVersionWithOptionalParts(1, 0, 0), - ) - ], - ) - dependency = PackageDependency.from_parts(parts) - assert dependency.root == "official/cpd[>1.0.0]" - - -def test_package_dependency_parts_roundtrip() -> None: - dependency = PackageDependency("official/cpd[>1.0.0]") - roundtrip_dependency = PackageDependency.from_parts(dependency.parts) - assert dependency == roundtrip_dependency diff --git a/src/tests/cpp_dev/project/dependency/test_utils.py b/src/tests/cpp_dev/project/dependency/test_utils.py deleted file mode 100644 index 9620765..0000000 --- a/src/tests/cpp_dev/project/dependency/test_utils.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) 2024 Andi Hellmund. All rights reserved. - -# This work is licensed under the terms of the BSD-3-Clause license. -# For a copy, see . - -from collections.abc import Mapping -from unittest.mock import patch - -from cpp_dev.conan.types import ConanPackageReference -from cpp_dev.project.dependency.types import PackageDependency -from cpp_dev.project.dependency.utils import refine_package_dependencies - - -def conan_list_side_effect(_remote: str, name: str) -> Mapping[ConanPackageReference, dict]: - if name == "cpd": - return { - ConanPackageReference("cpd/1.0.0@official/cppdev"): {}, - ConanPackageReference("cpd/2.0.0@official/cppdev"): {}, - } - if name == "cpd2": - return { - ConanPackageReference("cpd2/3.0.0@official/cppdev"): {}, - ConanPackageReference("cpd2/3.0.0@official/cppdev"): {}, - } - if name == "other": - return { - ConanPackageReference("other/2.0.0@custom/cppdev"): {}, - } - return {} - - -def test_refine_package_dependencies() -> None: - with patch("cpp_dev.conan.package.conan_list", side_effect=conan_list_side_effect): - refined_deps = refine_package_dependencies( - [ - PackageDependency("cpd"), - PackageDependency("custom/other[latest]"), - PackageDependency("cpd2[3.0.0]"), - ] - ) - assert refined_deps == [ - PackageDependency("official/cpd[>=2.0.0]"), - PackageDependency("custom/other[>=2.0.0]"), - PackageDependency("official/cpd2[3.0.0]"), - ] diff --git a/src/tests/cpp_dev/project/test_config.py b/src/tests/cpp_dev/project/test_config.py index 50e755b..b024289 100644 --- a/src/tests/cpp_dev/project/test_config.py +++ b/src/tests/cpp_dev/project/test_config.py @@ -8,11 +8,17 @@ import pytest -from cpp_dev.common.types import SemanticVersion -from cpp_dev.project.config import create_project_config, load_project_config, update_dependencies -from cpp_dev.project.constants import compose_project_config_file -from cpp_dev.project.dependency.types import PackageDependency -from cpp_dev.project.types import DependencyType, ProjectConfig +from cpp_dev.common.version import SemanticVersion +from cpp_dev.dependency.specifier import DependencySpecifier +from cpp_dev.project.config import ( + DependencyType, + ProjectConfig, + create_project_config, + load_project_config, + update_dependencies, + validate_dependencies, +) +from cpp_dev.project.path_composition import compose_project_config_file @pytest.fixture @@ -20,13 +26,13 @@ def project_config() -> ProjectConfig: return ProjectConfig( name="test", version=SemanticVersion("0.1.0"), - std="c++17", + std="c++20", author="author", license="license", description="description", dependencies=[], - dev_dependencies=[PackageDependency("cpd")], - cpd_dependencies=[PackageDependency("cpd"), PackageDependency("cpd2")], + dev_dependencies=[DependencySpecifier("cpd")], + cpd_dependencies=[DependencySpecifier("cpd"), DependencySpecifier("cpd2")], ) @@ -53,12 +59,12 @@ def project_setup(tmp_path: Path, project_config: ProjectConfig) -> ProjectSetup @pytest.mark.parametrize( ("dep_type", "new_deps", "expected_deps", "unchanged_dep_types"), [ - ("runtime", [PackageDependency("cpd")], [PackageDependency("cpd")], ["dev", "cpd"]), - ("dev", [PackageDependency("cpd[2.0.0]")], [PackageDependency("cpd[2.0.0]")], ["runtime", "cpd"]), + ("runtime", [DependencySpecifier("cpd")], [DependencySpecifier("cpd")], ["dev", "cpd"]), + ("dev", [DependencySpecifier("cpd[2.0.0]")], [DependencySpecifier("cpd[2.0.0]")], ["runtime", "cpd"]), ( "cpd", - [PackageDependency("cpd"), PackageDependency("cpd3")], - [PackageDependency("cpd"), PackageDependency("cpd2"), PackageDependency("cpd3")], + [DependencySpecifier("cpd"), DependencySpecifier("cpd3")], + [DependencySpecifier("cpd"), DependencySpecifier("cpd2"), DependencySpecifier("cpd3")], ["runtime", "dev"], ), ], @@ -66,8 +72,8 @@ def project_setup(tmp_path: Path, project_config: ProjectConfig) -> ProjectSetup def test_update_dependencies( project_setup: ProjectSetup, dep_type: DependencyType, - new_deps: list[PackageDependency], - expected_deps: list[PackageDependency], + new_deps: list[DependencySpecifier], + expected_deps: list[DependencySpecifier], unchanged_dep_types: list[DependencyType], ) -> None: new_config = update_dependencies(project_setup.project_config, new_deps, dep_type) @@ -78,3 +84,34 @@ def test_update_dependencies( assert new_config.get_dependencies(unchanged_type) == project_setup.project_config.get_dependencies( unchanged_type ) + + +def test_validate_dependencies_valid() -> None: + valid_config = ProjectConfig( + name="test", + version=SemanticVersion("0.1.0"), + std="c++20", + author="author", + license="license", + description="description", + dependencies=[DependencySpecifier("dep1"), DependencySpecifier("dep2")], + dev_dependencies=[DependencySpecifier("dev1")], + cpd_dependencies=[DependencySpecifier("cpd1")], + ) + validate_dependencies(valid_config) # Should not raise an exception + + +def test_validate_dependencies_invalid() -> None: + invalid_config = ProjectConfig( + name="test", + version=SemanticVersion("0.1.0"), + std="c++20", + author="author", + license="license", + description="description", + dependencies=[DependencySpecifier("dep1")], + dev_dependencies=[DependencySpecifier("dep1")], # Duplicate dependency + cpd_dependencies=[DependencySpecifier("cpd1")], + ) + with pytest.raises(ValueError, match="Dependency 'dep1' is defined multiple times."): + validate_dependencies(invalid_config) diff --git a/src/tests/cpp_dev/project/test_core.py b/src/tests/cpp_dev/project/test_core.py new file mode 100644 index 0000000..22a0000 --- /dev/null +++ b/src/tests/cpp_dev/project/test_core.py @@ -0,0 +1,100 @@ +# Copyright (c) 2024 Andi Hellmund. All rights reserved. + +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + + +from collections.abc import Mapping +from pathlib import Path + +import pytest + +from cpp_dev.common.version import SemanticVersion +from cpp_dev.dependency.conan.types import ConanPackageReferenceWithSemanticVersion +from cpp_dev.dependency.provider import DependencyIdentifier, DependencyProvider +from cpp_dev.dependency.specifier import DependencySpecifier +from cpp_dev.project.config import ProjectConfig, load_project_config +from cpp_dev.project.core import _refine_package_dependencies, setup_project +from cpp_dev.project.lockfile import load_lock_file +from cpp_dev.project.path_composition import compose_project_config_file, compose_project_lock_file +from tests.cpp_dev.project.utils.artificial_dependency_provider import ArtificialDependencyProvider, Dependency + + +@pytest.fixture +def dep_provider() -> DependencyProvider: + return ArtificialDependencyProvider( + dependencies=[ + Dependency(id=DependencyIdentifier.from_str("official/llvm/1.0.0"), cpp_standard="c++20", deps=[]), + Dependency(id=DependencyIdentifier.from_str("official/gtest/1.0.0"), cpp_standard="c++20", deps=[]), + ] + ) + + +def test_setup_project(tmp_path: Path, dep_provider: DependencyProvider) -> None: + project_config = ProjectConfig( + name="test_package", + version=SemanticVersion("1.0.0"), + std="c++20", + author="author", + license="license", + description="description", + dependencies=[], + dev_dependencies=[], + cpd_dependencies=[], + ) + + project = setup_project(project_config, dep_provider, parent_dir=tmp_path) + + assert project.project_dir.exists() + assert project.project_dir == tmp_path / project_config.name + + assert compose_project_config_file(project.project_dir).exists() + + config = load_project_config(project.project_dir) + assert config == project_config + + assert len(config.dependencies) == 0 + assert len(config.dev_dependencies) == 0 + assert len(config.cpd_dependencies) == 0 + + assert compose_project_lock_file(project.project_dir).exists() + locked_dependencies = load_lock_file(project.project_dir) + assert len(locked_dependencies.packages) == 0 + + assert (project.project_dir / "include" / project_config.name / f"{project_config.name}.hpp").exists() + assert (project.project_dir / "src" / f"{project_config.name}.cpp").exists() + assert (project.project_dir / "src" / f"{project_config.name}.test.cpp").exists() + + +def conan_list_side_effect(_remote: str, name: str) -> Mapping[ConanPackageReferenceWithSemanticVersion, dict]: + if name == "cpd": + return { + ConanPackageReferenceWithSemanticVersion("cpd/1.0.0@official/cppdev"): {}, + ConanPackageReferenceWithSemanticVersion("cpd/2.0.0@official/cppdev"): {}, + } + if name == "cpd2": + return { + ConanPackageReferenceWithSemanticVersion("cpd2/3.0.0@official/cppdev"): {}, + ConanPackageReferenceWithSemanticVersion("cpd2/3.0.0@official/cppdev"): {}, + } + if name == "other": + return { + ConanPackageReferenceWithSemanticVersion("other/2.0.0@custom/cppdev"): {}, + } + return {} + + +def test_refine_package_dependencies(dep_provider: DependencyProvider) -> None: + refined_deps = _refine_package_dependencies( + dep_provider, + [ + DependencySpecifier("llvm"), + DependencySpecifier("official/llvm[latest]"), + DependencySpecifier("gtest[3.0.0]"), + ], + ) + assert refined_deps == [ + DependencySpecifier("official/llvm[>=1.0.0]"), + DependencySpecifier("official/llvm[>=1.0.0]"), + DependencySpecifier("official/gtest[3.0.0]"), + ] diff --git a/src/tests/cpp_dev/project/test_lockfile.py b/src/tests/cpp_dev/project/test_lockfile.py index 007fea5..4d22913 100644 --- a/src/tests/cpp_dev/project/test_lockfile.py +++ b/src/tests/cpp_dev/project/test_lockfile.py @@ -8,8 +8,7 @@ import pytest -from cpp_dev.common.types import SemanticVersion -from cpp_dev.project.constants import compose_project_lock_file +from cpp_dev.common.version import SemanticVersion from cpp_dev.project.lockfile import ( LockedDependencies, LockedPackageDependency, @@ -17,6 +16,7 @@ load_lock_file, store_lock_file, ) +from cpp_dev.project.path_composition import compose_project_lock_file @pytest.fixture diff --git a/src/tests/cpp_dev/project/test_management.py b/src/tests/cpp_dev/project/test_management.py deleted file mode 100644 index a3a3978..0000000 --- a/src/tests/cpp_dev/project/test_management.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (c) 2024 Andi Hellmund. All rights reserved. - -# This work is licensed under the terms of the BSD-3-Clause license. -# For a copy, see . - - -from collections.abc import Mapping -from pathlib import Path -from unittest.mock import patch - -from cpp_dev.conan.types import ConanPackageReference -from cpp_dev.project.config import load_project_config -from cpp_dev.project.constants import compose_project_config_file, compose_project_lock_file -from cpp_dev.project.lockfile import load_lock_file -from cpp_dev.project.management import setup_project -from cpp_dev.project.types import ProjectConfig, SemanticVersion - - -def conan_list_side_effect(_remote: str, name: str) -> Mapping[ConanPackageReference, dict]: - if name == "llvm": - return { - ConanPackageReference("llvm/1.0.0@official/cppdev"): {}, - } - if name == "gtest": - return { - ConanPackageReference("gtest/1.0.0@official/cppdev"): {}, - } - return {} - - -def test_setup_project(tmp_path: Path) -> None: - with patch("cpp_dev.conan.package.conan_list", side_effect=conan_list_side_effect): - project_config = ProjectConfig( - name="test_package", - version=SemanticVersion("1.0.0"), - std="c++17", - author="author", - license="license", - description="description", - dependencies=[], - dev_dependencies=[], - cpd_dependencies=[], - ) - - project_dir = setup_project(project_config, parent_dir=tmp_path) - - assert project_dir.exists() - assert project_dir == tmp_path / project_config.name - - assert compose_project_config_file(project_dir).exists() - - config = load_project_config(project_dir) - assert config == project_config - - assert len(config.dependencies) == 0 - assert len(config.dev_dependencies) == 0 - assert len(config.cpd_dependencies) == 0 - - assert compose_project_lock_file(project_dir).exists() - locked_dependencies = load_lock_file(project_dir) - assert len(locked_dependencies.packages) == 0 - - assert (project_dir / "include" / project_config.name / f"{project_config.name}.hpp").exists() - assert (project_dir / "src" / f"{project_config.name}.cpp").exists() - assert (project_dir / "src" / f"{project_config.name}.test.cpp").exists() diff --git a/src/tests/cpp_dev/project/test_constants.py b/src/tests/cpp_dev/project/test_path_composition.py similarity index 96% rename from src/tests/cpp_dev/project/test_constants.py rename to src/tests/cpp_dev/project/test_path_composition.py index 6bc4d67..3eb17ab 100644 --- a/src/tests/cpp_dev/project/test_constants.py +++ b/src/tests/cpp_dev/project/test_path_composition.py @@ -7,7 +7,7 @@ import pytest -from cpp_dev.project.constants import ( +from cpp_dev.project.path_composition import ( compose_include_file, compose_project_config_file, compose_project_lock_file, diff --git a/src/tests/cpp_dev/project/utils/__init__.py b/src/tests/cpp_dev/project/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/cpp_dev/project/utils/artificial_dependency_provider.py b/src/tests/cpp_dev/project/utils/artificial_dependency_provider.py new file mode 100644 index 0000000..8c9f53c --- /dev/null +++ b/src/tests/cpp_dev/project/utils/artificial_dependency_provider.py @@ -0,0 +1,65 @@ +# Copyright (c) 2024 Andi Hellmund. All rights reserved. + +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + +from __future__ import annotations + +from dataclasses import dataclass + +from cpp_dev.common.types import CppStandard +from cpp_dev.common.version import SemanticVersion +from cpp_dev.dependency.provider import DependencyIdentifier, DependencyProvider +from cpp_dev.dependency.specifier import DependencySpecifier + +############################################################################### +# Public API ### +############################################################################### + + +@dataclass +class Dependency: + """Attributes of a dependency.""" + + id: DependencyIdentifier + cpp_standard: CppStandard + deps: list[DependencyIdentifier] + + +class ArtificialDependencyProvider(DependencyProvider): + """Test implementation of DependencyProvider for testing purposes.""" + + def __init__(self, dependencies: list[Dependency]) -> None: + self._dependencies = {d.id: d for d in dependencies} + _assert_dependencies_are_complete(self._dependencies) + + def fetch_versions(self, repository: str, name: str) -> list[SemanticVersion]: + """Fetch available versions for a dependency represented by repository and name.""" + available_versions = [ + dep.id.version + for dep in self._dependencies.values() + if dep.id.repository == repository and dep.id.name == name + ] + if len(available_versions) == 0: + raise ValueError(f"No available versions for package {name} at repository {repository}.") + return sorted(available_versions, reverse=True) + + def collect_dependency_hull(self, _deps: list[DependencySpecifier]) -> set[DependencyIdentifier]: + """Collect the dependency hull for a list of dependencies.""" + return set() + + def install_dependencies(self, _deps: list[DependencySpecifier]) -> list[DependencySpecifier]: + """Install the dependencies represented by the list of dependency specifiers.""" + return [] + + +############################################################################### +# Implementation ### +############################################################################### + + +def _assert_dependencies_are_complete(dependencies: dict[DependencyIdentifier, Dependency]) -> None: + for dep_id, dependency in dependencies.items(): + for transitive_dep_id in dependency.deps: + if str(transitive_dep_id) not in dependencies: + raise ValueError(f"Missing dependency: {transitive_dep_id} for {dep_id}") diff --git a/src/tests/cpp_dev/project/utils/test_artificial_dependency_provider.py b/src/tests/cpp_dev/project/utils/test_artificial_dependency_provider.py new file mode 100644 index 0000000..440730a --- /dev/null +++ b/src/tests/cpp_dev/project/utils/test_artificial_dependency_provider.py @@ -0,0 +1,24 @@ +# Copyright (c) 2024 Andi Hellmund. All rights reserved. + +# This work is licensed under the terms of the BSD-3-Clause license. +# For a copy, see . + + +from cpp_dev.common.version import SemanticVersion +from cpp_dev.dependency.provider import DependencyIdentifier +from tests.cpp_dev.project.utils.artificial_dependency_provider import ArtificialDependencyProvider, Dependency + + +def test_available_versions() -> None: + provider = ArtificialDependencyProvider( + [ + Dependency(id=DependencyIdentifier.from_str("official/gtest/1.0.0"), cpp_standard="c++20", deps=[]), + Dependency(id=DependencyIdentifier.from_str("official/gtest/1.10.0"), cpp_standard="c++20", deps=[]), + Dependency(id=DependencyIdentifier.from_str("custom/gtest/1.11.0"), cpp_standard="c++20", deps=[]), + ] + ) + assert provider.fetch_versions("official", "gtest") == [ + SemanticVersion("1.10.0"), + SemanticVersion("1.0.0"), + ] + assert provider.fetch_versions("custom", "gtest") == [SemanticVersion("1.11.0")] diff --git a/src/tests/cpp_dev/tool/test_init.py b/src/tests/cpp_dev/tool/test_init.py index 38347b3..4ad97bc 100644 --- a/src/tests/cpp_dev/tool/test_init.py +++ b/src/tests/cpp_dev/tool/test_init.py @@ -9,8 +9,8 @@ import pytest -from cpp_dev.common.types import SemanticVersion from cpp_dev.common.utils import updated_env +from cpp_dev.common.version import SemanticVersion from cpp_dev.tool.init import assure_cpd_is_initialized, get_conan_home_dir, get_cpd_dir, initialize_cpd, update_cpd from cpp_dev.tool.version import get_cpd_version_from_code, write_version_file @@ -18,7 +18,7 @@ @pytest.fixture(autouse=True) def conan_remote_login() -> object: """Mock the conan user and password setting function.""" - with patch("cpp_dev.conan.setup.conan_remote_login", return_value=None) as mock: + with patch("cpp_dev.dependency.conan.setup.conan_remote_login", return_value=None) as mock: yield mock diff --git a/src/tests/cpp_dev/tool/test_version.py b/src/tests/cpp_dev/tool/test_version.py index 17409f6..f5c2508 100644 --- a/src/tests/cpp_dev/tool/test_version.py +++ b/src/tests/cpp_dev/tool/test_version.py @@ -6,7 +6,7 @@ from pathlib import Path -from cpp_dev.common.types import SemanticVersion +from cpp_dev.common.version import SemanticVersion from cpp_dev.tool.version import read_version_file, write_version_file diff --git a/uv.lock b/uv.lock index 12cfd0a..fdc4dd3 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -requires-python = ">=3.13" +requires-python = ">=3.12" [[package]] name = "annotated-types" @@ -39,26 +39,37 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, - { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, - { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, - { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, - { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, - { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, - { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, - { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, - { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, - { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, - { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, - { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, - { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, - { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, - { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, - { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, ] [[package]] @@ -109,30 +120,40 @@ sdist = { url = "https://files.pythonhosted.org/packages/97/ee/79accc76c89254269 [[package]] name = "coverage" -version = "7.6.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5b/d2/c25011f4d036cf7e8acbbee07a8e09e9018390aee25ba085596c4b83d510/coverage-7.6.9.tar.gz", hash = "sha256:4a8d8977b0c6ef5aeadcb644da9e69ae0dcfe66ec7f368c89c72e058bd71164d", size = 801710 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/26/9abab6539d2191dbda2ce8c97b67d74cbfc966cc5b25abb880ffc7c459bc/coverage-7.6.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:899b8cd4781c400454f2f64f7776a5d87bbd7b3e7f7bda0cb18f857bb1334664", size = 207356 }, - { url = "https://files.pythonhosted.org/packages/44/da/d49f19402240c93453f606e660a6676a2a1fbbaa6870cc23207790aa9697/coverage-7.6.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:61f70dc68bd36810972e55bbbe83674ea073dd1dcc121040a08cdf3416c5349c", size = 207614 }, - { url = "https://files.pythonhosted.org/packages/da/e6/93bb9bf85497816082ec8da6124c25efa2052bd4c887dd3b317b91990c9e/coverage-7.6.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a289d23d4c46f1a82d5db4abeb40b9b5be91731ee19a379d15790e53031c014", size = 240129 }, - { url = "https://files.pythonhosted.org/packages/df/65/6a824b9406fe066835c1274a9949e06f084d3e605eb1a602727a27ec2fe3/coverage-7.6.9-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e216d8044a356fc0337c7a2a0536d6de07888d7bcda76febcb8adc50bdbbd00", size = 237276 }, - { url = "https://files.pythonhosted.org/packages/9f/79/6c7a800913a9dd23ac8c8da133ebb556771a5a3d4df36b46767b1baffd35/coverage-7.6.9-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c026eb44f744acaa2bda7493dad903aa5bf5fc4f2554293a798d5606710055d", size = 239267 }, - { url = "https://files.pythonhosted.org/packages/57/e7/834d530293fdc8a63ba8ff70033d5182022e569eceb9aec7fc716b678a39/coverage-7.6.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77363e8425325384f9d49272c54045bbed2f478e9dd698dbc65dbc37860eb0a", size = 238887 }, - { url = "https://files.pythonhosted.org/packages/15/05/ec9d6080852984f7163c96984444e7cd98b338fd045b191064f943ee1c08/coverage-7.6.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:777abfab476cf83b5177b84d7486497e034eb9eaea0d746ce0c1268c71652077", size = 236970 }, - { url = "https://files.pythonhosted.org/packages/0a/d8/775937670b93156aec29f694ce37f56214ed7597e1a75b4083ee4c32121c/coverage-7.6.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:447af20e25fdbe16f26e84eb714ba21d98868705cb138252d28bc400381f6ffb", size = 238831 }, - { url = "https://files.pythonhosted.org/packages/f4/58/88551cb7fdd5ec98cb6044e8814e38583436b14040a5ece15349c44c8f7c/coverage-7.6.9-cp313-cp313-win32.whl", hash = "sha256:d872ec5aeb086cbea771c573600d47944eea2dcba8be5f3ee649bfe3cb8dc9ba", size = 210000 }, - { url = "https://files.pythonhosted.org/packages/b7/12/cfbf49b95120872785ff8d56ab1c7fe3970a65e35010c311d7dd35c5fd00/coverage-7.6.9-cp313-cp313-win_amd64.whl", hash = "sha256:fd1213c86e48dfdc5a0cc676551db467495a95a662d2396ecd58e719191446e1", size = 210753 }, - { url = "https://files.pythonhosted.org/packages/7c/68/c1cb31445599b04bde21cbbaa6d21b47c5823cdfef99eae470dfce49c35a/coverage-7.6.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9e7484d286cd5a43744e5f47b0b3fb457865baf07bafc6bee91896364e1419", size = 208091 }, - { url = "https://files.pythonhosted.org/packages/11/73/84b02c6b19c4a11eb2d5b5eabe926fb26c21c080e0852f5e5a4f01165f9e/coverage-7.6.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1cf0872ee455c03e5674b5bca5e3e68e159379c1af0903e89f5eba9ccc3a", size = 208369 }, - { url = "https://files.pythonhosted.org/packages/de/e0/ae5d878b72ff26df2e994a5c5b1c1f6a7507d976b23beecb1ed4c85411ef/coverage-7.6.9-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d10e07aa2b91835d6abec555ec8b2733347956991901eea6ffac295f83a30e4", size = 251089 }, - { url = "https://files.pythonhosted.org/packages/ab/9c/0aaac011aef95a93ef3cb2fba3fde30bc7e68a6635199ed469b1f5ea355a/coverage-7.6.9-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:13a9e2d3ee855db3dd6ea1ba5203316a1b1fd8eaeffc37c5b54987e61e4194ae", size = 246806 }, - { url = "https://files.pythonhosted.org/packages/f8/19/4d5d3ae66938a7dcb2f58cef3fa5386f838f469575b0bb568c8cc9e3a33d/coverage-7.6.9-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c38bf15a40ccf5619fa2fe8f26106c7e8e080d7760aeccb3722664c8656b030", size = 249164 }, - { url = "https://files.pythonhosted.org/packages/b3/0b/4ee8a7821f682af9ad440ae3c1e379da89a998883271f088102d7ca2473d/coverage-7.6.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d5275455b3e4627c8e7154feaf7ee0743c2e7af82f6e3b561967b1cca755a0be", size = 248642 }, - { url = "https://files.pythonhosted.org/packages/8a/12/36ff1d52be18a16b4700f561852e7afd8df56363a5edcfb04cf26a0e19e0/coverage-7.6.9-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8f8770dfc6e2c6a2d4569f411015c8d751c980d17a14b0530da2d7f27ffdd88e", size = 246516 }, - { url = "https://files.pythonhosted.org/packages/43/d0/8e258f6c3a527c1655602f4f576215e055ac704de2d101710a71a2affac2/coverage-7.6.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8d2dfa71665a29b153a9681edb1c8d9c1ea50dfc2375fb4dac99ea7e21a0bcd9", size = 247783 }, - { url = "https://files.pythonhosted.org/packages/a9/0d/1e4a48d289429d38aae3babdfcadbf35ca36bdcf3efc8f09b550a845bdb5/coverage-7.6.9-cp313-cp313t-win32.whl", hash = "sha256:5e6b86b5847a016d0fbd31ffe1001b63355ed309651851295315031ea7eb5a9b", size = 210646 }, - { url = "https://files.pythonhosted.org/packages/26/74/b0729f196f328ac55e42b1e22ec2f16d8bcafe4b8158a26ec9f1cdd1d93e/coverage-7.6.9-cp313-cp313t-win_amd64.whl", hash = "sha256:97ddc94d46088304772d21b060041c97fc16bdda13c6c7f9d8fcd8d5ae0d8611", size = 211815 }, +version = "7.6.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/ba/ac14d281f80aab516275012e8875991bb06203957aa1e19950139238d658/coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23", size = 803868 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/77/19d09ea06f92fdf0487499283b1b7af06bc422ea94534c8fe3a4cd023641/coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853", size = 208281 }, + { url = "https://files.pythonhosted.org/packages/b6/67/5479b9f2f99fcfb49c0d5cf61912a5255ef80b6e80a3cddba39c38146cf4/coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078", size = 208514 }, + { url = "https://files.pythonhosted.org/packages/15/d1/febf59030ce1c83b7331c3546d7317e5120c5966471727aa7ac157729c4b/coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0", size = 241537 }, + { url = "https://files.pythonhosted.org/packages/4b/7e/5ac4c90192130e7cf8b63153fe620c8bfd9068f89a6d9b5f26f1550f7a26/coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50", size = 238572 }, + { url = "https://files.pythonhosted.org/packages/dc/03/0334a79b26ecf59958f2fe9dd1f5ab3e2f88db876f5071933de39af09647/coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022", size = 240639 }, + { url = "https://files.pythonhosted.org/packages/d7/45/8a707f23c202208d7b286d78ad6233f50dcf929319b664b6cc18a03c1aae/coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b", size = 240072 }, + { url = "https://files.pythonhosted.org/packages/66/02/603ce0ac2d02bc7b393279ef618940b4a0535b0868ee791140bda9ecfa40/coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0", size = 238386 }, + { url = "https://files.pythonhosted.org/packages/04/62/4e6887e9be060f5d18f1dd58c2838b2d9646faf353232dec4e2d4b1c8644/coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852", size = 240054 }, + { url = "https://files.pythonhosted.org/packages/5c/74/83ae4151c170d8bd071924f212add22a0e62a7fe2b149edf016aeecad17c/coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359", size = 210904 }, + { url = "https://files.pythonhosted.org/packages/c3/54/de0893186a221478f5880283119fc40483bc460b27c4c71d1b8bba3474b9/coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247", size = 211692 }, + { url = "https://files.pythonhosted.org/packages/25/6d/31883d78865529257bf847df5789e2ae80e99de8a460c3453dbfbe0db069/coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9", size = 208308 }, + { url = "https://files.pythonhosted.org/packages/70/22/3f2b129cc08de00c83b0ad6252e034320946abfc3e4235c009e57cfeee05/coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b", size = 208565 }, + { url = "https://files.pythonhosted.org/packages/97/0a/d89bc2d1cc61d3a8dfe9e9d75217b2be85f6c73ebf1b9e3c2f4e797f4531/coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690", size = 241083 }, + { url = "https://files.pythonhosted.org/packages/4c/81/6d64b88a00c7a7aaed3a657b8eaa0931f37a6395fcef61e53ff742b49c97/coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18", size = 238235 }, + { url = "https://files.pythonhosted.org/packages/9a/0b/7797d4193f5adb4b837207ed87fecf5fc38f7cc612b369a8e8e12d9fa114/coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c", size = 240220 }, + { url = "https://files.pythonhosted.org/packages/65/4d/6f83ca1bddcf8e51bf8ff71572f39a1c73c34cf50e752a952c34f24d0a60/coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd", size = 239847 }, + { url = "https://files.pythonhosted.org/packages/30/9d/2470df6aa146aff4c65fee0f87f58d2164a67533c771c9cc12ffcdb865d5/coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e", size = 237922 }, + { url = "https://files.pythonhosted.org/packages/08/dd/723fef5d901e6a89f2507094db66c091449c8ba03272861eaefa773ad95c/coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694", size = 239783 }, + { url = "https://files.pythonhosted.org/packages/3d/f7/64d3298b2baf261cb35466000628706ce20a82d42faf9b771af447cd2b76/coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6", size = 210965 }, + { url = "https://files.pythonhosted.org/packages/d5/58/ec43499a7fc681212fe7742fe90b2bc361cdb72e3181ace1604247a5b24d/coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e", size = 211719 }, + { url = "https://files.pythonhosted.org/packages/ab/c9/f2857a135bcff4330c1e90e7d03446b036b2363d4ad37eb5e3a47bbac8a6/coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe", size = 209050 }, + { url = "https://files.pythonhosted.org/packages/aa/b3/f840e5bd777d8433caa9e4a1eb20503495709f697341ac1a8ee6a3c906ad/coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273", size = 209321 }, + { url = "https://files.pythonhosted.org/packages/85/7d/125a5362180fcc1c03d91850fc020f3831d5cda09319522bcfa6b2b70be7/coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8", size = 252039 }, + { url = "https://files.pythonhosted.org/packages/a9/9c/4358bf3c74baf1f9bddd2baf3756b54c07f2cfd2535f0a47f1e7757e54b3/coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098", size = 247758 }, + { url = "https://files.pythonhosted.org/packages/cf/c7/de3eb6fc5263b26fab5cda3de7a0f80e317597a4bad4781859f72885f300/coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb", size = 250119 }, + { url = "https://files.pythonhosted.org/packages/3e/e6/43de91f8ba2ec9140c6a4af1102141712949903dc732cf739167cfa7a3bc/coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0", size = 249597 }, + { url = "https://files.pythonhosted.org/packages/08/40/61158b5499aa2adf9e37bc6d0117e8f6788625b283d51e7e0c53cf340530/coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf", size = 247473 }, + { url = "https://files.pythonhosted.org/packages/50/69/b3f2416725621e9f112e74e8470793d5b5995f146f596f133678a633b77e/coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2", size = 248737 }, + { url = "https://files.pythonhosted.org/packages/3c/6e/fe899fb937657db6df31cc3e61c6968cb56d36d7326361847440a430152e/coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312", size = 211611 }, + { url = "https://files.pythonhosted.org/packages/1c/55/52f5e66142a9d7bc93a15192eba7a78513d2abf6b3558d77b4ca32f5f424/coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d", size = 212781 }, ] [[package]] @@ -260,11 +281,11 @@ wheels = [ [[package]] name = "importlib-resources" -version = "6.4.5" +version = "6.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/be/f3e8c6081b684f176b761e6a2fef02a0be939740ed6f54109a2951d806f3/importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065", size = 43372 } +sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693 } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/6a/4604f9ae2fa62ef47b9de2fa5ad599589d28c9fd1d335f32759813dfa91e/importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717", size = 36115 }, + { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461 }, ] [[package]] @@ -311,6 +332,16 @@ version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } wheels = [ + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, @@ -344,20 +375,27 @@ wheels = [ [[package]] name = "mypy" -version = "1.14.0" +version = "1.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8c/7b/08046ef9330735f536a09a2e31b00f42bccdb2795dcd979636ba43bb2d63/mypy-1.14.0.tar.gz", hash = "sha256:822dbd184d4a9804df5a7d5335a68cf7662930e70b8c1bc976645d1509f9a9d6", size = 3215684 } +sdist = { url = "https://files.pythonhosted.org/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6", size = 3216051 } wheels = [ - { url = "https://files.pythonhosted.org/packages/13/33/8380efd0ebdfdfac7fc0bf065f03a049800ca1e6c296ec1afc634340d992/mypy-1.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9f6f4c0b27401d14c483c622bc5105eff3911634d576bbdf6695b9a7c1ba741", size = 11251509 }, - { url = "https://files.pythonhosted.org/packages/15/6d/4e1c21c60fee11af7d8e4f2902a29886d1387d6a836be16229eb3982a963/mypy-1.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b2280cedcb312c7a79f5001ae5325582d0d339bce684e4a529069d0e7ca1e7", size = 10244282 }, - { url = "https://files.pythonhosted.org/packages/8b/cf/7a8ae5c0161edae15d25c2c67c68ce8b150cbdc45aefc13a8be271ee80b2/mypy-1.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:342de51c48bab326bfc77ce056ba08c076d82ce4f5a86621f972ed39970f94d8", size = 12867676 }, - { url = "https://files.pythonhosted.org/packages/9c/d0/71f7bbdcc7cfd0f2892db5b13b1e8857673f2cc9e0c30e3e4340523dc186/mypy-1.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:00df23b42e533e02a6f0055e54de9a6ed491cd8b7ea738647364fd3a39ea7efc", size = 12964189 }, - { url = "https://files.pythonhosted.org/packages/a7/40/fb4ad65d6d5f8c51396ecf6305ec0269b66013a5bf02d0e9528053640b4a/mypy-1.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:e8c8387e5d9dff80e7daf961df357c80e694e942d9755f3ad77d69b0957b8e3f", size = 9888247 }, - { url = "https://files.pythonhosted.org/packages/39/32/0214608af400cdf8f5102144bb8af10d880675c65ed0b58f7e0e77175d50/mypy-1.14.0-py3-none-any.whl", hash = "sha256:2238d7f93fc4027ed1efc944507683df3ba406445a2b6c96e79666a045aadfab", size = 2752803 }, + { url = "https://files.pythonhosted.org/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14", size = 11266668 }, + { url = "https://files.pythonhosted.org/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9", size = 10254060 }, + { url = "https://files.pythonhosted.org/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11", size = 11933167 }, + { url = "https://files.pythonhosted.org/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e", size = 12864341 }, + { url = "https://files.pythonhosted.org/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89", size = 12972991 }, + { url = "https://files.pythonhosted.org/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b", size = 9879016 }, + { url = "https://files.pythonhosted.org/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255", size = 11252097 }, + { url = "https://files.pythonhosted.org/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34", size = 10239728 }, + { url = "https://files.pythonhosted.org/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a", size = 11924965 }, + { url = "https://files.pythonhosted.org/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9", size = 12867660 }, + { url = "https://files.pythonhosted.org/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd", size = 12969198 }, + { url = "https://files.pythonhosted.org/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107", size = 9885276 }, + { url = "https://files.pythonhosted.org/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1", size = 2752905 }, ] [[package]] @@ -470,6 +508,20 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, + { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, + { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, + { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, + { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, + { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, + { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, + { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, + { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, + { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, + { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, + { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, + { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, + { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, @@ -550,6 +602,15 @@ version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, @@ -578,36 +639,36 @@ wheels = [ [[package]] name = "ruff" -version = "0.8.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/34/37/9c02181ef38d55b77d97c68b78e705fd14c0de0e5d085202bb2b52ce5be9/ruff-0.8.4.tar.gz", hash = "sha256:0d5f89f254836799af1615798caa5f80b7f935d7a670fad66c5007928e57ace8", size = 3402103 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/67/f480bf2f2723b2e49af38ed2be75ccdb2798fca7d56279b585c8f553aaab/ruff-0.8.4-py3-none-linux_armv6l.whl", hash = "sha256:58072f0c06080276804c6a4e21a9045a706584a958e644353603d36ca1eb8a60", size = 10546415 }, - { url = "https://files.pythonhosted.org/packages/eb/7a/5aba20312c73f1ce61814e520d1920edf68ca3b9c507bd84d8546a8ecaa8/ruff-0.8.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ffb60904651c00a1e0b8df594591770018a0f04587f7deeb3838344fe3adabac", size = 10346113 }, - { url = "https://files.pythonhosted.org/packages/76/f4/c41de22b3728486f0aa95383a44c42657b2db4062f3234ca36fc8cf52d8b/ruff-0.8.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ddf5d654ac0d44389f6bf05cee4caeefc3132a64b58ea46738111d687352296", size = 9943564 }, - { url = "https://files.pythonhosted.org/packages/0e/f0/afa0d2191af495ac82d4cbbfd7a94e3df6f62a04ca412033e073b871fc6d/ruff-0.8.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e248b1f0fa2749edd3350a2a342b67b43a2627434c059a063418e3d375cfe643", size = 10805522 }, - { url = "https://files.pythonhosted.org/packages/12/57/5d1e9a0fd0c228e663894e8e3a8e7063e5ee90f8e8e60cf2085f362bfa1a/ruff-0.8.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf197b98ed86e417412ee3b6c893f44c8864f816451441483253d5ff22c0e81e", size = 10306763 }, - { url = "https://files.pythonhosted.org/packages/04/df/f069fdb02e408be8aac6853583572a2873f87f866fe8515de65873caf6b8/ruff-0.8.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c41319b85faa3aadd4d30cb1cffdd9ac6b89704ff79f7664b853785b48eccdf3", size = 11359574 }, - { url = "https://files.pythonhosted.org/packages/d3/04/37c27494cd02e4a8315680debfc6dfabcb97e597c07cce0044db1f9dfbe2/ruff-0.8.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:9f8402b7c4f96463f135e936d9ab77b65711fcd5d72e5d67597b543bbb43cf3f", size = 12094851 }, - { url = "https://files.pythonhosted.org/packages/81/b1/c5d7fb68506cab9832d208d03ea4668da9a9887a4a392f4f328b1bf734ad/ruff-0.8.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4e56b3baa9c23d324ead112a4fdf20db9a3f8f29eeabff1355114dd96014604", size = 11655539 }, - { url = "https://files.pythonhosted.org/packages/ef/38/8f8f2c8898dc8a7a49bc340cf6f00226917f0f5cb489e37075bcb2ce3671/ruff-0.8.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:736272574e97157f7edbbb43b1d046125fce9e7d8d583d5d65d0c9bf2c15addf", size = 12912805 }, - { url = "https://files.pythonhosted.org/packages/06/dd/fa6660c279f4eb320788876d0cff4ea18d9af7d9ed7216d7bd66877468d0/ruff-0.8.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fe710ab6061592521f902fca7ebcb9fabd27bc7c57c764298b1c1f15fff720", size = 11205976 }, - { url = "https://files.pythonhosted.org/packages/a8/d7/de94cc89833b5de455750686c17c9e10f4e1ab7ccdc5521b8fe911d1477e/ruff-0.8.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:13e9ec6d6b55f6da412d59953d65d66e760d583dd3c1c72bf1f26435b5bfdbae", size = 10792039 }, - { url = "https://files.pythonhosted.org/packages/6d/15/3e4906559248bdbb74854af684314608297a05b996062c9d72e0ef7c7097/ruff-0.8.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:97d9aefef725348ad77d6db98b726cfdb075a40b936c7984088804dfd38268a7", size = 10400088 }, - { url = "https://files.pythonhosted.org/packages/a2/21/9ed4c0e8133cb4a87a18d470f534ad1a8a66d7bec493bcb8bda2d1a5d5be/ruff-0.8.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ab78e33325a6f5374e04c2ab924a3367d69a0da36f8c9cb6b894a62017506111", size = 10900814 }, - { url = "https://files.pythonhosted.org/packages/0d/5d/122a65a18955bd9da2616b69bc839351f8baf23b2805b543aa2f0aed72b5/ruff-0.8.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8ef06f66f4a05c3ddbc9121a8b0cecccd92c5bf3dd43b5472ffe40b8ca10f0f8", size = 11268828 }, - { url = "https://files.pythonhosted.org/packages/43/a9/1676ee9106995381e3d34bccac5bb28df70194167337ed4854c20f27c7ba/ruff-0.8.4-py3-none-win32.whl", hash = "sha256:552fb6d861320958ca5e15f28b20a3d071aa83b93caee33a87b471f99a6c0835", size = 8805621 }, - { url = "https://files.pythonhosted.org/packages/10/98/ed6b56a30ee76771c193ff7ceeaf1d2acc98d33a1a27b8479cbdb5c17a23/ruff-0.8.4-py3-none-win_amd64.whl", hash = "sha256:f21a1143776f8656d7f364bd264a9d60f01b7f52243fbe90e7670c0dfe0cf65d", size = 9660086 }, - { url = "https://files.pythonhosted.org/packages/13/9f/026e18ca7d7766783d779dae5e9c656746c6ede36ef73c6d934aaf4a6dec/ruff-0.8.4-py3-none-win_arm64.whl", hash = "sha256:9183dd615d8df50defa8b1d9a074053891ba39025cf5ae88e8bcb52edcc4bf08", size = 9074500 }, +version = "0.8.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/00/089db7890ea3be5709e3ece6e46408d6f1e876026ec3fd081ee585fef209/ruff-0.8.6.tar.gz", hash = "sha256:dcad24b81b62650b0eb8814f576fc65cfee8674772a6e24c9b747911801eeaa5", size = 3473116 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/28/aa07903694637c2fa394a9f4fe93cf861ad8b09f1282fa650ef07ff9fe97/ruff-0.8.6-py3-none-linux_armv6l.whl", hash = "sha256:defed167955d42c68b407e8f2e6f56ba52520e790aba4ca707a9c88619e580e3", size = 10628735 }, + { url = "https://files.pythonhosted.org/packages/2b/43/827bb1448f1fcb0fb42e9c6edf8fb067ca8244923bf0ddf12b7bf949065c/ruff-0.8.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:54799ca3d67ae5e0b7a7ac234baa657a9c1784b48ec954a094da7c206e0365b1", size = 10386758 }, + { url = "https://files.pythonhosted.org/packages/df/93/fc852a81c3cd315b14676db3b8327d2bb2d7508649ad60bfdb966d60738d/ruff-0.8.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e88b8f6d901477c41559ba540beeb5a671e14cd29ebd5683903572f4b40a9807", size = 10007808 }, + { url = "https://files.pythonhosted.org/packages/94/e9/e0ed4af1794335fb280c4fac180f2bf40f6a3b859cae93a5a3ada27325ae/ruff-0.8.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0509e8da430228236a18a677fcdb0c1f102dd26d5520f71f79b094963322ed25", size = 10861031 }, + { url = "https://files.pythonhosted.org/packages/82/68/da0db02f5ecb2ce912c2bef2aa9fcb8915c31e9bc363969cfaaddbc4c1c2/ruff-0.8.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a7ddb221779871cf226100e677b5ea38c2d54e9e2c8ed847450ebbdf99b32d", size = 10388246 }, + { url = "https://files.pythonhosted.org/packages/ac/1d/b85383db181639019b50eb277c2ee48f9f5168f4f7c287376f2b6e2a6dc2/ruff-0.8.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:248b1fb3f739d01d528cc50b35ee9c4812aa58cc5935998e776bf8ed5b251e75", size = 11424693 }, + { url = "https://files.pythonhosted.org/packages/ac/b7/30bc78a37648d31bfc7ba7105b108cb9091cd925f249aa533038ebc5a96f/ruff-0.8.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:bc3c083c50390cf69e7e1b5a5a7303898966be973664ec0c4a4acea82c1d4315", size = 12141921 }, + { url = "https://files.pythonhosted.org/packages/60/b3/ee0a14cf6a1fbd6965b601c88d5625d250b97caf0534181e151504498f86/ruff-0.8.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52d587092ab8df308635762386f45f4638badb0866355b2b86760f6d3c076188", size = 11692419 }, + { url = "https://files.pythonhosted.org/packages/ef/d6/c597062b2931ba3e3861e80bd2b147ca12b3370afc3889af46f29209037f/ruff-0.8.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:61323159cf21bc3897674e5adb27cd9e7700bab6b84de40d7be28c3d46dc67cf", size = 12981648 }, + { url = "https://files.pythonhosted.org/packages/68/84/21f578c2a4144917985f1f4011171aeff94ab18dfa5303ac632da2f9af36/ruff-0.8.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ae4478b1471fc0c44ed52a6fb787e641a2ac58b1c1f91763bafbc2faddc5117", size = 11251801 }, + { url = "https://files.pythonhosted.org/packages/6c/aa/1ac02537c8edeb13e0955b5db86b5c050a1dcba54f6d49ab567decaa59c1/ruff-0.8.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0c000a471d519b3e6cfc9c6680025d923b4ca140ce3e4612d1a2ef58e11f11fe", size = 10849857 }, + { url = "https://files.pythonhosted.org/packages/eb/00/020cb222252d833956cb3b07e0e40c9d4b984fbb2dc3923075c8f944497d/ruff-0.8.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9257aa841e9e8d9b727423086f0fa9a86b6b420fbf4bf9e1465d1250ce8e4d8d", size = 10470852 }, + { url = "https://files.pythonhosted.org/packages/00/56/e6d6578202a0141cd52299fe5acb38b2d873565f4670c7a5373b637cf58d/ruff-0.8.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45a56f61b24682f6f6709636949ae8cc82ae229d8d773b4c76c09ec83964a95a", size = 10972997 }, + { url = "https://files.pythonhosted.org/packages/be/31/dd0db1f4796bda30dea7592f106f3a67a8f00bcd3a50df889fbac58e2786/ruff-0.8.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:496dd38a53aa173481a7d8866bcd6451bd934d06976a2505028a50583e001b76", size = 11317760 }, + { url = "https://files.pythonhosted.org/packages/d4/70/cfcb693dc294e034c6fed837fa2ec98b27cc97a26db5d049345364f504bf/ruff-0.8.6-py3-none-win32.whl", hash = "sha256:e169ea1b9eae61c99b257dc83b9ee6c76f89042752cb2d83486a7d6e48e8f764", size = 8799729 }, + { url = "https://files.pythonhosted.org/packages/60/22/ae6bcaa0edc83af42751bd193138bfb7598b2990939d3e40494d6c00698c/ruff-0.8.6-py3-none-win_amd64.whl", hash = "sha256:f1d70bef3d16fdc897ee290d7d20da3cbe4e26349f62e8a0274e7a3f4ce7a905", size = 9673857 }, + { url = "https://files.pythonhosted.org/packages/91/f8/3765e053acd07baa055c96b2065c7fab91f911b3c076dfea71006666f5b0/ruff-0.8.6-py3-none-win_arm64.whl", hash = "sha256:7d7fc2377a04b6e04ffe588caad613d0c460eb2ecba4c0ccbbfe2bc973cbc162", size = 9149556 }, ] [[package]] name = "setuptools" -version = "75.6.0" +version = "75.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/54/292f26c208734e9a7f067aea4a7e282c080750c4546559b58e2e45413ca0/setuptools-75.6.0.tar.gz", hash = "sha256:8199222558df7c86216af4f84c30e9b34a61d8ba19366cc914424cdbd28252f6", size = 1337429 } +sdist = { url = "https://files.pythonhosted.org/packages/ac/57/e6f0bde5a2c333a32fbcce201f906c1fd0b3a7144138712a5e9d9598c5ec/setuptools-75.7.0.tar.gz", hash = "sha256:886ff7b16cd342f1d1defc16fc98c9ce3fde69e087a4e1983d7ab634e5f41f4f", size = 1338616 } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/21/47d163f615df1d30c094f6c8bbb353619274edccf0327b185cc2493c2c33/setuptools-75.6.0-py3-none-any.whl", hash = "sha256:ce74b49e8f7110f9bf04883b730f4765b774ef3ef28f722cce7c273d253aaf7d", size = 1224032 }, + { url = "https://files.pythonhosted.org/packages/4e/6e/abdfaaf5c294c553e7a81cf5d801fbb4f53f5c5b6646de651f92a2667547/setuptools-75.7.0-py3-none-any.whl", hash = "sha256:84fb203f278ebcf5cd08f97d3fb96d3fbed4b629d500b29ad60d11e00769b183", size = 1224467 }, ] [[package]] @@ -633,11 +694,11 @@ wheels = [ [[package]] name = "types-pyyaml" -version = "6.0.12.20241221" +version = "6.0.12.20241230" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f4/60/ba3f23024bdd406e65c359b9dbd9757f058986bd57d94f6639015f9a9fae/types_pyyaml-6.0.12.20241221.tar.gz", hash = "sha256:4f149aa893ff6a46889a30af4c794b23833014c469cc57cbc3ad77498a58996f", size = 17034 } +sdist = { url = "https://files.pythonhosted.org/packages/9a/f9/4d566925bcf9396136c0a2e5dc7e230ff08d86fa011a69888dd184469d80/types_pyyaml-6.0.12.20241230.tar.gz", hash = "sha256:7f07622dbd34bb9c8b264fe860a17e0efcad00d50b5f27e93984909d9363498c", size = 17078 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/04/1cc4fffeb4ace85c205e84cd48eb12cb37ec6ffb68245b7eef8f2086d490/types_PyYAML-6.0.12.20241221-py3-none-any.whl", hash = "sha256:0657a4ff8411a030a2116a196e8e008ea679696b5b1a8e1a6aa8ebb737b34688", size = 20023 }, + { url = "https://files.pythonhosted.org/packages/e8/c1/48474fbead512b70ccdb4f81ba5eb4a58f69d100ba19f17c92c0c4f50ae6/types_PyYAML-6.0.12.20241230-py3-none-any.whl", hash = "sha256:fa4d32565219b68e6dee5f67534c722e53c00d1cfc09c435ef04d7353e1e96e6", size = 20029 }, ] [[package]]