diff --git a/CHANGELOG.md b/CHANGELOG.md index 30267700..3eaccf1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,13 +12,23 @@ The **third number** is the patch version (bug fixes) ## Unreleased +### Added + +- Command: `project repos` to list repositories in a project. A more intuitive way to list repositories for a project than going through `repository list`. +- `--sbom-generation` option for `project create` and `project update` commands to enable automatic SBOM generation for the project. + ### Changed - Styling of multiline help text in commands. +### Removed + +- Mentions of valid values from `project {create,update}` commands. + ### Fixed - REPL closing when certain errors are raised. +- `artifact list` for artifacts with no extra attributes. ## [0.2.2](https://github.com/unioslo/harbor-cli/tree/harbor-cli-v0.2.2) - 2024-03-01 @@ -72,7 +82,6 @@ The **third number** is the patch version (bug fixes) - Double printing of messages in terminal if logging wasn't properly configured. - ## [0.1.0](https://github.com/unioslo/harbor-cli/tree/ca08e7e8830ff3a10e1be447b5555acd5ed672ed) - 2023-12-06 ### Added diff --git a/harbor_cli/commands/api/artifact.py b/harbor_cli/commands/api/artifact.py index 173592aa..8daf898e 100644 --- a/harbor_cli/commands/api/artifact.py +++ b/harbor_cli/commands/api/artifact.py @@ -325,7 +325,7 @@ def create_artifact_tag( """Create a tag for an artifact.""" an = parse_artifact_name(artifact) # NOTE: We might need to fetch repo and artifact IDs - t = Tag(name=tag) # pyright: ignore[reportCallIssue] + t = Tag(name=tag) location = state.run( state.client.create_artifact_tag(an.project, an.repository, an.reference, t), f"Creating tag {tag!r} for {artifact}...", @@ -393,7 +393,7 @@ def add_artifact_label( description=description, color=color, scope=scope, - ) # pyright: ignore[reportCallIssue] + ) state.run( state.client.add_artifact_label(an.project, an.repository, an.reference, label), f"Adding label {label.name!r} to {artifact}...", diff --git a/harbor_cli/commands/api/auditlog.py b/harbor_cli/commands/api/auditlog.py index e63fb795..ee150845 100644 --- a/harbor_cli/commands/api/auditlog.py +++ b/harbor_cli/commands/api/auditlog.py @@ -213,7 +213,7 @@ def _get_schedule( return Schedule( parameters=params, schedule=ScheduleObj(**obj_kwargs), - ) # pyright: ignore[reportCallIssue] + ) # HarborAsyncClient.create_audit_log_rotation_schedule() diff --git a/harbor_cli/commands/api/cve_allowlist.py b/harbor_cli/commands/api/cve_allowlist.py index 4ccc7dcf..9fd8d16f 100644 --- a/harbor_cli/commands/api/cve_allowlist.py +++ b/harbor_cli/commands/api/cve_allowlist.py @@ -86,7 +86,7 @@ def clear_allowlist( ) -> None: """Clear the current CVE allowlist of all CVEs, and optionally all metadata as well.""" if full_clear: - allowlist = CVEAllowlist(items=[]) # pyright: ignore[reportCallIssue] # create a whole new allowlist + allowlist = CVEAllowlist(items=[]) # create a whole new allowlist else: # Fetch existing allowlist to preserve metadata allowlist = state.run( diff --git a/harbor_cli/commands/api/gc.py b/harbor_cli/commands/api/gc.py index 4132826a..59fa89ab 100644 --- a/harbor_cli/commands/api/gc.py +++ b/harbor_cli/commands/api/gc.py @@ -59,9 +59,9 @@ def create_gc_schedule( schedule_obj = ScheduleObj( type=type, cron=cron, - ) # pyright: ignore[reportCallIssue] + ) # TODO: investigate which parameters the `parameters` field takes - schedule = Schedule(schedule=schedule_obj) # pyright: ignore[reportCallIssue] + schedule = Schedule(schedule=schedule_obj) state.run( state.client.create_gc_schedule(schedule), "Creating Garbage Collection schedule...", diff --git a/harbor_cli/commands/api/project.py b/harbor_cli/commands/api/project.py index 41387f6a..3fdf77b5 100644 --- a/harbor_cli/commands/api/project.py +++ b/harbor_cli/commands/api/project.py @@ -87,6 +87,39 @@ def get_project_info( render_result(p, ctx) +# HarborAsyncClient.get_repositories() +@app.command("repos") +@inject_resource_options() +def list_repos( + ctx: typer.Context, + project: Optional[str] = typer.Argument( + help="Name of project to fetch repositories from.", + ), + query: Optional[str] = None, + sort: Optional[str] = None, + page: int = 1, + page_size: int = 10, + limit: Optional[int] = ..., +) -> None: + """List all repositories in a project. + + Alternative to `repository list`""" + repos = state.run( + state.client.get_repositories( + project, + query=query, + sort=sort, + page=page, + page_size=page_size, + limit=limit, + ), + "Fetching repositories...", + ) + if not repos: + info(f"{project!r} has no repositories.") + render_result(repos, ctx) + + # HarborAsyncClient.get_project_logs() @app.command("logs") @inject_resource_options() @@ -143,7 +176,8 @@ def project_exists( @app.command("create", no_args_is_help=True) @inject_help(ProjectReq) @inject_help( - ProjectMetadata + ProjectMetadata, + remove=['The valid values are "true", "false".'], ) # inject this first so its "public" field takes precedence def create_project( ctx: typer.Context, @@ -196,6 +230,11 @@ def create_project( None, "--retention-id", ), + auto_sbom_generation: Optional[bool] = typer.Option( + None, + "--sbom-generation", + is_flag=False, + ), # TODO: add support for adding CVE allowlist when creating a project ) -> None: """Create a new project.""" @@ -203,16 +242,20 @@ def create_project( project_name=project_name, storage_limit=storage_limit, registry_id=registry_id, - metadata=ProjectMetadata( - # validator does bool -> str conversion for the string bool fields - public=public, # type: ignore - enable_content_trust=enable_content_trust, - enable_content_trust_cosign=enable_content_trust_cosign, - prevent_vul=prevent_vul, - severity=severity, - auto_scan=auto_scan, - reuse_sys_cve_allowlist=reuse_sys_cve_allowlist, - retention_id=retention_id, + metadata=ProjectMetadata.model_validate( + # NOTE: Constructing via a dict here to avoid type checking noise. + # See tests/api/test_models.py for more information. + { + "public": public, + "enable_content_trust": enable_content_trust, + "enable_content_trust_cosign": enable_content_trust_cosign, + "prevent_vul": prevent_vul, + "severity": severity, + "auto_scan": auto_scan, + "reuse_sys_cve_allowlist": reuse_sys_cve_allowlist, + "retention_id": retention_id, + "auto_sbom_generation": auto_sbom_generation, + } ), ) location = state.run( @@ -283,7 +326,8 @@ def list_projects( @app.command("update", no_args_is_help=True) @inject_help(ProjectReq) @inject_help( - ProjectMetadata + ProjectMetadata, + remove=['The valid values are "true", "false".'], ) # inject this first so its "public" field takes precedence def update_project( ctx: typer.Context, @@ -336,6 +380,11 @@ def update_project( None, "--retention-id", ), + auto_sbom_generation: Optional[bool] = typer.Option( + None, + "--sbom-generation", + is_flag=False, + ), ) -> None: """Update project information.""" req_params = model_params_from_ctx(ctx, ProjectReq) @@ -346,7 +395,7 @@ def update_project( arg = get_project_arg(project_name_or_id) project = get_project(arg) if project.metadata is None: - project.metadata = ProjectMetadata() # pyright: ignore[reportCallIssue] # mypy bug + project.metadata = ProjectMetadata() # Create updated models from params req = create_updated_model( @@ -748,9 +797,9 @@ def list_project_members( entity_name: Optional[str] = typer.Option( None, "--entity", help="Entity name to search for." ), - page: int = ..., # type: ignore - page_size: int = ..., # type: ignore - limit: Optional[int] = ..., # type: ignore + page: int = ..., + page_size: int = ..., + limit: Optional[int] = ..., ) -> None: """List all members of a project.""" project_arg = get_project_arg(project_name_or_id) diff --git a/harbor_cli/commands/api/registry.py b/harbor_cli/commands/api/registry.py index 6a3b50ec..3f1c653f 100644 --- a/harbor_cli/commands/api/registry.py +++ b/harbor_cli/commands/api/registry.py @@ -92,7 +92,7 @@ def create_registry( type=type, insecure=insecure, description=description, - ) # pyright: ignore[reportCallIssue] + ) location = state.run(state.client.create_registry(registry), "Creating registry...") render_result(location, ctx) diff --git a/harbor_cli/commands/api/repository.py b/harbor_cli/commands/api/repository.py index c9d45d6a..b7da15db 100644 --- a/harbor_cli/commands/api/repository.py +++ b/harbor_cli/commands/api/repository.py @@ -109,7 +109,7 @@ def list_repos( sort: Optional[str] = None, page: int = 1, page_size: int = 10, - limit: Optional[int] = ..., # type: ignore + limit: Optional[int] = ..., ) -> None: """List repositories in all projects or a specific project.""" repos = state.run( diff --git a/harbor_cli/commands/api/retention.py b/harbor_cli/commands/api/retention.py index 00d4562d..5cabc12a 100644 --- a/harbor_cli/commands/api/retention.py +++ b/harbor_cli/commands/api/retention.py @@ -200,9 +200,9 @@ def list_retention_jobs( ctx: typer.Context, project_name_or_id: Optional[str] = ARG_PROJECT_NAME_OR_ID_OPTIONAL, policy_id: Optional[int] = OPTION_POLICY_ID, - page: int = ..., # type: ignore - page_size: int = ..., # type: ignore - limit: Optional[int] = ..., # type: ignore + page: int = ..., + page_size: int = ..., + limit: Optional[int] = ..., ) -> None: """List retention jobs.""" policy_id = policy_id_from_args(project_name_or_id, policy_id) @@ -266,9 +266,9 @@ def list_retention_tasks( project_name_or_id: Optional[str] = ARG_PROJECT_NAME_OR_ID_OPTIONAL, job_id: int = typer.Argument(help="ID of the job to list tasks for."), policy_id: Optional[int] = OPTION_POLICY_ID, - page: int = ..., # type: ignore - page_size: int = ..., # type: ignore - limit: Optional[int] = ..., # type: ignore + page: int = ..., + page_size: int = ..., + limit: Optional[int] = ..., ) -> None: """List retention tasks.""" policy_id = policy_id_from_args(project_name_or_id, policy_id) diff --git a/harbor_cli/commands/api/scan.py b/harbor_cli/commands/api/scan.py index b3aa3de4..aae218cd 100644 --- a/harbor_cli/commands/api/scan.py +++ b/harbor_cli/commands/api/scan.py @@ -176,7 +176,7 @@ def start_scan_export( ) -> None: # TODO: resolve label names to IDs (?) - req = ScanDataExportRequest() # pyright: ignore[reportCallIssue] + req = ScanDataExportRequest() if job_name: req.job_name = job_name if cve: diff --git a/harbor_cli/commands/api/scanall.py b/harbor_cli/commands/api/scanall.py index 0c1040a0..d185f212 100644 --- a/harbor_cli/commands/api/scanall.py +++ b/harbor_cli/commands/api/scanall.py @@ -63,7 +63,7 @@ def create_scanall_schedule( cron: Optional[str] = typer.Option(None), ) -> None: params = model_params_from_ctx(ctx, ScheduleObj) - schedule = Schedule(schedule=ScheduleObj(**params)) # pyright: ignore[reportCallIssue] + schedule = Schedule(schedule=ScheduleObj(**params)) state.run( state.client.create_scan_all_schedule(schedule), "Creating 'Scan All' schedule...", diff --git a/harbor_cli/commands/api/user.py b/harbor_cli/commands/api/user.py index 38e58194..48ab7499 100644 --- a/harbor_cli/commands/api/user.py +++ b/harbor_cli/commands/api/user.py @@ -286,14 +286,14 @@ def unset_user_admin( def set_user_password( username_or_id: str = ARG_USERNAME_OR_ID, old_password: str = typer.Option( - ..., # type: ignore # pyright unable to infer type? + ..., # type: ignore # pyright unable to infer type? "--old-password", prompt="Enter old password", hide_input=True, help="Old password for user. Prompted if not provided.", ), new_password: str = typer.Option( - ..., # type: ignore + ..., # type: ignore # pyright unable to infer type? "--new-password", prompt="Enter new password", hide_input=True, diff --git a/harbor_cli/commands/api/usergroup.py b/harbor_cli/commands/api/usergroup.py index f8578ac1..6d8f1b15 100644 --- a/harbor_cli/commands/api/usergroup.py +++ b/harbor_cli/commands/api/usergroup.py @@ -61,7 +61,7 @@ def create_usergroup( group_name=group_name, group_type=group_type.as_int(), ldap_group_dn=ldap_group_dn, - ) # pyright: ignore[reportCallIssue] + ) location = state.run( state.client.create_usergroup(usergroup), f"Creating user group {group_name}...", @@ -77,7 +77,7 @@ def update_usergroup( # NOTE: make group_name optional if we can update other fields in the future ) -> None: """Update a user group. Only the name can be updated currently.""" - usergroup = UserGroup(group_name=group_name) # pyright: ignore[reportCallIssue] + usergroup = UserGroup(group_name=group_name) state.run( state.client.update_usergroup(group_id, usergroup), f"Updating user group {group_id}...", @@ -115,9 +115,9 @@ def get_usergroups( "--group-name", help="Group name to filter by (fuzzy matching).", ), - page: int = ..., # type: ignore - page_size: int = ..., # type: ignore - limit: Optional[int] = ..., # type: ignore + page: int = ..., + page_size: int = ..., + limit: Optional[int] = ..., ) -> None: """List user groups.""" usergroups = state.run( @@ -141,7 +141,7 @@ def search_usergroups( group_name: str = typer.Argument(help="Name of group to search for."), page: int = 1, page_size: int = 10, - # limit: Optional[int] = ..., # type: ignore # NYI in harborapi + # limit: Optional[int] = ..., # NYI in harborapi ) -> None: """Search for user groups by name.""" usergroups = state.run( diff --git a/harbor_cli/utils/commands.py b/harbor_cli/utils/commands.py index f97193b7..c0f8a029 100644 --- a/harbor_cli/utils/commands.py +++ b/harbor_cli/utils/commands.py @@ -5,6 +5,7 @@ from functools import lru_cache from typing import Any from typing import List +from typing import Optional from typing import Type import click @@ -102,7 +103,10 @@ def get_app_callback_options(app: typer.Typer) -> list[typer.models.OptionInfo]: def inject_help( - model: Type[BaseModel], strict: bool = False, **field_additions: str + model: Type[BaseModel], + strict: bool = False, + remove: Optional[List[str]] = None, + **field_additions: str, ) -> Any: """ Injects a Pydantic model's field descriptions into the help attributes @@ -139,6 +143,8 @@ def my_command(my_field: str = typer.Option(...)): strict : bool If True, fail if a field in the model does not correspond to a function parameter of the same name with a typer.OptionInfo as a default value. + remove: Optional[List[str]] + List of strings to remove from descriptions before injecting them. **field_additions Additional help text to add to the help attribute of a field. The parameter name should be the name of the field, and the value @@ -164,7 +170,13 @@ def decorator(func: Any) -> Any: addition = field_additions.get(field_name, "") if addition: addition = f" {addition}" # add leading space - param.default.help = f"{field.description or ''}{addition}" + description = field.description or "" + if remove: + for to_remove in remove: + # Could this be faster with a regex? + description = description.replace(to_remove, "") + description = description.strip() + param.default.help = f"{description}{addition}" return func return decorator diff --git a/pyproject.toml b/pyproject.toml index d8af2d88..201ba711 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ ] dependencies = [ "typer==0.9.0", - "harborapi>=0.25.1", + "harborapi>=0.25.2", "pydantic>=2.7.4", "trogon>=0.5.0", "platformdirs>=2.5.4", @@ -134,7 +134,7 @@ typeCheckingMode = "strict" [tool.ruff] src = ["harbor_cli"] -extend-exclude = ["tests", "harbor_cli/__init__.py"] +extend-exclude = ["harbor_cli/__init__.py"] [tool.ruff.lint] extend-select = ["I"] diff --git a/tests/_strategies.py b/tests/_strategies.py index a55d22be..8305b2be 100644 --- a/tests/_strategies.py +++ b/tests/_strategies.py @@ -9,12 +9,11 @@ from pydantic import ValidationError if TYPE_CHECKING: - from pydantic import BaseModel # noqa: F401 from hypothesis.strategies import SearchStrategy # noqa: F401 + from pydantic import BaseModel # noqa: F401 from harbor_cli.output.table import RENDER_FUNCTIONS - COMPACT_TABLE_MODELS = [] # type: list[SearchStrategy[BaseModel | list[BaseModel]]] for model in RENDER_FUNCTIONS.keys(): try: diff --git a/tests/_utils.py b/tests/_utils.py index 57f0546d..a75b67ba 100644 --- a/tests/_utils.py +++ b/tests/_utils.py @@ -4,9 +4,8 @@ from typing import NamedTuple from typing import Optional -from pydantic import ValidationError - from harbor_cli.output.table import RENDER_FUNCTIONS +from pydantic import ValidationError class Parameter(NamedTuple): diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/api/test_models.py b/tests/api/test_models.py new file mode 100644 index 00000000..1321d049 --- /dev/null +++ b/tests/api/test_models.py @@ -0,0 +1,48 @@ +"""Tests for models from the harborapi package. + +In certain places, we take advantage of custom validators on +the models from the package to perform things like type coercion +and extra validation. + +These tests ensure that the custom validators work as expected.""" + +from __future__ import annotations + +import pytest +from harborapi.models import ProjectMetadata + + +@pytest.mark.parametrize("inp,expect", [(True, "true"), (False, "false"), (None, None)]) +def test_project_metadata_bool_to_string(inp: bool, expect: str | None) -> None: + """Test that the project metadata model can convert a bool to a string.""" + metadata = ProjectMetadata( + # validator does bool -> str conversion for the string bool fields + public=inp, + enable_content_trust=inp, + enable_content_trust_cosign=inp, + prevent_vul=inp, + auto_scan=inp, + reuse_sys_cve_allowlist=inp, + auto_sbom_generation=inp, + ) + assert metadata.public == expect + assert metadata.enable_content_trust == expect + assert metadata.enable_content_trust_cosign == expect + assert metadata.prevent_vul == expect + assert metadata.auto_scan == expect + assert metadata.reuse_sys_cve_allowlist == expect + assert metadata.auto_sbom_generation == expect + + # Test with dict input + metadata_from_dict = ProjectMetadata.model_validate( + { + "public": inp, + "enable_content_trust": inp, + "enable_content_trust_cosign": inp, + "prevent_vul": inp, + "auto_scan": inp, + "reuse_sys_cve_allowlist": inp, + "auto_sbom_generation": inp, + } + ) + assert metadata == metadata_from_dict diff --git a/tests/commands/cli/test_cli_config.py b/tests/commands/cli/test_cli_config.py index 6a3918cd..955889bd 100644 --- a/tests/commands/cli/test_cli_config.py +++ b/tests/commands/cli/test_cli_config.py @@ -3,8 +3,6 @@ import os from pathlib import Path -from pytest import LogCaptureFixture - from harbor_cli.config import EnvVar from harbor_cli.config import HarborCLIConfig from harbor_cli.format import OutputFormat diff --git a/tests/commands/cli/test_find.py b/tests/commands/cli/test_find.py index 47ef9de9..813c2f8a 100644 --- a/tests/commands/cli/test_find.py +++ b/tests/commands/cli/test_find.py @@ -2,12 +2,12 @@ import pytest import typer - -from ..._utils import Parameter -from harbor_cli.commands.cli.find import _do_find from harbor_cli.commands.cli.find import MatchStrategy +from harbor_cli.commands.cli.find import _do_find from harbor_cli.models import CommandSummary +from ..._utils import Parameter + # Tests for the underlying _do_find() function diff --git a/tests/conftest.py b/tests/conftest.py index 0fb8a4d3..e4c977c7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,11 @@ from __future__ import annotations import os -from copy import deepcopy from functools import partial from pathlib import Path +from typing import IO from typing import Any from typing import Generator -from typing import IO from typing import Iterator from typing import Mapping from typing import Protocol @@ -14,11 +13,6 @@ import click import pytest import typer -from harborapi import HarborAsyncClient -from pydantic import SecretStr -from typer.testing import CliRunner -from typer.testing import Result - from harbor_cli import logs from harbor_cli import state from harbor_cli.config import EnvVar @@ -26,6 +20,10 @@ from harbor_cli.format import OutputFormat from harbor_cli.main import app as main_app from harbor_cli.utils.keyring import keyring_supported +from harborapi import HarborAsyncClient +from pydantic import SecretStr +from typer.testing import CliRunner +from typer.testing import Result runner = CliRunner(mix_stderr=False) @@ -136,8 +134,7 @@ def __call__( catch_exceptions: bool = True, color: bool = False, **extra: Any, - ) -> Result: - ... + ) -> Result: ... @pytest.fixture diff --git a/tests/output/formatting/test_builtin.py b/tests/output/formatting/test_builtin.py index 933113ee..f5f8f6d9 100644 --- a/tests/output/formatting/test_builtin.py +++ b/tests/output/formatting/test_builtin.py @@ -4,19 +4,17 @@ from typing import Sequence import pytest -from harborapi.models.base import BaseModel - +from harbor_cli.output.formatting.builtin import NONE_STR from harbor_cli.output.formatting.builtin import bool_str from harbor_cli.output.formatting.builtin import float_str from harbor_cli.output.formatting.builtin import int_str -from harbor_cli.output.formatting.builtin import NONE_STR from harbor_cli.output.formatting.builtin import plural_str from harbor_cli.output.formatting.constants import FALSE_STR from harbor_cli.output.formatting.constants import TRUE_STR from harbor_cli.state import get_state from harbor_cli.style import EMOJI_NO from harbor_cli.style import EMOJI_YES - +from harborapi.models.base import BaseModel state = get_state() diff --git a/tests/output/formatting/test_bytes.py b/tests/output/formatting/test_bytes.py index 5981e71f..d5826fe1 100644 --- a/tests/output/formatting/test_bytes.py +++ b/tests/output/formatting/test_bytes.py @@ -1,7 +1,6 @@ from __future__ import annotations import pytest - from harbor_cli.output.formatting.bytes import bytesize_str from harbor_cli.output.formatting.constants import NONE_STR diff --git a/tests/output/formatting/test_dates.py b/tests/output/formatting/test_dates.py index 238d74e7..f404c9a4 100644 --- a/tests/output/formatting/test_dates.py +++ b/tests/output/formatting/test_dates.py @@ -3,7 +3,6 @@ from datetime import datetime import pytest - from harbor_cli.output.formatting.dates import datetime_str diff --git a/tests/output/table/test_table.py b/tests/output/table/test_table.py index ca44312e..399250bd 100644 --- a/tests/output/table/test_table.py +++ b/tests/output/table/test_table.py @@ -5,15 +5,15 @@ import pytest import rich +from harbor_cli.output.table import BuiltinTypeException +from harbor_cli.output.table import EmptySequenceError +from harbor_cli.output.table import get_renderable from hypothesis import assume from hypothesis import given from hypothesis import strategies as st from pydantic import BaseModel from ..._strategies import COMPACT_TABLE_MODELS -from harbor_cli.output.table import BuiltinTypeException -from harbor_cli.output.table import EmptySequenceError -from harbor_cli.output.table import get_renderable T = TypeVar("T", bound=BaseModel) diff --git a/tests/output/test_color.py b/tests/output/test_color.py index 55bdfa08..405d5b49 100644 --- a/tests/output/test_color.py +++ b/tests/output/test_color.py @@ -4,13 +4,12 @@ from typing import Tuple import pytest - -from harbor_cli.style.color import Color from harbor_cli.style.color import COLOR_FUNCTIONS -from harbor_cli.style.color import fallback_color -from harbor_cli.style.color import get_color_func +from harbor_cli.style.color import Color from harbor_cli.style.color import Severity from harbor_cli.style.color import SeverityColor +from harbor_cli.style.color import fallback_color +from harbor_cli.style.color import get_color_func def test_from_severity(): diff --git a/tests/output/test_console.py b/tests/output/test_console.py index 457b8f4e..cbf159aa 100644 --- a/tests/output/test_console.py +++ b/tests/output/test_console.py @@ -5,9 +5,6 @@ from typing import Callable import pytest -from pytest import CaptureFixture -from pytest import LogCaptureFixture - from harbor_cli.config import LoggingSettings from harbor_cli.logs import LogLevel from harbor_cli.logs import update_logging @@ -16,6 +13,8 @@ from harbor_cli.output.console import success from harbor_cli.output.console import warning from harbor_cli.state import State +from pytest import CaptureFixture +from pytest import LogCaptureFixture def _configure_logging( diff --git a/tests/output/test_prompts.py b/tests/output/test_prompts.py index bf65ec54..a62953bc 100644 --- a/tests/output/test_prompts.py +++ b/tests/output/test_prompts.py @@ -8,14 +8,6 @@ from typing import Iterator import pytest -from hypothesis import assume -from hypothesis import given -from hypothesis import HealthCheck -from hypothesis import settings -from hypothesis import strategies as st -from pytest import CaptureFixture -from pytest import MonkeyPatch - from harbor_cli.output.prompts import float_prompt from harbor_cli.output.prompts import int_prompt from harbor_cli.output.prompts import path_prompt @@ -23,6 +15,13 @@ from harbor_cli.output.prompts import str_prompt from harbor_cli.style.color import green from harbor_cli.style.style import Icon +from hypothesis import HealthCheck +from hypothesis import assume +from hypothesis import given +from hypothesis import settings +from hypothesis import strategies as st +from pytest import CaptureFixture +from pytest import MonkeyPatch # TODO: test defaults diff --git a/tests/output/test_render.py b/tests/output/test_render.py index f49f0eca..d0fb4972 100644 --- a/tests/output/test_render.py +++ b/tests/output/test_render.py @@ -5,8 +5,14 @@ from unittest.mock import patch import pytest -from hypothesis import given +from harbor_cli.format import OutputFormat +from harbor_cli.output import render +from harbor_cli.output.render import render_json +from harbor_cli.output.render import render_result +from harbor_cli.output.render import render_table +from harbor_cli.state import State from hypothesis import HealthCheck +from hypothesis import given from hypothesis import settings from hypothesis import strategies as st from pydantic import BaseModel @@ -14,12 +20,6 @@ from pytest_mock import MockerFixture from .._strategies import COMPACT_TABLE_MODELS -from harbor_cli.format import OutputFormat -from harbor_cli.output import render -from harbor_cli.output.render import render_json -from harbor_cli.output.render import render_result -from harbor_cli.output.render import render_table -from harbor_cli.state import State # The actual testing of the render functions is done in test_render_() diff --git a/tests/test_app.py b/tests/test_app.py index 2a9db327..230f35d3 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,14 +1,17 @@ from __future__ import annotations -from enum import Enum from enum import IntEnum from pathlib import Path from typing import Iterable -import keyring import pytest import pytest_mock import typer +from harbor_cli.config import EnvVar +from harbor_cli.format import OutputFormat +from harbor_cli.state import State +from harbor_cli.utils.keyring import delete_password +from harbor_cli.utils.keyring import set_password from harborapi.utils import get_basicauth from keyring.errors import NoKeyringError from keyring.errors import PasswordDeleteError @@ -16,13 +19,6 @@ from .conftest import PartialInvoker from .conftest import requires_keyring -from harbor_cli.config import EnvVar -from harbor_cli.format import OutputFormat -from harbor_cli.state import State -from harbor_cli.utils.keyring import delete_password -from harbor_cli.utils.keyring import get_backend -from harbor_cli.utils.keyring import keyring_supported -from harbor_cli.utils.keyring import set_password def test_envvars( diff --git a/tests/test_config.py b/tests/test_config.py index e6b8ca31..80e24d66 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -3,30 +3,29 @@ import copy from pathlib import Path -import keyring import pytest import tomli from freezegun import freeze_time -from hypothesis import given -from hypothesis import strategies as st -from pydantic import ValidationError -from pytest import CaptureFixture -from pytest import LogCaptureFixture - -from .conftest import requires_keyring -from .conftest import requires_no_keyring from harbor_cli.config import HarborCLIConfig from harbor_cli.config import HarborSettings +from harbor_cli.config import LoggingSettings +from harbor_cli.config import TableSettings +from harbor_cli.config import TableStyleSettings from harbor_cli.config import load_config from harbor_cli.config import load_toml_file -from harbor_cli.config import LoggingSettings from harbor_cli.config import sample_config from harbor_cli.config import save_config -from harbor_cli.config import TableSettings -from harbor_cli.config import TableStyleSettings from harbor_cli.output.console import warning from harbor_cli.state import State from harbor_cli.utils.keyring import set_password +from hypothesis import given +from hypothesis import strategies as st +from pydantic import ValidationError +from pytest import CaptureFixture +from pytest import LogCaptureFixture + +from .conftest import requires_keyring +from .conftest import requires_no_keyring def test_save_config(tmp_path: Path, config: HarborCLIConfig) -> None: @@ -399,8 +398,8 @@ def test_loggingsettings_path_notime(config: HarborCLIConfig, tmp_path: Path) -> def test_loggingsettings_datetime_format_deprecated_name() -> None: """Test that we can still use the old name for the datetime_format field.""" - l = LoggingSettings(timeformat="%Y-%m-%d") # type: ignore - assert l.datetime_format == "%Y-%m-%d" + settings = LoggingSettings(timeformat="%Y-%m-%d") # type: ignore + assert settings.datetime_format == "%Y-%m-%d" @freeze_time("1970-01-01") diff --git a/tests/test_deprecation.py b/tests/test_deprecation.py index c00e7005..59700d7e 100644 --- a/tests/test_deprecation.py +++ b/tests/test_deprecation.py @@ -3,14 +3,13 @@ from typing import Optional import typer -from pytest import LogCaptureFixture -from pytest_mock import MockerFixture -from typer.testing import CliRunner - from harbor_cli.deprecation import Deprecated from harbor_cli.deprecation import get_deprecated_params from harbor_cli.deprecation import get_used_deprecated_params from harbor_cli.deprecation import issue_deprecation_warnings +from pytest import LogCaptureFixture +from pytest_mock import MockerFixture +from typer.testing import CliRunner app = typer.Typer() runner = CliRunner(mix_stderr=False) diff --git a/tests/test_keyring.py b/tests/test_keyring.py index 602d9f66..45261b90 100644 --- a/tests/test_keyring.py +++ b/tests/test_keyring.py @@ -1,12 +1,12 @@ from __future__ import annotations import pytest +from harbor_cli.utils.keyring import KeyringUnsupportedError +from harbor_cli.utils.keyring import get_password +from harbor_cli.utils.keyring import set_password from .conftest import requires_keyring from .conftest import requires_no_keyring -from harbor_cli.utils.keyring import get_password -from harbor_cli.utils.keyring import KeyringUnsupportedError -from harbor_cli.utils.keyring import set_password @requires_keyring diff --git a/tests/test_logs.py b/tests/test_logs.py index e8931207..4e25f143 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -5,10 +5,9 @@ from pathlib import Path from harbor_cli.config import HarborCLIConfig +from harbor_cli.logs import LogLevel from harbor_cli.logs import disable_logging from harbor_cli.logs import logger -from harbor_cli.logs import LogLevel -from harbor_cli.logs import replace_handler from harbor_cli.logs import setup_logging from harbor_cli.logs import update_logging diff --git a/tests/test_models.py b/tests/test_models.py index 643a998a..f44c147d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,7 +1,6 @@ from __future__ import annotations import pytest - from harbor_cli.models import MemberRoleType from harbor_cli.models import UserGroupType diff --git a/tests/test_option.py b/tests/test_option.py index 1479b45d..b411bc10 100644 --- a/tests/test_option.py +++ b/tests/test_option.py @@ -1,11 +1,10 @@ from __future__ import annotations import typer -from typer.models import OptionInfo - from harbor_cli.config import env_var -from harbor_cli.option import help_config_override from harbor_cli.option import Option +from harbor_cli.option import help_config_override +from typer.models import OptionInfo def test_option() -> None: diff --git a/tests/test_repl.py b/tests/test_repl.py index 751d7bfa..3f4e4906 100644 --- a/tests/test_repl.py +++ b/tests/test_repl.py @@ -1,16 +1,13 @@ from __future__ import annotations -import os - -import pytest import typer -from pytest_mock import MockFixture - -from .conftest import PartialInvoker from harbor_cli.format import OutputFormat from harbor_cli.models import BaseModel from harbor_cli.output import render from harbor_cli.state import State +from pytest_mock import MockFixture + +from .conftest import PartialInvoker def test_repl_reset_between_commands( diff --git a/tests/test_state.py b/tests/test_state.py index 08353af5..054da0c2 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -5,12 +5,11 @@ from base64 import b64encode import pytest -from harborapi import HarborAsyncClient - from harbor_cli.config import HarborCLIConfig from harbor_cli.output.console import console -from harbor_cli.state import get_state from harbor_cli.state import State +from harbor_cli.state import get_state +from harborapi import HarborAsyncClient def test_state_run_nohandle(state: State) -> None: diff --git a/tests/test_style.py b/tests/test_style.py index 26576685..b850a5b9 100644 --- a/tests/test_style.py +++ b/tests/test_style.py @@ -3,15 +3,14 @@ from typing import Any import pytest -from harborapi.models.scanner import Severity -from strenum import StrEnum - from harbor_cli.style import color from harbor_cli.style.color import HealthColor from harbor_cli.style.color import SeverityColor from harbor_cli.style.markup import CODEBLOCK_STYLES from harbor_cli.style.markup import markup_as_plain_text from harbor_cli.style.markup import markup_to_markdown +from harborapi.models.scanner import Severity +from strenum import StrEnum @pytest.mark.parametrize( @@ -60,7 +59,10 @@ def test_markup_to_markdown_code_styles(style: str) -> None: def test_markup_to_markdown_emoji() -> None: """Test that emoji are converted to unicode characters and play nice when nested inside other styles""" s = f":sparkles: [{CODEBLOCK_STYLES[0]}]Hello world :desktop_computer:[/] foo [bold]:sparkles: bar :sparkles:[/] [bold italic]:sparkles: baz :sparkles:[/] :sparkles:" - assert markup_to_markdown(s) == "✨ `Hello world 🖥` foo **✨ bar ✨** ***✨ baz ✨*** ✨" + assert ( + markup_to_markdown(s) + == "✨ `Hello world 🖥` foo **✨ bar ✨** ***✨ baz ✨*** ✨" + ) @pytest.mark.parametrize( diff --git a/tests/test_types.py b/tests/test_types.py index 6510a335..894d5bc2 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -2,6 +2,7 @@ When we drop 3.8, we can move on to using built-ins as generics. """ + from __future__ import annotations from typing import Any @@ -11,7 +12,6 @@ from typing import Type import pytest - from harbor_cli.types import assert_type from harbor_cli.types import is_sequence_func diff --git a/tests/utils/test_args.py b/tests/utils/test_args.py index 9a226d26..2d8ac89a 100644 --- a/tests/utils/test_args.py +++ b/tests/utils/test_args.py @@ -10,8 +10,6 @@ import pytest import typer -from pydantic import BaseModel - from harbor_cli.utils.args import add_to_query from harbor_cli.utils.args import as_query from harbor_cli.utils.args import construct_query_list @@ -23,6 +21,7 @@ from harbor_cli.utils.args import model_params_from_ctx from harbor_cli.utils.args import parse_commalist from harbor_cli.utils.args import parse_key_value_args +from pydantic import BaseModel class Model(BaseModel): diff --git a/tests/utils/test_commands.py b/tests/utils/test_commands.py index 42a2bd6a..e011b36f 100644 --- a/tests/utils/test_commands.py +++ b/tests/utils/test_commands.py @@ -5,16 +5,15 @@ import click import pytest import typer -from pydantic import BaseModel -from pydantic import Field -from typer import Context -from typer.models import CommandInfo - from harbor_cli.utils.commands import get_app_commands from harbor_cli.utils.commands import get_command_help from harbor_cli.utils.commands import get_parent_ctx from harbor_cli.utils.commands import inject_help from harbor_cli.utils.commands import inject_resource_options +from pydantic import BaseModel +from pydantic import Field +from typer import Context +from typer.models import CommandInfo def test_get_parent_ctx() -> None: diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index 076864e2..1bcca9b7 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -3,9 +3,6 @@ from typing import Any import pytest -from pydantic import ValidationError - -from harbor_cli.config import HarborCLIConfig from harbor_cli.utils.utils import PackageVersion from harbor_cli.utils.utils import parse_version_string from harbor_cli.utils.utils import replace_none @@ -67,8 +64,8 @@ def test_replace_none(d: dict[str, Any], expected: dict[str, Any]) -> None: ), ( # deeply nested tuple - {"a": 1, "b": ((({"c": 2}, {"d": None})))}, - {"a": 1, "b": (((({"c": 2}, {"d": ""}))))}, + {"a": 1, "b": (({"c": 2}, {"d": None}))}, + {"a": 1, "b": (({"c": 2}, {"d": ""}))}, ), ], )