diff --git a/.ci/run_container.sh b/.ci/run_container.sh index ed8deb383..d1a837cbe 100755 --- a/.ci/run_container.sh +++ b/.ci/run_container.sh @@ -69,8 +69,8 @@ else fi export PULP_CONTENT_ORIGIN -PULP_DJANGO_SECRET="$(python3 -c "import secrets; print(secrets.token_urlsafe(50))")" -export PULP_DJANGO_SECRET +PULP_SECRET_KEY="$(python3 -c "import secrets; print(secrets.token_urlsafe(50))")" +export PULP_SECRET_KEY "${CONTAINER_RUNTIME}" \ run ${RM:+--rm} \ @@ -82,7 +82,7 @@ export PULP_DJANGO_SECRET ${PULP_DOMAIN_ENABLED:+--env PULP_DOMAIN_ENABLED} \ ${PULP_ENABLED_PLUGINS:+--env PULP_ENABLED_PLUGINS} \ --env PULP_CONTENT_ORIGIN \ - --env PULP_DJANGO_SECRET \ + --env PULP_SECRET_KEY \ --detach \ --name "pulp-ephemeral" \ --volume "${PULP_CLI_TEST_TMPDIR}/settings:/etc/pulp${SELINUX:+:Z}" \ diff --git a/.ci/scripts/check_click_for_mypy.py b/.ci/scripts/check_click_for_mypy.py index 33ecf4ca1..d4b1aea7f 100755 --- a/.ci/scripts/check_click_for_mypy.py +++ b/.ci/scripts/check_click_for_mypy.py @@ -5,7 +5,7 @@ # "packaging>=25.0,<25.1", # ] # /// - +import sys from importlib import metadata from packaging.version import Version @@ -15,4 +15,4 @@ if click_version < Version("8.1.1"): print("🚧 Linting with mypy is currently only supported with click>=8.1.1. 🚧") print("🔧 Please run `pip install click>=8.1.1` first. 🔨") - exit(1) + sys.exit(1) diff --git a/.ci/scripts/collect_changes.py b/.ci/scripts/collect_changes.py index 80d7477a6..9ed2b481a 100755 --- a/.ci/scripts/collect_changes.py +++ b/.ci/scripts/collect_changes.py @@ -9,10 +9,12 @@ import itertools import re +import typing as t from pathlib import Path import tomllib from git import GitCommandError, Repo +from packaging.version import Version from packaging.version import parse as parse_version # Read Towncrier settings @@ -51,7 +53,7 @@ ) -def get_changelog(repo, branch): +def get_changelog(repo: Repo, branch: str) -> str: branch_tc_settings = tomllib.loads(repo.git.show(f"{branch}:pyproject.toml"))["tool"][ "towncrier" ] @@ -59,7 +61,7 @@ def get_changelog(repo, branch): return repo.git.show(f"{branch}:{branch_changelog_file}") + "\n" -def _tokenize_changes(splits): +def _tokenize_changes(splits: list[str]) -> t.Iterator[list[Version | str]]: assert len(splits) % 3 == 0 for i in range(len(splits) // 3): title = splits[3 * i] @@ -67,13 +69,13 @@ def _tokenize_changes(splits): yield [version, title + splits[3 * i + 2]] -def split_changelog(changelog): +def split_changelog(changelog: str) -> tuple[str, list[list[Version | str]]]: preamble, rest = changelog.split(START_STRING, maxsplit=1) split_rest = re.split(TITLE_REGEX, rest) return preamble + START_STRING + split_rest[0], list(_tokenize_changes(split_rest[1:])) -def main(): +def main() -> None: repo = Repo(Path.cwd()) remote = repo.remotes[0] branches = [ref for ref in remote.refs if re.match(r"^([0-9]+)\.([0-9]+)$", ref.remote_head)] @@ -91,7 +93,7 @@ def main(): except GitCommandError: print("No changelog found on this branch.") continue - dummy, changes = split_changelog(changelog) + _dummy, changes = split_changelog(changelog) new_changes = sorted(main_changes + changes, key=lambda x: x[0], reverse=True) # Now remove duplicates (retain the first one) main_changes = [new_changes[0]] diff --git a/Makefile b/Makefile index 63218ab45..a05ac1621 100644 --- a/Makefile +++ b/Makefile @@ -93,11 +93,11 @@ unittest_glue: .PHONY: docs docs: - pulp-docs build + uv run --only-group docs pulp-docs build --draft --no-blog .PHONY: servedocs servedocs: - pulp-docs serve -w CHANGES.md -w pulp-glue/pulp_glue -w pulp_cli/generic.py + uv run --only-group docs pulp-docs serve --draft --no-blog -w CHANGES.md -w src -w pulp-glue/src pulp-glue/pulp_glue/%/locale/messages.pot: pulp-glue/pulp_glue/%/*.py xgettext -d $* -o $@ pulp-glue/pulp_glue/$*/*.py diff --git a/cookiecutter/ci/{{ cookiecutter.__project_name }}/.ci/run_container.sh b/cookiecutter/ci/{{ cookiecutter.__project_name }}/.ci/run_container.sh index ed8deb383..d1a837cbe 100755 --- a/cookiecutter/ci/{{ cookiecutter.__project_name }}/.ci/run_container.sh +++ b/cookiecutter/ci/{{ cookiecutter.__project_name }}/.ci/run_container.sh @@ -69,8 +69,8 @@ else fi export PULP_CONTENT_ORIGIN -PULP_DJANGO_SECRET="$(python3 -c "import secrets; print(secrets.token_urlsafe(50))")" -export PULP_DJANGO_SECRET +PULP_SECRET_KEY="$(python3 -c "import secrets; print(secrets.token_urlsafe(50))")" +export PULP_SECRET_KEY "${CONTAINER_RUNTIME}" \ run ${RM:+--rm} \ @@ -82,7 +82,7 @@ export PULP_DJANGO_SECRET ${PULP_DOMAIN_ENABLED:+--env PULP_DOMAIN_ENABLED} \ ${PULP_ENABLED_PLUGINS:+--env PULP_ENABLED_PLUGINS} \ --env PULP_CONTENT_ORIGIN \ - --env PULP_DJANGO_SECRET \ + --env PULP_SECRET_KEY \ --detach \ --name "pulp-ephemeral" \ --volume "${PULP_CLI_TEST_TMPDIR}/settings:/etc/pulp${SELINUX:+:Z}" \ diff --git a/cookiecutter/ci/{{ cookiecutter.__project_name }}/.ci/scripts/check_click_for_mypy.py b/cookiecutter/ci/{{ cookiecutter.__project_name }}/.ci/scripts/check_click_for_mypy.py index 33ecf4ca1..d4b1aea7f 100755 --- a/cookiecutter/ci/{{ cookiecutter.__project_name }}/.ci/scripts/check_click_for_mypy.py +++ b/cookiecutter/ci/{{ cookiecutter.__project_name }}/.ci/scripts/check_click_for_mypy.py @@ -5,7 +5,7 @@ # "packaging>=25.0,<25.1", # ] # /// - +import sys from importlib import metadata from packaging.version import Version @@ -15,4 +15,4 @@ if click_version < Version("8.1.1"): print("🚧 Linting with mypy is currently only supported with click>=8.1.1. 🚧") print("🔧 Please run `pip install click>=8.1.1` first. 🔨") - exit(1) + sys.exit(1) diff --git a/cookiecutter/ci/{{ cookiecutter.__project_name }}/.ci/scripts/collect_changes.py b/cookiecutter/ci/{{ cookiecutter.__project_name }}/.ci/scripts/collect_changes.py index 80d7477a6..9ed2b481a 100755 --- a/cookiecutter/ci/{{ cookiecutter.__project_name }}/.ci/scripts/collect_changes.py +++ b/cookiecutter/ci/{{ cookiecutter.__project_name }}/.ci/scripts/collect_changes.py @@ -9,10 +9,12 @@ import itertools import re +import typing as t from pathlib import Path import tomllib from git import GitCommandError, Repo +from packaging.version import Version from packaging.version import parse as parse_version # Read Towncrier settings @@ -51,7 +53,7 @@ ) -def get_changelog(repo, branch): +def get_changelog(repo: Repo, branch: str) -> str: branch_tc_settings = tomllib.loads(repo.git.show(f"{branch}:pyproject.toml"))["tool"][ "towncrier" ] @@ -59,7 +61,7 @@ def get_changelog(repo, branch): return repo.git.show(f"{branch}:{branch_changelog_file}") + "\n" -def _tokenize_changes(splits): +def _tokenize_changes(splits: list[str]) -> t.Iterator[list[Version | str]]: assert len(splits) % 3 == 0 for i in range(len(splits) // 3): title = splits[3 * i] @@ -67,13 +69,13 @@ def _tokenize_changes(splits): yield [version, title + splits[3 * i + 2]] -def split_changelog(changelog): +def split_changelog(changelog: str) -> tuple[str, list[list[Version | str]]]: preamble, rest = changelog.split(START_STRING, maxsplit=1) split_rest = re.split(TITLE_REGEX, rest) return preamble + START_STRING + split_rest[0], list(_tokenize_changes(split_rest[1:])) -def main(): +def main() -> None: repo = Repo(Path.cwd()) remote = repo.remotes[0] branches = [ref for ref in remote.refs if re.match(r"^([0-9]+)\.([0-9]+)$", ref.remote_head)] @@ -91,7 +93,7 @@ def main(): except GitCommandError: print("No changelog found on this branch.") continue - dummy, changes = split_changelog(changelog) + _dummy, changes = split_changelog(changelog) new_changes = sorted(main_changes + changes, key=lambda x: x[0], reverse=True) # Now remove duplicates (retain the first one) main_changes = [new_changes[0]] diff --git a/cookiecutter/ci/{{ cookiecutter.__project_name }}/Makefile b/cookiecutter/ci/{{ cookiecutter.__project_name }}/Makefile index fd5ad2501..4c2612a9d 100644 --- a/cookiecutter/ci/{{ cookiecutter.__project_name }}/Makefile +++ b/cookiecutter/ci/{{ cookiecutter.__project_name }}/Makefile @@ -107,11 +107,11 @@ unittest_glue: .PHONY: docs docs: - pulp-docs build + uv run --only-group docs pulp-docs build --draft --no-blog .PHONY: servedocs servedocs: - pulp-docs serve -w CHANGES.md -w pulp-glue/pulp_glue -w pulp_cli/generic.py + uv run --only-group docs pulp-docs serve --draft --no-blog -w CHANGES.md -w src -w pulp-glue/src {%- endif %} {%- if cookiecutter.translations %} {%- if cookiecutter.glue %} diff --git a/cookiecutter/ci/{{ cookiecutter.__project_name }}/pulp-glue{{ cookiecutter.__app_label_suffix }}/pyproject.toml.update b/cookiecutter/ci/{{ cookiecutter.__project_name }}/pulp-glue{{ cookiecutter.__app_label_suffix }}/pyproject.toml.update index 90d8fea6f..f30a66294 100644 --- a/cookiecutter/ci/{{ cookiecutter.__project_name }}/pulp-glue{{ cookiecutter.__app_label_suffix }}/pyproject.toml.update +++ b/cookiecutter/ci/{{ cookiecutter.__project_name }}/pulp-glue{{ cookiecutter.__app_label_suffix }}/pyproject.toml.update @@ -19,7 +19,11 @@ line-length = 100 [tool.ruff.lint] # This section is managed by the cookiecutter templates. select = ["E4", "E7", "E9", "F"] -extend-select = ["I", "INT", "PTH"] +extend-select = ["FURB", "I", "INT", "PTH", "SIM1", "TID", "T10", "UP"] + +[tool.ruff.lint.flake8-tidy-imports.banned-api] +# This section is managed by the cookiecutter templates. +"distutils".msg = "The 'distutils' module has been deprecated since Python 3.9." [tool.mypy] # This section is managed by the cookiecutter templates. diff --git a/cookiecutter/ci/{{ cookiecutter.__project_name }}/pyproject.toml.update b/cookiecutter/ci/{{ cookiecutter.__project_name }}/pyproject.toml.update index 8da24d85d..56f13e557 100644 --- a/cookiecutter/ci/{{ cookiecutter.__project_name }}/pyproject.toml.update +++ b/cookiecutter/ci/{{ cookiecutter.__project_name }}/pyproject.toml.update @@ -12,7 +12,7 @@ dev = [ lint = [ {include-group = "test"}, "mypy~=1.20.0", - "ruff~=0.15.1", + "ruff~=0.16.0", "shellcheck-py~=0.11.0.1", "types-pygments", "types-pyyaml", @@ -28,16 +28,34 @@ test = [ "secretstorage>=3.5.0", "trustme>=1.1.0,<1.3", ] -{%- if cookiecutter.glue %} +{%- if cookiecutter.docs %} +docs = [ + "pulp-docs", +] +{%- endif %} +{%- if cookiecutter.glue or cookiecutter.docs %} [tool.uv.sources] # This section is managed by the cookiecutter templates. +{%- if cookiecutter.glue %} pulp-glue{{ cookiecutter.__app_label_suffix }} = { workspace = true } +{%- endif %} +{%- if cookiecutter.docs %} +pulp-docs = { git = "https://github.com/pulp/pulp-docs" } +{%- endif %} +{%- endif %} +{%- if cookiecutter.glue %} [tool.uv.workspace] # This section is managed by the cookiecutter templates. members = ["pulp-glue{{ cookiecutter.__app_label_suffix }}"] {%- endif %} +{%- if cookiecutter.docs %} + +[tool.uv.dependency-groups] +# This section is managed by the cookiecutter templates. +docs = {requires-python = ">=3.11"} +{%- endif %} [tool.uv.build-backend] # This section is managed by the cookiecutter templates. @@ -210,13 +228,18 @@ extend-exclude = ["cookiecutter/bootstrap", "cookiecutter/ci", "cookiecutter/doc [tool.ruff.lint] # This section is managed by the cookiecutter templates. select = ["E4", "E7", "E9", "F"] -extend-select = ["I", "INT", "PTH"] +extend-select = ["FURB", "I", "INT", "PTH", "SIM1", "TID", "T10", "UP"] [tool.ruff.lint.isort] # This section is managed by the cookiecutter templates. sections = { second-party = ["pulp_glue"] } section-order = ["future", "standard-library", "third-party", "second-party", "first-party", "local-folder"] +[tool.ruff.lint.flake8-tidy-imports.banned-api] +# This section is managed by the cookiecutter templates. +"distutils".msg = "The 'distutils' module has been deprecated since Python 3.9." +"pulpcore.cli.common.generic".msg = "This module moved to 'pulp_cli.generic'." + [tool.mypy] # This section is managed by the cookiecutter templates. strict = true diff --git a/cookiecutter/update_pulp_cli.py b/cookiecutter/update_pulp_cli.py index c31aab4cd..c13acf5ab 100755 --- a/cookiecutter/update_pulp_cli.py +++ b/cookiecutter/update_pulp_cli.py @@ -41,7 +41,7 @@ def strategy_semver(requirement: Requirement, latest_version: Version) -> Requir rest.append(spec) assert upper_bound is not None if latest_version < Version(upper_bound.version): - _logger.warn( + _logger.warning( f"Dependency on {requirement} cannot be updated " "to include latest version {latest_version}." ) @@ -73,7 +73,7 @@ def latest_version(canonical_name: str, allow_prereleases: bool | None = None) - releases = json.loads(response.read())["releases"] available_versions = sorted( version - for version in (Version(key) for key in releases.keys()) + for version in (Version(key) for key in releases) if allow_prereleases or not version.is_prerelease ) return available_versions[-1] diff --git a/docs/user/guides/installation.md b/docs/user/guides/installation.md index d5b342781..36c28c85b 100644 --- a/docs/user/guides/installation.md +++ b/docs/user/guides/installation.md @@ -2,19 +2,19 @@ ## TL;DR: Get Started Real Fast -=== "pip" +=== "uv" ```bash - pip install pulp-cli[pygments] + uv tool install pulp-cli[pygments] pulp config create -e # insert your server configuration here pulp status ``` -=== "uv" +=== "pip" ```bash - uv tool install pulp-cli[pygments] + pip install pulp-cli[pygments] pulp config create -e # insert your server configuration here pulp status @@ -97,14 +97,47 @@ pip install pulp-cli-deb ## From a source checkout If you intend to use unreleased features, or want to contribute to the CLI, you can install from source: -```bash -git clone git@github.com:pulp/pulp-cli.git -pip install -e ./pulp-cli -e ./pulp-cli/pulp-glue -# Optionally install plugins from source -git clone git@github.com:pulp/pulp-cli-deb.git -pip install -e ./pulp-cli-deb +=== "uv" -git clone git@github.com:pulp/pulp-cli-maven.git -pip install -e ./pulp-cli-maven -e ./pulp-cli-maven/pulp-glue-maven -``` + ```bash + # Upon entering the source directory, uv handles the virtual environment on the fly. + + git clone git@github.com:pulp/pulp-cli.git + cd pulp-cli + uv run pulp status + + # Alternatively run in plugin development: + git clone git@github.com:pulp/pulp-cli-gem.git + cd pulp-cli-gem + uv run pulp status + ``` + +=== "uv (install)" + + ```bash + # The matching glue development version is automatically taken from the same workspace. + + git clone git@github.com:pulp/pulp-cli.git + uv tool install --editable pulp-cli + + # Alternatively install plugins from source: + git clone git@github.com:pulp/pulp-cli-gem.git + uv tool install pulp-cli --with-editable pulp-cli-gem + ``` + +=== "pip" + + ```bash + # The matching glue development version must be installed from the same source. + + git clone git@github.com:pulp/pulp-cli.git + pip install -e ./pulp-cli -e ./pulp-cli/pulp-glue + + # Alternatively install plugins from source: + git clone git@github.com:pulp/pulp-cli-deb.git + pip install -e ./pulp-cli-deb + + git clone git@github.com:pulp/pulp-cli-maven.git + pip install -e ./pulp-cli-maven -e ./pulp-cli-maven/pulp-glue-maven + ``` diff --git a/pulp-glue/pyproject.toml b/pulp-glue/pyproject.toml index c4a95a0c9..f47ae8a89 100644 --- a/pulp-glue/pyproject.toml +++ b/pulp-glue/pyproject.toml @@ -76,5 +76,9 @@ line-length = 100 [tool.ruff.lint] # This section is managed by the cookiecutter templates. select = ["E4", "E7", "E9", "F"] -extend-select = ["I", "INT", "PTH"] +extend-select = ["FURB", "I", "INT", "PTH", "SIM1", "TID", "T10", "UP"] + +[tool.ruff.lint.flake8-tidy-imports.banned-api] +# This section is managed by the cookiecutter templates. +"distutils".msg = "The 'distutils' module has been deprecated since Python 3.9." diff --git a/pulp-glue/src/pulp_glue/ansible/context.py b/pulp-glue/src/pulp_glue/ansible/context.py index 208c24589..27c09eb61 100644 --- a/pulp-glue/src/pulp_glue/ansible/context.py +++ b/pulp-glue/src/pulp_glue/ansible/context.py @@ -124,7 +124,7 @@ class PulpAnsibleCollectionRemoteContext(PulpRemoteContext): def preprocess_entity(self, body: EntityDefinition, partial: bool = False) -> EntityDefinition: body = super().preprocess_entity(body, partial=partial) - if "requirements" in body.keys(): + if "requirements" in body: body["requirements_file"] = body.pop("requirements") return body diff --git a/pulp-glue/src/pulp_glue/common/context.py b/pulp-glue/src/pulp_glue/common/context.py index bdcc20d5e..46fc35d47 100644 --- a/pulp-glue/src/pulp_glue/common/context.py +++ b/pulp-glue/src/pulp_glue/common/context.py @@ -199,9 +199,9 @@ def walk_operations(api_spec: t.Any) -> t.Iterator[tuple[str, str, str, t.Any]]: def _patch_api_hook(spec: t.Any) -> t.Any: for req, quirk in _REGISTERED_API_SPEC_QUIRKS: - if ver := spec["info"].get("x-pulp-app-versions", {}).get(req.name): - if ver in req: - spec = quirk(spec) + ver: str | None = spec["info"].get("x-pulp-app-versions", {}).get(req.name) + if ver is not None and ver in req: + spec = quirk(spec) return spec @@ -269,15 +269,16 @@ def patch_upstream_pulp_replicate_request_body(api_spec: t.Any) -> t.Any: @api_spec_quirk(PluginRequirement("core", specifier="<3.85")) def patch_security_scheme_mutual_tls(api_spec: t.Any) -> t.Any: # Trick to allow tls cert auth on older Pulp. - if (components := api_spec.get("components")) is not None: - if (security_schemes := components.get("securitySchemes")) is not None: - # Only if it is going to be idempotent... - if "gluePatchTLS" not in security_schemes: - security_schemes["gluePatchTLS"] = {"type": "mutualTLS"} - for path, method, operation_id, operation in walk_operations(api_spec): - security = operation.get("security") - if security is not None: - security.append({"gluePatchTLS": []}) + if ( + (components := api_spec.get("components")) is not None + and (security_schemes := components.get("securitySchemes")) is not None + and "gluePatchTLS" not in security_schemes # Only if it is going to be idempotent... + ): + security_schemes["gluePatchTLS"] = {"type": "mutualTLS"} + for path, method, operation_id, operation in walk_operations(api_spec): + security = operation.get("security") + if security is not None: + security.append({"gluePatchTLS": []}) return api_spec @@ -529,10 +530,13 @@ def call( """ if parameters is None: parameters = {} - if self.domain_enabled: + if ( + self.domain_enabled + and # Validation will fail if path doesn't need domain parameter - if "pulp_domain" in self.api.param_spec(operation_id, "path", required=True): - parameters["pulp_domain"] = self.pulp_domain + "pulp_domain" in self.api.param_spec(operation_id, "path", required=True) + ): + parameters["pulp_domain"] = self.pulp_domain parameters = preprocess_payload(parameters) if body is not None: body = preprocess_payload(body) @@ -558,9 +562,11 @@ def call( ) if not non_blocking: result = self.wait_for_task(result) - if self.has_plugin(PluginRequirement("core", specifier=">=3.86")): - if result["result"] is not None: - result = result["result"] + if ( + self.has_plugin(PluginRequirement("core", specifier=">=3.86")) + and result["result"] is not None + ): + result = result["result"] elif isinstance(result, dict) and ["task_group"] == list(result.keys()): task_group_href = result["task_group"] result = self.api.call( @@ -1548,7 +1554,7 @@ def scope(self) -> dict[str, t.Any]: def entity(self) -> EntityDefinition: if ( self._entity is None - and "number" in self._entity_lookup.keys() + and "number" in self._entity_lookup and self._entity_lookup.get("number") is None ): self.pulp_href = self.repository_ctx.entity["latest_version_href"] @@ -1625,11 +1631,14 @@ def preprocess_entity(self, body: EntityDefinition, partial: bool = False) -> En body = super().preprocess_entity(body, partial=partial) if "retain_repo_versions" in body: self.pulp_ctx.needs_plugin(PluginRequirement("core", specifier=">=3.13.0")) - if self.pulp_ctx.has_plugin(PluginRequirement("core", specifier=">=3.13.0,<3.15.0")): + if ( + self.pulp_ctx.has_plugin(PluginRequirement("core", specifier=">=3.13.0,<3.15.0")) + and # "retain_repo_versions" has been named "retained_versions" until pulpcore 3.15 # https://github.com/pulp/pulpcore/pull/1472 - if "retain_repo_versions" in body: - body["retained_versions"] = body.pop("retain_repo_versions") + "retain_repo_versions" in body + ): + body["retained_versions"] = body.pop("retain_repo_versions") return body def sync(self, body: EntityDefinition | None = None) -> t.Any: @@ -1777,8 +1786,7 @@ def create( chunk_size: int | None = body.pop("chunk_size", None) if file: if isinstance(file, str | Path): - file = Path(file).open("rb") - cleanup.enter_context(file) + file = cleanup.enter_context(Path(file).open("rb")) self._prepare_upload(body, file, chunk_size) if self.repository_ctx is not None: body["repository"] = self.repository_ctx diff --git a/pulp-glue/src/pulp_glue/common/oas.py b/pulp-glue/src/pulp_glue/common/oas.py index f93225051..f1a92b3c8 100644 --- a/pulp-glue/src/pulp_glue/common/oas.py +++ b/pulp-glue/src/pulp_glue/common/oas.py @@ -18,9 +18,7 @@ class ExtensibleOASBase(OASBase, extra="allow"): @pydantic.model_validator(mode="after") def _check_extensions(self) -> "t.Self": if self.__pydantic_extra__ is not None: - invalid_keys = [ - key for key in self.__pydantic_extra__.keys() if not key.startswith("x-") - ] + invalid_keys = [key for key in self.__pydantic_extra__ if not key.startswith("x-")] if invalid_keys: raise PydanticCustomError( "invalid_extensions", diff --git a/pulp-glue/src/pulp_glue/common/schema.py b/pulp-glue/src/pulp_glue/common/schema.py index e51de9d96..8803518d4 100644 --- a/pulp-glue/src/pulp_glue/common/schema.py +++ b/pulp-glue/src/pulp_glue/common/schema.py @@ -129,11 +129,10 @@ def validate( _validate_ref(schema.ref, name, value, components) return - if value is None: + if value is None and schema.nullable: # This seems to be the openapi 3.0.3 way. # in 3.1.* they use `"type": ["string", "null"]` instead. - if schema.nullable: - return + return if isinstance(schema, oas.TypeSchema): if isinstance(schema.type_, list): @@ -211,23 +210,20 @@ def _validate_array( schema: oas.TypeSchema, name: str, value: t.Any, components: dict[str, t.Any] ) -> None: _assert_type(name, value, list, "array") - if schema.min_items is not None: - if len(value) < schema.min_items: - raise ValidationError( - _("'{name}' is expected to have at least {min_items} items.").format( - name=name, min_items=schema.min_items - ) + if schema.min_items is not None and len(value) < schema.min_items: + raise ValidationError( + _("'{name}' is expected to have at least {min_items} items.").format( + name=name, min_items=schema.min_items ) - if schema.max_items is not None: - if len(value) > schema.max_items: - raise ValidationError( - _("'{name}' is expected to have at most {max_items} items.").format( - name=name, max_items=schema.max_items - ) + ) + if schema.max_items is not None and len(value) > schema.max_items: + raise ValidationError( + _("'{name}' is expected to have at most {max_items} items.").format( + name=name, max_items=schema.max_items ) - if schema.unique_items: - if len(set(value)) != len(value): - raise ValidationError(_("'{name}' is expected to have unique items.").format(name=name)) + ) + if schema.unique_items and len(set(value)) != len(value): + raise ValidationError(_("'{name}' is expected to have unique items.").format(name=name)) if schema.items is not None: for i, item in enumerate(value): @@ -246,13 +242,12 @@ def _validate_integer( _assert_type(name, value, int, "integer") _assert_min_max(schema, name, value) - if schema.multiple_of is not None: - if value % schema.multiple_of != 0: - raise ValidationError( - _("'{name}' is expected to be a multiple of {multiple_of}").format( - name=name, multiple_of=schema.multiple_of - ) + if schema.multiple_of is not None and value % schema.multiple_of != 0: + raise ValidationError( + _("'{name}' is expected to be a multiple of {multiple_of}").format( + name=name, multiple_of=schema.multiple_of ) + ) def _validate_null( @@ -289,13 +284,12 @@ def _validate_object( ) for pname, pvalue in extra_values.items(): validate(schema.additional_properties, f"{name}[{pname}]", pvalue, components) - if schema.required is not None: - if missing_keys := set(schema.required) - set(value.keys()): - raise ValidationError( - _("'{name}' is missing properties ({missing}).").format( - name=name, missing=", ".join(missing_keys) - ) + if schema.required is not None and (missing_keys := set(schema.required) - set(value.keys())): + raise ValidationError( + _("'{name}' is missing properties ({missing}).").format( + name=name, missing=", ".join(missing_keys) ) + ) def _validate_string( @@ -314,13 +308,12 @@ def _validate_string( _assert_type(name, value, datetime.datetime, "date-time") else: _assert_type(name, value, str, "string") - if schema.enum is not None: - if value not in schema.enum: - raise ValidationError( - _("'{name}' is expected to be one of [{enums}].").format( - name=name, enums=", ".join(schema.enum) - ) + if schema.enum is not None and value not in schema.enum: + raise ValidationError( + _("'{name}' is expected to be one of [{enums}].").format( + name=name, enums=", ".join(schema.enum) ) + ) _TYPED_VALIDATORS = { diff --git a/pulp-glue/src/pulp_glue/core/context.py b/pulp-glue/src/pulp_glue/core/context.py index cd70a6187..a70e04a77 100644 --- a/pulp-glue/src/pulp_glue/core/context.py +++ b/pulp-glue/src/pulp_glue/core/context.py @@ -32,9 +32,11 @@ def reset(self) -> t.Any: def preprocess_entity(self, body: EntityDefinition, partial: bool = False) -> EntityDefinition: body = super().preprocess_entity(body, partial=partial) - if not self.pulp_ctx.has_plugin(PluginRequirement("core", specifier=">=3.17.0")): - if "creation_hooks" in body: - body["permissions_assignment"] = body.pop("creation_hooks") + if ( + not self.pulp_ctx.has_plugin(PluginRequirement("core", specifier=">=3.17.0")) + and "creation_hooks" in body + ): + body["permissions_assignment"] = body.pop("creation_hooks") return body @@ -294,11 +296,7 @@ def cleanup(self, body: dict[str, t.Any] | None = None) -> t.Any: else: if body: self.pulp_ctx.needs_plugin(PluginRequirement("core", specifier=">=3.14.0")) - if not self.pulp_ctx.fake_mode: - result = self.pulp_ctx.call("orphans_delete") - else: - # Do we need something better? - result = {} + result = {} if self.pulp_ctx.fake_mode else self.pulp_ctx.call("orphans_delete") return result diff --git a/pulp-glue/src/pulp_glue/rpm/context.py b/pulp-glue/src/pulp_glue/rpm/context.py index d85a7cb26..87516d7f6 100644 --- a/pulp-glue/src/pulp_glue/rpm/context.py +++ b/pulp-glue/src/pulp_glue/rpm/context.py @@ -129,9 +129,8 @@ class PulpRpmPackageContext(PulpContentContext): def preprocess_entity(self, body: EntityDefinition, partial: bool = False) -> EntityDefinition: body = super().preprocess_entity(body, partial=partial) - if partial is False: - if body.get("relative_path") is None: - self.pulp_ctx.needs_plugin(PluginRequirement("rpm", specifier=">=3.18.0")) + if partial is False and body.get("relative_path") is None: + self.pulp_ctx.needs_plugin(PluginRequirement("rpm", specifier=">=3.18.0")) return body def list_iterator( diff --git a/pyproject.toml b/pyproject.toml index 1c313912f..68389a76c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ dev = [ lint = [ {include-group = "test"}, "mypy~=1.20.0", - "ruff~=0.15.1", + "ruff~=0.16.0", "shellcheck-py~=0.11.0.1", "types-pygments", "types-pyyaml", @@ -78,15 +78,23 @@ test = [ "secretstorage>=3.5.0", "trustme>=1.1.0,<1.3", ] +docs = [ + "pulp-docs", +] [tool.uv.sources] # This section is managed by the cookiecutter templates. pulp-glue = { workspace = true } +pulp-docs = { git = "https://github.com/pulp/pulp-docs" } [tool.uv.workspace] # This section is managed by the cookiecutter templates. members = ["pulp-glue"] +[tool.uv.dependency-groups] +# This section is managed by the cookiecutter templates. +docs = {requires-python = ">=3.11"} + [tool.uv.build-backend] # This section is managed by the cookiecutter templates. module-name = ["pulpcore.cli", "pulp_cli", "pytest_pulp_cli"] @@ -246,13 +254,18 @@ extend-exclude = ["cookiecutter/bootstrap", "cookiecutter/ci", "cookiecutter/doc [tool.ruff.lint] # This section is managed by the cookiecutter templates. select = ["E4", "E7", "E9", "F"] -extend-select = ["I", "INT", "PTH"] +extend-select = ["FURB", "I", "INT", "PTH", "SIM1", "TID", "T10", "UP"] [tool.ruff.lint.isort] # This section is managed by the cookiecutter templates. sections = { second-party = ["pulp_glue"] } section-order = ["future", "standard-library", "third-party", "second-party", "first-party", "local-folder"] +[tool.ruff.lint.flake8-tidy-imports.banned-api] +# This section is managed by the cookiecutter templates. +"distutils".msg = "The 'distutils' module has been deprecated since Python 3.9." +"pulpcore.cli.common.generic".msg = "This module moved to 'pulp_cli.generic'." + [tool.pytest] testpaths = ["tests", "pulp-glue/tests"] diff --git a/src/pulp_cli/__init__.py b/src/pulp_cli/__init__.py index d5fc022a4..781be2c3c 100644 --- a/src/pulp_cli/__init__.py +++ b/src/pulp_cli/__init__.py @@ -229,15 +229,15 @@ def main( if verbose: logging.basicConfig(level=logging.DEBUG + 4 - verbose, format="%(message)s") - api_kwargs = dict( - base_url=base_url, - headers=dict(header.split(":", maxsplit=1) for header in headers), - verify_ssl=verify_ssl, - refresh_cache=refresh_api, - dry_run=dry_run, - user_agent=f"Pulp-CLI/{__version__}", - cid=cid, - ) + api_kwargs = { + "base_url": base_url, + "headers": dict(header.split(":", maxsplit=1) for header in headers), + "verify_ssl": verify_ssl, + "refresh_cache": refresh_api, + "dry_run": dry_run, + "user_agent": f"Pulp-CLI/{__version__}", + "cid": cid, + } ctx.obj = PulpCLIContext( api_root=api_root, api_kwargs=api_kwargs, diff --git a/src/pulp_cli/config.py b/src/pulp_cli/config.py index a465e7847..9d864db71 100644 --- a/src/pulp_cli/config.py +++ b/src/pulp_cli/config.py @@ -190,9 +190,9 @@ def validate_config(config: dict[str, t.Any], strict: bool = False) -> None: if "domain" in config and not re.match(r"^[-a-zA-Z0-9_]+\Z", config["domain"]): errors.append(_("'domain' must be a slug string")) if "headers" in config: - if not isinstance(config["headers"], list) or not all( - isinstance(header, str) and re.match(HEADER_REGEX, header) - for header in config["headers"] + headers = config["headers"] + if not isinstance(headers, list) or not all( + isinstance(header, str) and re.match(HEADER_REGEX, header) for header in headers ): errors.append(_("'headers' must be a list of strings with a colon separator")) if "plugins" in config and not ( diff --git a/src/pulp_cli/generic.py b/src/pulp_cli/generic.py index 1fed6dba4..391210943 100644 --- a/src/pulp_cli/generic.py +++ b/src/pulp_cli/generic.py @@ -441,10 +441,12 @@ def get_params(self, ctx: click.Context) -> list[click.Parameter]: params = super().get_params(ctx) new_params: list[click.Parameter] = [] for param in params: - if isinstance(param, PulpOption): - if param.allowed_with_contexts is not None: - if not isinstance(ctx.obj, param.allowed_with_contexts): - continue + if ( + isinstance(param, PulpOption) + and param.allowed_with_contexts is not None + and not isinstance(ctx.obj, param.allowed_with_contexts) + ): + continue new_params.append(param) return new_params @@ -1121,7 +1123,7 @@ def _multi_option_callback( ) -> t.Iterable[EntityFieldDefinition]: if value: return [_option_callback(ctx, param, item) for item in value] - return tuple() + return () if "cls" not in kwargs: kwargs["cls"] = PulpOption @@ -1536,10 +1538,9 @@ def callback( """ Show the list of optionally filtered {entities}. """ - if "ordering" in kwargs: - # Workaround for missing ordering filter - if not kwargs["ordering"]: - kwargs["ordering"] = None + # Workaround for missing ordering filter + if "ordering" in kwargs and not kwargs["ordering"]: + kwargs["ordering"] = None result = entity_ctx.list(limit=limit, offset=offset, parameters=kwargs) pulp_ctx.output_result(result) diff --git a/src/pulpcore/cli/common/generic.py b/src/pulpcore/cli/common/generic.py index 54e881a1f..6ccc7a91e 100644 --- a/src/pulpcore/cli/common/generic.py +++ b/src/pulpcore/cli/common/generic.py @@ -1 +1,2 @@ -from pulp_cli.generic import * # noqa +from pulp_cli.generic import * # noqa: F403 +# This module is deprecated. diff --git a/src/pulpcore/cli/common/i18n.py b/src/pulpcore/cli/common/i18n.py index 7cb83a429..66cac85d7 100644 --- a/src/pulpcore/cli/common/i18n.py +++ b/src/pulpcore/cli/common/i18n.py @@ -1,3 +1,3 @@ -from pulp_glue.common.i18n import get_translation # noqa: F401 +from pulp_glue.common.i18n import get_translation __all__ = ["get_translation"] diff --git a/src/pulpcore/cli/core/vulnerability_report.py b/src/pulpcore/cli/core/vulnerability_report.py index 6e4b926ff..726cad634 100644 --- a/src/pulpcore/cli/core/vulnerability_report.py +++ b/src/pulpcore/cli/core/vulnerability_report.py @@ -3,7 +3,7 @@ from pulp_glue.common.i18n import get_translation from pulp_glue.core.context import PulpVulnerabilityReportContext -from pulpcore.cli.common.generic import ( +from pulp_cli.generic import ( PulpCLIContext, href_option, list_command, diff --git a/src/pulpcore/cli/python/remote.py b/src/pulpcore/cli/python/remote.py index b35ecc6f7..8b25854a0 100644 --- a/src/pulpcore/cli/python/remote.py +++ b/src/pulpcore/cli/python/remote.py @@ -41,10 +41,7 @@ def _package_list_callback(ctx: click.Context, param: click.Parameter, value: st if not value: return value - if value.startswith("@"): - json_string = f'["{value[1:]}"]' - else: - json_string = value + json_string = f'["{value[1:]}"]' if value.startswith("@") else value try: json_object = json.loads(json_string) diff --git a/src/pytest_pulp_cli/__init__.py b/src/pytest_pulp_cli/__init__.py index 2527fcb87..3a14e5b40 100644 --- a/src/pytest_pulp_cli/__init__.py +++ b/src/pytest_pulp_cli/__init__.py @@ -50,7 +50,7 @@ def __init__(self, path: pathlib.Path, **kwargs: t.Any): def _runscript( self, pulp_cli_env: dict[str, t.Any], tmp_path: pathlib.Path, pulp_container_log: None ) -> None: - run = subprocess.run([self.path], cwd=tmp_path) + run = subprocess.run([self.path], cwd=tmp_path, check=False) if run.returncode == 23: pytest.skip("Skipped as requested by the script.") if run.returncode != 0: @@ -149,7 +149,7 @@ def pulp_cli_gnupghome(tmp_path_factory: pytest.TempPathFactory) -> pathlib.Path if key_file.exists(): private_key_data = key_file.read_text() else: - private_key_url = "https://github.com/pulp/pulp-fixtures/raw/master/common/GPG-PRIVATE-KEY-fixture-signing" # noqa: E501 + private_key_url = "https://github.com/pulp/pulp-fixtures/raw/master/common/GPG-PRIVATE-KEY-fixture-signing" with urllib.request.urlopen(private_key_url) as response: private_key_data = response.read() key_file.write_bytes(private_key_data) @@ -183,8 +183,6 @@ def pulp_cli_env( for key, value in pulp_cli_vars.items(): monkeypatch.setenv(key, value) - return None - if "PULP_LOGGING" in os.environ: diff --git a/tests/test_help_pages.py b/tests/test_help_pages.py index dbc4ea027..28e9c9ce2 100644 --- a/tests/test_help_pages.py +++ b/tests/test_help_pages.py @@ -18,15 +18,14 @@ def traverse_commands(command: click.Command, args: list[str]) -> t.Iterator[lis yield from traverse_commands(sub, args + [name]) params = command.params - if params: - if "--type" in params[0].opts: - # iterate over commands with specific context types - assert isinstance(params[0].type, click.Choice) - for context_type in params[0].type.choices: - yield args + ["--type", context_type] + if params and "--type" in params[0].opts: + # iterate over commands with specific context types + assert isinstance(params[0].type, click.Choice) + for context_type in params[0].type.choices: + yield args + ["--type", context_type] - for name, sub in command.commands.items(): - yield from traverse_commands(sub, args + ["--type", context_type, name]) + for name, sub in command.commands.items(): + yield from traverse_commands(sub, args + ["--type", context_type, name]) def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: @@ -38,7 +37,7 @@ def pytest_generate_tests(metafunc: pytest.Metafunc) -> None: sub = rel_main.commands[step] assert isinstance(sub, click.Group) rel_main = sub - metafunc.parametrize("args", traverse_commands(rel_main, base_cmd), ids=" ".join) + metafunc.parametrize("args", list(traverse_commands(rel_main, base_cmd)), ids=" ".join) @pytest.fixture @@ -73,7 +72,7 @@ def test_help_shows_all_available_commands(no_api: None) -> None: runner = CliRunner() result = runner.invoke(main, ["--help"], catch_exceptions=False) assert result.exit_code == 0 - for command in main.commands.keys(): + for command in main.commands: assert command in result.stdout