diff --git a/concoursetools/__init__.py b/concoursetools/__init__.py index 4d20462..51a5f36 100644 --- a/concoursetools/__init__.py +++ b/concoursetools/__init__.py @@ -2,6 +2,7 @@ """ A Python package for easily implementing Concourse resource types. """ + from concoursetools.metadata import BuildMetadata from concoursetools.resource import ConcourseResource from concoursetools.version import TypedVersion, Version diff --git a/concoursetools/__main__.py b/concoursetools/__main__.py index 828b22a..5cb6143 100644 --- a/concoursetools/__main__.py +++ b/concoursetools/__main__.py @@ -4,6 +4,7 @@ $ python3 -m concoursetools --help """ + import sys from concoursetools.cli import cli diff --git a/concoursetools/additional.py b/concoursetools/additional.py index a7b69d2..30b66e8 100644 --- a/concoursetools/additional.py +++ b/concoursetools/additional.py @@ -2,6 +2,7 @@ """ Concourse Tools comes with some additional resource type "patterns" to cover some common requirements. """ + from __future__ import annotations from abc import abstractmethod @@ -13,7 +14,13 @@ from concoursetools import ConcourseResource from concoursetools.metadata import BuildMetadata -from concoursetools.typing import Metadata, ResourceConfig, SortableVersionT, VersionConfig, VersionT +from concoursetools.typing import ( + Metadata, + ResourceConfig, + SortableVersionT, + VersionConfig, + VersionT, +) from concoursetools.version import TypedVersion, Version @@ -28,10 +35,15 @@ class OutOnlyConcourseResource(ConcourseResource[VersionT]): :param version_class: The resource parses all inputs with this version class. """ - def fetch_new_versions(self, previous_version: VersionT | None = None) -> list[VersionT]: + + def fetch_new_versions( + self, previous_version: VersionT | None = None + ) -> list[VersionT]: return [] - def download_version(self, version: VersionT, destination_dir: Path, build_metadata: BuildMetadata) -> tuple[VersionT, Metadata]: + def download_version( + self, version: VersionT, destination_dir: Path, build_metadata: BuildMetadata + ) -> tuple[VersionT, Metadata]: metadata: dict[str, str] = {} return version, metadata @@ -41,6 +53,7 @@ class DatetimeVersion(TypedVersion): """ A placeholder version containing only the time at which it was created. """ + execution_date: datetime @classmethod @@ -77,23 +90,35 @@ class InOnlyConcourseResource(ConcourseResource[DatetimeVersion]): This resource used a pre-defined version consisting of a single :class:`~datetime.datetime` object to ensure unique, non-empty versions. """ + def __init__(self) -> None: super().__init__(DatetimeVersion) - def fetch_new_versions(self, previous_version: DatetimeVersion | None = None) -> list[DatetimeVersion]: + def fetch_new_versions( + self, previous_version: DatetimeVersion | None = None + ) -> list[DatetimeVersion]: return [] - def download_version(self, version: DatetimeVersion, destination_dir: Path, build_metadata: BuildMetadata, - **kwargs: object) -> tuple[DatetimeVersion, Metadata]: + def download_version( + self, + version: DatetimeVersion, + destination_dir: Path, + build_metadata: BuildMetadata, + **kwargs: object, + ) -> tuple[DatetimeVersion, Metadata]: metadata = self.download_data(destination_dir, build_metadata, **kwargs) return version, metadata - def publish_new_version(self, sources_dir: Path, build_metadata: BuildMetadata) -> tuple[DatetimeVersion, Metadata]: + def publish_new_version( + self, sources_dir: Path, build_metadata: BuildMetadata + ) -> tuple[DatetimeVersion, Metadata]: version = DatetimeVersion.now() return version, {} @abstractmethod - def download_data(self, destination_dir: Path, build_metadata: BuildMetadata) -> Metadata: + def download_data( + self, destination_dir: Path, build_metadata: BuildMetadata + ) -> Metadata: """ Download resource data and place files within the resource directory in your pipeline. @@ -160,7 +185,10 @@ class TriggerOnChangeConcourseResource(ConcourseResource[VersionT]): :param version_class: The resource parses all inputs with this version class. """ - def fetch_new_versions(self, previous_version: VersionT | None = None) -> list[VersionT]: + + def fetch_new_versions( + self, previous_version: VersionT | None = None + ) -> list[VersionT]: latest_version = self.fetch_latest_version() if previous_version is None: @@ -193,6 +221,7 @@ class MultiVersion(Version, Generic[SortableVersionT]): .. tip:: Two multi-versions are equal if their respective set of subversions are also equal. """ + _key: str = "versions" _sub_version_class: type[SortableVersionT] = Version # type: ignore[assignment] @@ -236,7 +265,9 @@ def to_flat_dict(self) -> VersionConfig: } @classmethod - def from_flat_dict(cls: "type[MultiVersion[SortableVersionT]]", version_dict: VersionConfig) -> "MultiVersion[SortableVersionT]": + def from_flat_dict( + cls: "type[MultiVersion[SortableVersionT]]", version_dict: VersionConfig + ) -> "MultiVersion[SortableVersionT]": """ Load an instance from a dictionary representing the version. @@ -248,12 +279,18 @@ def from_flat_dict(cls: "type[MultiVersion[SortableVersionT]]", version_dict: Ve """ data = version_dict[cls._key] sub_version_dicts: list[VersionConfig] = json.loads(data) - versions = {cls._sub_version_class.from_flat_dict(sub_version_dict) for sub_version_dict in sub_version_dicts} + versions = { + cls._sub_version_class.from_flat_dict(sub_version_dict) + for sub_version_dict in sub_version_dicts + } return cls(versions) -def _create_multi_version_class(key: str, sub_version_class: type[SortableVersionT]) -> type[MultiVersion[SortableVersionT]]: +def _create_multi_version_class( + key: str, sub_version_class: type[SortableVersionT] +) -> type[MultiVersion[SortableVersionT]]: """Create a new version subclass containing multiple sub-versions.""" + class NewMultiVersion(MultiVersion[SortableVersionT]): _key = key _sub_version_class = sub_version_class @@ -261,7 +298,9 @@ class NewMultiVersion(MultiVersion[SortableVersionT]): return NewMultiVersion -class MultiVersionConcourseResource(TriggerOnChangeConcourseResource[MultiVersion[SortableVersionT]]): +class MultiVersionConcourseResource( + TriggerOnChangeConcourseResource[MultiVersion[SortableVersionT]] +): """ A Concourse resource type designed to trigger to a change in available versions. @@ -288,6 +327,7 @@ class MultiVersionConcourseResource(TriggerOnChangeConcourseResource[MultiVersio This resource class is best suited to resources used in conjunction with the :concourse:`set-pipeline-step`. """ + def __init__(self, key: str, sub_version_class: type[SortableVersionT]): self.key = key multi_version_class = _create_multi_version_class(key, sub_version_class) @@ -306,8 +346,14 @@ def fetch_latest_sub_versions(self) -> set[SortableVersionT]: :returns: A set of the latest subversions from the resource. """ - def download_version(self, version: MultiVersion[SortableVersionT], destination_dir: Path, build_metadata: BuildMetadata, - file_name: str | None = None, indent: int | None = None) -> tuple[MultiVersion[SortableVersionT], Metadata]: + def download_version( + self, + version: MultiVersion[SortableVersionT], + destination_dir: Path, + build_metadata: BuildMetadata, + file_name: str | None = None, + indent: int | None = None, + ) -> tuple[MultiVersion[SortableVersionT], Metadata]: """ Download a JSON file containing the sub-version data. @@ -322,7 +368,9 @@ def download_version(self, version: MultiVersion[SortableVersionT], destination_ file_path.write_text(json.dumps(version.sub_version_data, indent=indent)) return version, {} - def publish_new_version(self, sources_dir: Path, build_metadata: BuildMetadata) -> tuple[MultiVersion[SortableVersionT], Metadata]: + def publish_new_version( + self, sources_dir: Path, build_metadata: BuildMetadata + ) -> tuple[MultiVersion[SortableVersionT], Metadata]: raise TypeError("Publishing new versions of this resource is not permitted.") @@ -348,7 +396,10 @@ class SelfOrganisingConcourseResource(ConcourseResource[SortableVersionT]): :param version_class: The resource parses all inputs with this version class. """ - def fetch_new_versions(self, previous_version: SortableVersionT | None = None) -> list[SortableVersionT]: + + def fetch_new_versions( + self, previous_version: SortableVersionT | None = None + ) -> list[SortableVersionT]: all_versions = self.fetch_all_versions() try: newest_version = max(all_versions) @@ -360,7 +411,9 @@ def fetch_new_versions(self, previous_version: SortableVersionT | None = None) - if previous_version is None: return [newest_version] - versions = sorted(version for version in all_versions if previous_version < version) + versions = sorted( + version for version in all_versions if previous_version < version + ) if not versions: versions = [previous_version] @@ -381,13 +434,13 @@ def fetch_all_versions(self) -> set[SortableVersionT]: class _PseudoConcourseResource(ConcourseResource[VersionT]): - def __new__(cls) -> "_PseudoConcourseResource[VersionT]": raise TypeError(f"Cannot instantiate a {cls.__name__} type") -def combine_resource_types(resources: dict[str, type[ConcourseResource[VersionT]]], - param_key: str = "resource") -> type[_PseudoConcourseResource[VersionT]]: +def combine_resource_types( + resources: dict[str, type[ConcourseResource[VersionT]]], param_key: str = "resource" +) -> type[_PseudoConcourseResource[VersionT]]: """ Return a pseudo-resource which will delegate to other resources depending on a flag. @@ -420,12 +473,16 @@ def combine_resource_types(resources: dict[str, type[ConcourseResource[VersionT] resource: A ... """ + class MultiResourceConcourseResource(_PseudoConcourseResource[VersionT]): """ A special Resource class which delegates to multiple other resource classes. """ + @classmethod - def _from_resource_config(cls, resource_config: ResourceConfig) -> "ConcourseResource[VersionT]": + def _from_resource_config( + cls, resource_config: ResourceConfig + ) -> "ConcourseResource[VersionT]": try: resource_key = resource_config.pop(param_key) except KeyError as error: @@ -435,8 +492,10 @@ def _from_resource_config(cls, resource_config: ResourceConfig) -> "ConcourseRes resource_class = resources[resource_key] except KeyError as error: possible = set(resources) - raise KeyError(f"Couldn't find resource matching {resource_key!r}: " - f"possible options: {possible}") from error + raise KeyError( + f"Couldn't find resource matching {resource_key!r}: " + f"possible options: {possible}" + ) from error return resource_class(**resource_config) diff --git a/concoursetools/cli/__init__.py b/concoursetools/cli/__init__.py index 048e8f4..ee0557b 100644 --- a/concoursetools/cli/__init__.py +++ b/concoursetools/cli/__init__.py @@ -2,6 +2,7 @@ """ Contains the Concourse Tools CLI. """ + from __future__ import annotations from concoursetools.cli.commands import cli diff --git a/concoursetools/cli/commands.py b/concoursetools/cli/commands.py index b3e3432..ffe5737 100644 --- a/concoursetools/cli/commands.py +++ b/concoursetools/cli/commands.py @@ -2,6 +2,7 @@ """ Commands for the Concourse Tools CLI. """ + from __future__ import annotations from pathlib import Path @@ -20,8 +21,14 @@ @cli.register(allow_short={"executable", "class_name", "resource_file"}) -def assets(path: str, /, *, executable: str | None = None, resource_file: str = "concourse.py", - class_name: str | None = None) -> None: +def assets( + path: str, + /, + *, + executable: str | None = None, + resource_file: str = "concourse.py", + class_name: str | None = None, +) -> None: """ Create the assets script directory. @@ -31,8 +38,11 @@ def assets(path: str, /, *, executable: str | None = None, resource_file: str = :param resource_file: The path to the module containing the resource class. Defaults to 'concourse.py'. :param class_name: The name of the resource class in the module, if there are multiple. """ - resource_class = import_single_class_from_module(Path(resource_file), parent_class=ConcourseResource, # type: ignore[type-abstract] - class_name=class_name) + resource_class = import_single_class_from_module( + Path(resource_file), + parent_class=ConcourseResource, # type: ignore[type-abstract] + class_name=class_name, + ) assets_folder = Path(path) assets_folder.mkdir(parents=True, exist_ok=True) @@ -43,15 +53,32 @@ def assets(path: str, /, *, executable: str | None = None, resource_file: str = } for file_name, method_name in file_name_to_method_name.items(): file_path = assets_folder / file_name - dockertools.create_script_file(file_path, resource_class, method_name, - executable or dockertools.DEFAULT_EXECUTABLE) + dockertools.create_script_file( + file_path, + resource_class, + method_name, + executable or dockertools.DEFAULT_EXECUTABLE, + ) @cli.register(allow_short={"executable", "class_name", "resource_file"}) -def dockerfile(path: str, /, *, executable: str | None = None, image: str = "python", tag: str | None = None, - suffix: str | None = None, resource_file: str = "concourse.py", class_name: str | None = None, - pip_args: str | None = None, include_rsa: bool = False, include_netrc: bool = False, - encoding: str | None = None, no_venv: bool = False, dev: bool = False) -> None: +def dockerfile( + path: str, + /, + *, + executable: str | None = None, + image: str = "python", + tag: str | None = None, + suffix: str | None = None, + resource_file: str = "concourse.py", + class_name: str | None = None, + pip_args: str | None = None, + include_rsa: bool = False, + include_netrc: bool = False, + encoding: str | None = None, + no_venv: bool = False, + dev: bool = False, +) -> None: """ Create the Dockerfile. @@ -83,9 +110,21 @@ def dockerfile(path: str, /, *, executable: str | None = None, image: str = "pyt "-c": class_name, "-e": executable, } - assets_options = {key: value for key, value in assets_to_potentially_include.items() if value is not None} + assets_options = { + key: value + for key, value in assets_to_potentially_include.items() + if value is not None + } - cli_split_command = ["python3", "-m", "concoursetools", "assets", ".", "-r", resource_file] + cli_split_command = [ + "python3", + "-m", + "concoursetools", + "assets", + ".", + "-r", + resource_file, + ] for key, value in assets_options.items(): if value is not None: cli_split_command.extend([key, value]) @@ -120,29 +159,33 @@ def dockerfile(path: str, /, *, executable: str | None = None, image: str = "pyt mounts: list[dockertools.Mount] = [] if include_rsa: - mounts.extend([ - dockertools.SecretMount( - secret_id="private_key", - target="/root/.ssh/id_rsa", - mode=0o600, - required=True, - ), - dockertools.SecretMount( - secret_id="known_hosts", - target="/root/.ssh/known_hosts", - mode=0o644, - ), - ]) + mounts.extend( + [ + dockertools.SecretMount( + secret_id="private_key", + target="/root/.ssh/id_rsa", + mode=0o600, + required=True, + ), + dockertools.SecretMount( + secret_id="known_hosts", + target="/root/.ssh/known_hosts", + mode=0o644, + ), + ] + ) if include_netrc: - mounts.extend([ - dockertools.SecretMount( - secret_id="netrc", - target="/root/.netrc", - mode=0o600, - required=True, - ), - ]) + mounts.extend( + [ + dockertools.SecretMount( + secret_id="netrc", + target="/root/.netrc", + mode=0o600, + required=True, + ), + ] + ) if pip_args is None: pip_string_suffix = "" @@ -154,18 +197,24 @@ def dockerfile(path: str, /, *, executable: str | None = None, image: str = "pyt dockertools.CopyInstruction("concoursetools"), ) final_dockerfile.new_instruction_group( - dockertools.MultiLineRunInstruction([ - "python3 -m pip install --upgrade pip" + pip_string_suffix, - "pip install ./concoursetools", - "pip install -r requirements.txt --no-deps" + pip_string_suffix, - ], mounts=mounts), + dockertools.MultiLineRunInstruction( + [ + "python3 -m pip install --upgrade pip" + pip_string_suffix, + "pip install ./concoursetools", + "pip install -r requirements.txt --no-deps" + pip_string_suffix, + ], + mounts=mounts, + ), ) else: final_dockerfile.new_instruction_group( - dockertools.MultiLineRunInstruction([ - "python3 -m pip install --upgrade pip" + pip_string_suffix, - "pip install -r requirements.txt --no-deps" + pip_string_suffix, - ], mounts=mounts), + dockertools.MultiLineRunInstruction( + [ + "python3 -m pip install --upgrade pip" + pip_string_suffix, + "pip install -r requirements.txt --no-deps" + pip_string_suffix, + ], + mounts=mounts, + ), ) final_dockerfile.new_instruction_group( @@ -182,8 +231,16 @@ def dockerfile(path: str, /, *, executable: str | None = None, image: str = "pyt @cli.register(allow_short={"executable", "class_name", "resource_file"}) -def legacy(path: str, /, *, executable: str | None = None, resource_file: str = "concourse.py", - class_name: str | None = None, docker: bool = False, include_rsa: bool = False) -> None: +def legacy( + path: str, + /, + *, + executable: str | None = None, + resource_file: str = "concourse.py", + class_name: str | None = None, + docker: bool = False, + include_rsa: bool = False, +) -> None: """ Invoke the legacy CLI. @@ -194,12 +251,24 @@ def legacy(path: str, /, *, executable: str | None = None, resource_file: str = :param docker: Pass to create a skeleton Dockerfile at the path instead. :param include_rsa: Enable the Dockerfile to (securely) use your RSA private key during building. """ - colour_print(textwrap.dedent(""" + colour_print( + textwrap.dedent(""" The legacy CLI has been deprecated. Please refer to the documentation or help pages for the up to date CLI. This CLI will be removed in version 0.10.0, or in version 1.0.0, whichever is sooner. - """), colour=Colour.RED) + """), + colour=Colour.RED, + ) if docker: - return dockerfile(path, suffix="alpine", executable=executable, resource_file=resource_file, class_name=class_name, - include_rsa=include_rsa, no_venv=True) - assets(path, executable=executable, resource_file=resource_file, class_name=class_name) + return dockerfile( + path, + suffix="alpine", + executable=executable, + resource_file=resource_file, + class_name=class_name, + include_rsa=include_rsa, + no_venv=True, + ) + assets( + path, executable=executable, resource_file=resource_file, class_name=class_name + ) diff --git a/concoursetools/cli/docstring.py b/concoursetools/cli/docstring.py index 986cd07..ae349f1 100644 --- a/concoursetools/cli/docstring.py +++ b/concoursetools/cli/docstring.py @@ -2,6 +2,7 @@ """ Concourse Tools uses a custom CLI tool for easier management of command line functions. """ + from __future__ import annotations from collections.abc import Callable, Generator @@ -27,6 +28,7 @@ class Docstring: :param description: The remaining description before any parameters. :param parameters: A mapping of parameter name to description, with newlines replaced with whitespace. """ + first_line: str description: str parameters: dict[str, str] @@ -51,7 +53,9 @@ def from_string(cls, raw_docstring: str) -> "Docstring": try: first_line, remaining_lines = raw_docstring.split("\n\n", maxsplit=1) except ValueError: - if RE_PARAM.match(raw_docstring.strip()): # all we have are params with no description + if RE_PARAM.match( + raw_docstring.strip() + ): # all we have are params with no description first_line = "" remaining_lines = raw_docstring.strip() else: @@ -59,7 +63,10 @@ def from_string(cls, raw_docstring: str) -> "Docstring": return cls(first_line, "", {}) description, *remaining_params = RE_PARAM.split(remaining_lines.lstrip()) - parameters = {param: " ".join(info.split()).strip() for param, info in _pair_up(remaining_params)} + parameters = { + param: " ".join(info.split()).strip() + for param, info in _pair_up(remaining_params) + } return cls(first_line, description.strip(), parameters) @@ -76,6 +83,6 @@ def _pair_up(data: list[str]) -> Generator[tuple[str, str], None, None]: """ for i in range(0, len(data), 2): try: - yield data[i], data[i+1] + yield data[i], data[i + 1] except IndexError: raise ValueError(f"Needed an even number of values, got {len(data)}") diff --git a/concoursetools/cli/parser.py b/concoursetools/cli/parser.py index 9033b37..5cca0a5 100644 --- a/concoursetools/cli/parser.py +++ b/concoursetools/cli/parser.py @@ -2,6 +2,7 @@ """ Concourse Tools uses a custom CLI tool for easier management of command line functions. """ + from __future__ import annotations from abc import ABC, abstractmethod @@ -21,6 +22,7 @@ if _CURRENT_PYTHON_VERSION >= (3, 10): from types import UnionType + _ANNOTATIONS_TO_TYPES: dict[type[Any] | UnionType | str, type] = {} else: _ANNOTATIONS_TO_TYPES: dict[type[Any] | Any | str, type] = {} # type: ignore[no-redef] @@ -32,17 +34,18 @@ _AVAILABLE_TYPES = (str, bool, int, float) for type_ in _AVAILABLE_TYPES: - _ANNOTATIONS_TO_TYPES.update({ - type_: type_, - type_.__name__: type_, - f"{type_.__name__} | None": type_, - }) + _ANNOTATIONS_TO_TYPES.update( + { + type_: type_, + type_.__name__: type_, + f"{type_.__name__} | None": type_, + } + ) if _CURRENT_PYTHON_VERSION >= (3, 10): _ANNOTATIONS_TO_TYPES[type_ | None] = type_ class _CLIParser(ABC): - @abstractmethod def invoke(self, args: list[str]) -> None: """ @@ -52,21 +55,36 @@ def invoke(self, args: list[str]) -> None: """ ... - def print_help_page(self, usage_string: str, help_sections: dict[str, dict[str, str | None]], spacing: int = 2, - separation: int = 1) -> None: + def print_help_page( + self, + usage_string: str, + help_sections: dict[str, dict[str, str | None]], + spacing: int = 2, + separation: int = 1, + ) -> None: max_key_width = max(self._max_key_length(d) for _, d in help_sections.items()) - self.print_help_section("Usage", {usage_string: None}, key_width=max_key_width, spacing=spacing) + self.print_help_section( + "Usage", {usage_string: None}, key_width=max_key_width, spacing=spacing + ) separator = "\n" * separation print(separator, end="") for title, options in help_sections.items(): - self.print_help_section(title, options, key_width=max_key_width, spacing=spacing) + self.print_help_section( + title, options, key_width=max_key_width, spacing=spacing + ) print(separator, end="") - def print_help_section(self, title: str, options: dict[str, str | None], spacing: int = 2, - key_width: int | None = None, sort_keys: bool = False) -> None: + def print_help_section( + self, + title: str, + options: dict[str, str | None], + spacing: int = 2, + key_width: int | None = None, + sort_keys: bool = False, + ) -> None: """ Print a section in the help page. @@ -86,12 +104,16 @@ def print_help_section(self, title: str, options: dict[str, str | None], spacing else: items = list(options.items()) - total_indent_length = (spacing + key_width + spacing) + total_indent_length = spacing + key_width + spacing max_width, _ = shutil.get_terminal_size() - wrapper = textwrap.TextWrapper(width=max_width, subsequent_indent=" " * total_indent_length) + wrapper = textwrap.TextWrapper( + width=max_width, subsequent_indent=" " * total_indent_length + ) usage_suffix = " \\" - usage_wrapper = textwrap.TextWrapper(width=max_width - len(usage_suffix), subsequent_indent=" " * (spacing + 2)) + usage_wrapper = textwrap.TextWrapper( + width=max_width - len(usage_suffix), subsequent_indent=" " * (spacing + 2) + ) for key, value in items: if value is None: # this is for the usage string @@ -114,6 +136,7 @@ class CLI(_CLIParser): """ Represents a command line interface. """ + def __init__(self) -> None: self.commands: dict[str, CLICommand] = {} @@ -139,7 +162,9 @@ def invoke(self, args: list[str]) -> None: return self.invoke(["legacy"] + args) command.invoke(remaining_args) - def register(self, allow_short: set[str] | None = None) -> Callable[[CLIFunctionT], CLIFunctionT]: + def register( + self, allow_short: set[str] | None = None + ) -> Callable[[CLIFunctionT], CLIFunctionT]: """ Decorate a function. @@ -150,12 +175,16 @@ def register(self, allow_short: set[str] | None = None) -> Callable[[CLIFunction the parameter ``my_parameter`` becomes ``--my-parameter``, but by including ``my_parameter`` in this set, ``-m`` will also be valid on the command line. """ + def decorator(func: CLIFunctionT) -> CLIFunctionT: self.register_function(func, allow_short=allow_short) return func + return decorator - def register_function(self, func: CLIFunction, allow_short: set[str] | None = None) -> None: + def register_function( + self, func: CLIFunction, allow_short: set[str] | None = None + ) -> None: """ Manually register a function. @@ -188,11 +217,15 @@ def print_help(self, spacing: int = 2, separation: int = 1) -> None: major_sections = { "Global Options": global_options, - "Available Commands": {command_name: self.commands[command_name].description - for command_name in sorted(self.commands)} + "Available Commands": { + command_name: self.commands[command_name].description + for command_name in sorted(self.commands) + }, } - self.print_help_page(usage_string, major_sections, spacing=spacing, separation=separation) + self.print_help_page( + usage_string, major_sections, spacing=spacing, separation=separation + ) def print_version(self) -> None: """Print the version of Concourse Tools.""" @@ -210,8 +243,16 @@ class CLICommand(_CLIParser): :param positional_arguments: A list of positional arguments. :param options: A list of options. """ - def __init__(self, name: str, description: str | None, inner_function: CLIFunction, inner_parser: ArgumentParser, - positional_arguments: list[PositionalArgument[Any]], options: list[Option[Any]]) -> None: + + def __init__( + self, + name: str, + description: str | None, + inner_function: CLIFunction, + inner_parser: ArgumentParser, + positional_arguments: list[PositionalArgument[Any]], + options: list[Option[Any]], + ) -> None: self.name = name self.description = description self.inner_function = inner_function @@ -241,7 +282,9 @@ def parse_args(self, args: list[str]) -> tuple[list[Any], dict[str, Any]]: parsed_args_namespace = self.inner_parser.parse_args(args) kwargs = dict(parsed_args_namespace._get_kwargs()) args = [] - for param_name, parameter in inspect.signature(self.inner_function).parameters.items(): + for param_name, parameter in inspect.signature( + self.inner_function + ).parameters.items(): if parameter.kind is inspect._ParameterKind.POSITIONAL_ONLY: value = kwargs.pop(param_name) args.append(value) @@ -259,8 +302,14 @@ def print_help(self, spacing: int = 2, separation: int = 1) -> None: "-h, --help": "Show this help message", } - command_arguments: dict[str, str | None] = {parameter.name: parameter.description or "" for parameter in self.positional_arguments} - command_options: dict[str, str | None] = {", ".join(parameter.aliases): parameter.description for parameter in self.options} + command_arguments: dict[str, str | None] = { + parameter.name: parameter.description or "" + for parameter in self.positional_arguments + } + command_options: dict[str, str | None] = { + ", ".join(parameter.aliases): parameter.description + for parameter in self.options + } major_sections = { "Global Options": global_options, @@ -270,9 +319,12 @@ def print_help(self, spacing: int = 2, separation: int = 1) -> None: self.print_help_page(major_sections, spacing=spacing, separation=separation) - def print_help_page(self, help_sections: dict[str, dict[str, str | None]], spacing: int = 2, - separation: int = 1) -> None: - + def print_help_page( + self, + help_sections: dict[str, dict[str, str | None]], + spacing: int = 2, + separation: int = 1, + ) -> None: self.print_help_section("Usage", {self.usage_string(): None}, spacing=spacing) separator = "\n" * separation @@ -281,7 +333,9 @@ def print_help_page(self, help_sections: dict[str, dict[str, str | None]], spaci max_key_width = max(self._max_key_length(d) for _, d in help_sections.items()) for title, options in help_sections.items(): - self.print_help_section(title, options, key_width=max_key_width, spacing=spacing) + self.print_help_section( + title, options, key_width=max_key_width, spacing=spacing + ) print(separator, end="") def usage_string(self) -> str: @@ -294,7 +348,9 @@ def usage_string(self) -> str: return " ".join(usage_components) @classmethod - def from_function(cls, func: CLIFunction, allow_short: set[str] | None = None) -> CLICommand: + def from_function( + cls, func: CLIFunction, allow_short: set[str] | None = None + ) -> CLICommand: """ Create a new parser from a function. @@ -309,7 +365,10 @@ def from_function(cls, func: CLIFunction, allow_short: set[str] | None = None) - docstring = Docstring.from_object(func) - parser = ArgumentParser(f"python3 -m concoursetools {func.__name__}", description=docstring.first_line) + parser = ArgumentParser( + f"python3 -m concoursetools {func.__name__}", + description=docstring.first_line, + ) positional_arguments: list[PositionalArgument[Any]] = [] options: list[Option[Any]] = [] @@ -322,7 +381,14 @@ def from_function(cls, func: CLIFunction, allow_short: set[str] | None = None) - else: raise TypeError - return cls(func.__name__, docstring.first_line, func, parser, positional_arguments, options) + return cls( + func.__name__, + docstring.first_line, + func, + parser, + positional_arguments, + options, + ) @dataclass @@ -334,6 +400,7 @@ class Parameter(ABC, Generic[T]): :param param_type: The Python type of the parameter. :param description: An optional description of the parameter. """ + name: str param_type: type[T] description: str | None = None @@ -377,7 +444,9 @@ def add_to_parser(self, parser: ArgumentParser) -> None: ... @classmethod - def yield_from_function(cls, func: CLIFunction, allow_short: set[str]) -> Generator[Parameter[Any], None, None]: + def yield_from_function( + cls, func: CLIFunction, allow_short: set[str] + ) -> Generator[Parameter[Any], None, None]: """ Yield parameters from a function. @@ -401,8 +470,13 @@ def yield_from_function(cls, func: CLIFunction, allow_short: set[str]) -> Genera if issubclass(parameter_type, bool): yield FlagOption(parameter_name, parameter_help, parameter.default) else: - yield Option(parameter_name, parameter_type, parameter_help, parameter.default, - allow_short=(parameter_name in allow_short)) + yield Option( + parameter_name, + parameter_type, + parameter_help, + parameter.default, + allow_short=(parameter_name in allow_short), + ) else: raise ValueError("Parameters must be positional or keyword only.") @@ -416,6 +490,7 @@ class PositionalArgument(Parameter[T]): :param param_type: The Python type of the argument. :param description: An optional description of the argument. """ + @property def aliases(self) -> tuple[str, ...]: """The aliases for the option.""" @@ -436,6 +511,7 @@ class Option(Parameter[T]): :param default: The option default, if set. :param allow_short: Set to :data:`True` to allow a short option, i.e. ``-o`` as well as ``--option``. """ + default: T | None = None allow_short: bool = False @@ -446,7 +522,12 @@ def aliases(self) -> tuple[str, ...]: return (self.long_alias,) def add_to_parser(self, parser: ArgumentParser) -> None: - parser.add_argument(*self.aliases, type=self.param_type, default=self.default, help=self.description) + parser.add_argument( + *self.aliases, + type=self.param_type, + default=self.default, + help=self.description, + ) class FlagOption(Option[bool]): @@ -457,6 +538,7 @@ class FlagOption(Option[bool]): :param description: An optional description of the flag. :param default: The option default, if set. Should be either :data:`True` or :data:`False`. """ + def __init__(self, name: str, description: str | None, default: bool): super().__init__(name, bool, description, default, allow_short=False) diff --git a/concoursetools/colour.py b/concoursetools/colour.py index cbc0468..b378fdc 100644 --- a/concoursetools/colour.py +++ b/concoursetools/colour.py @@ -11,6 +11,7 @@ Concourse Tools specifically has no external dependencies, and so these must be actively installed and managed by a user. """ + from __future__ import annotations from collections.abc import Generator @@ -23,7 +24,6 @@ class _NoPrint(str): - def __str__(self) -> str: raise TypeError("Can't print!") @@ -52,8 +52,13 @@ def colourise(string: str, colour: str) -> str: return f"{colour}{string}{END_COLOUR}" -def colour_print(*values: object, colour: str = MISSING_COLOUR, bold: bool = False, underline: bool = False, - **print_kwargs: Any) -> None: +def colour_print( + *values: object, + colour: str = MISSING_COLOUR, + bold: bool = False, + underline: bool = False, + **print_kwargs: Any, +) -> None: """ Print something in colour. @@ -72,12 +77,16 @@ def colour_print(*values: object, colour: str = MISSING_COLOUR, bold: bool = Fal print(*values, **print_kwargs) except TypeError as error: if colour is MISSING_COLOUR: - raise ValueError("You forgot to pass the colour as a keyword argument") from error + raise ValueError( + "You forgot to pass the colour as a keyword argument" + ) from error raise @contextmanager -def print_in_colour(colour: str, bold: bool = False, underline: bool = False) -> Generator[None, None, None]: +def print_in_colour( + colour: str, bold: bool = False, underline: bool = False +) -> Generator[None, None, None]: """ Print anything in colour within a :ref:`context manager `. @@ -108,6 +117,7 @@ class Colour: """ A few common ANSI colours. """ + BLUE = "\033[94m" CYAN = "\033[96m" GREEN = "\033[92m" diff --git a/concoursetools/dockertools.py b/concoursetools/dockertools.py index 0be34b4..8a3a39e 100644 --- a/concoursetools/dockertools.py +++ b/concoursetools/dockertools.py @@ -2,6 +2,7 @@ """ Functions for creating the Dockerfile or asset files. """ + from __future__ import annotations from abc import ABC, abstractmethod @@ -23,9 +24,14 @@ DEFAULT_PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}" -def create_script_file(path: Path, resource_class: type[ConcourseResource[Any]], method_name: MethodName, - executable: str = DEFAULT_EXECUTABLE, permissions: int = 0o755, - encoding: str | None = None) -> None: +def create_script_file( + path: Path, + resource_class: type[ConcourseResource[Any]], + method_name: MethodName, + executable: str = DEFAULT_EXECUTABLE, + permissions: int = 0o755, + encoding: str | None = None, +) -> None: """ Create a script file at a given path. @@ -61,6 +67,7 @@ class Instruction(ABC): """ Represents an instruction in a Dockerfile. """ + def __str__(self) -> str: return self.to_string() @@ -80,6 +87,7 @@ class Comment(Instruction): >>> print(Comment("This is a comment")) # This is a comment """ + def __init__(self, comment: str) -> None: self.comment = comment @@ -101,6 +109,7 @@ class CopyInstruction(Instruction): >>> print(CopyInstruction("folder/file.txt", "folder/new_file.txt")) COPY folder/file.txt folder/new_file.txt """ + def __init__(self, source: str, dest: str | None = None) -> None: if dest is None: *_, dest = source.split(os.sep) @@ -125,11 +134,12 @@ class EntryPointInstruction(Instruction): >>> print(EntryPointInstruction(["python3", "-m", "http.server"])) ENTRYPOINT ["python3", "-m", "http.server"] """ + def __init__(self, commands: list[str]) -> None: self.commands = commands def to_string(self) -> str: - command_string = ", ".join(f"\"{command}\"" for command in self.commands) + command_string = ", ".join(f'"{command}"' for command in self.commands) return f"ENTRYPOINT [{command_string}]" @@ -145,11 +155,14 @@ class EnvInstruction(Instruction): >>> print(EnvInstruction({"MY_NAME": "John Doe", "MY_AGE": "42"})) ENV MY_NAME="John Doe" MY_AGE="42" """ + def __init__(self, variables: dict[str, str]) -> None: self.variables = variables def to_string(self) -> str: - variable_string = " ".join(f"{key}=\"{value}\"" for key, value in self.variables.items()) + variable_string = " ".join( + f'{key}="{value}"' for key, value in self.variables.items() + ) return f"ENV {variable_string}" @@ -172,8 +185,14 @@ class FromInstruction(Instruction): >>> print(FromInstruction("python", tag="3.11-slim", platform="linux/386")) FROM --platform=linux/386 python:3.11-slim """ - def __init__(self, image: str, tag: str | None = None, digest: str | None = None, - platform: str | None = None) -> None: + + def __init__( + self, + image: str, + tag: str | None = None, + digest: str | None = None, + platform: str | None = None, + ) -> None: if tag and digest: raise ValueError("Cannot pass BOTH tag and digest.") self.image = image @@ -210,6 +229,7 @@ class RunInstruction(Instruction): >>> print(RunInstruction(["pip install --upgrade pip", "pip install -r requirements.txt"])) RUN pip install --upgrade pip && pip install -r requirements.txt """ + def __init__(self, commands: list[str]) -> None: self.commands = commands @@ -224,6 +244,7 @@ class WorkDirInstruction(Instruction): :param work_dir: The directory to set as the working directory on the image. """ + def __init__(self, work_dir: str) -> None: self.work_dir = work_dir @@ -235,12 +256,15 @@ class Mount(ABC): """ Represents a mount for the run command. """ + def __str__(self) -> str: return self.to_string() def to_string(self) -> str: """Return a string representation of the mount.""" - info_string = ",".join(f"{key}={value}" for key, value in self.to_dict().items()) + info_string = ",".join( + f"{key}={value}" for key, value in self.to_dict().items() + ) return f"--mount={info_string}" @abstractmethod @@ -264,8 +288,16 @@ class SecretMount(Mount): >>> print(SecretMount(secret_id="aws", target="/root/.aws/credentials")) --mount=type=secret,id=aws,target=/root/.aws/credentials """ - def __init__(self, secret_id: str | None = None, target: str | None = None, required: bool | None = None, - mode: int | None = None, user_id: int | None = None, group_id: int | None = None) -> None: + + def __init__( + self, + secret_id: str | None = None, + target: str | None = None, + required: bool | None = None, + mode: int | None = None, + user_id: int | None = None, + group_id: int | None = None, + ) -> None: if not (secret_id or target): raise ValueError("Either a secret ID or target must be passed.") @@ -323,6 +355,7 @@ class MultiLineRunInstruction(Instruction): pip install --upgrade pip && \ pip install -r requirements.txt """ + def __init__(self, commands: list[str], mounts: list[Mount] | None = None) -> None: self.commands = commands self.mounts = mounts or [] @@ -344,7 +377,10 @@ class Dockerfile: :param instruction_groups: A list of lists of instructions or comments. Separation between groups is larger than between instructions within the group to imply sections to the Dockerfile. """ - def __init__(self, instruction_groups: list[list[Instruction | Comment]] | None = None) -> None: + + def __init__( + self, instruction_groups: list[list[Instruction | Comment]] | None = None + ) -> None: self.instruction_groups = instruction_groups or [] def new_instruction_group(self, *instructions: Instruction | Comment) -> None: diff --git a/concoursetools/importing.py b/concoursetools/importing.py index 2987e0a..36af6c1 100644 --- a/concoursetools/importing.py +++ b/concoursetools/importing.py @@ -2,6 +2,7 @@ """ Functions for dynamically importing from Python modules. """ + from __future__ import annotations from collections.abc import Generator, Sequence @@ -16,7 +17,9 @@ T = TypeVar("T") -def import_single_class_from_module(file_path: Path, parent_class: type[T], class_name: str | None = None) -> type[T]: +def import_single_class_from_module( + file_path: Path, parent_class: type[T], class_name: str | None = None +) -> type[T]: """ Import the resource class from the module. @@ -29,23 +32,31 @@ def import_single_class_from_module(file_path: Path, parent_class: type[T], clas :returns: The extracted class. :raises RuntimeError: If too many or too few classes are available in the module, unless the class name is specified. """ - possible_resource_classes = import_classes_from_module(file_path, parent_class=parent_class) + possible_resource_classes = import_classes_from_module( + file_path, parent_class=parent_class + ) if class_name is None: if len(possible_resource_classes) == 1: _, resource_class = possible_resource_classes.popitem() else: if len(possible_resource_classes) == 0: - raise RuntimeError(f"No subclasses of {parent_class.__name__!r} found in {file_path}") - raise RuntimeError(f"Multiple subclasses of {parent_class.__name__!r} found in {file_path}:" - f" {set(possible_resource_classes)}") + raise RuntimeError( + f"No subclasses of {parent_class.__name__!r} found in {file_path}" + ) + raise RuntimeError( + f"Multiple subclasses of {parent_class.__name__!r} found in {file_path}:" + f" {set(possible_resource_classes)}" + ) else: resource_class = possible_resource_classes[class_name] return resource_class -def import_classes_from_module(file_path: Path, parent_class: type[T]) -> dict[str, type[T]]: +def import_classes_from_module( + file_path: Path, parent_class: type[T] +) -> dict[str, type[T]]: """ Import all available resource classes from the module. @@ -64,10 +75,14 @@ def import_classes_from_module(file_path: Path, parent_class: type[T]) -> dict[s except TypeError: class_is_subclass_of_parent = False - class_is_defined_in_this_module = (cls.__module__ == import_path) - class_is_not_private = (not cls.__name__.startswith("_")) + class_is_defined_in_this_module = cls.__module__ == import_path + class_is_not_private = not cls.__name__.startswith("_") - if class_is_subclass_of_parent and class_is_defined_in_this_module and class_is_not_private: + if ( + class_is_subclass_of_parent + and class_is_defined_in_this_module + and class_is_not_private + ): possible_resource_classes[cls.__name__] = cls return possible_resource_classes @@ -127,7 +142,9 @@ def import_py_file(import_path: str, file_path: Path) -> ModuleType: @contextmanager -def edit_sys_path(prepend: Sequence[Path] = (), append: Sequence[Path] = ()) -> Generator[None, None, None]: +def edit_sys_path( + prepend: Sequence[Path] = (), append: Sequence[Path] = () +) -> Generator[None, None, None]: """ Temporarily add to :data:`sys.path` within a context manager. @@ -137,7 +154,9 @@ def edit_sys_path(prepend: Sequence[Path] = (), append: Sequence[Path] = ()) -> """ original_sys_path = sys.path.copy() # otherwise we just reference the original try: - sys.path = [str(path) for path in prepend] + sys.path + [str(path) for path in append] + sys.path = ( + [str(path) for path in prepend] + sys.path + [str(path) for path in append] + ) yield finally: sys.path = original_sys_path diff --git a/concoursetools/metadata.py b/concoursetools/metadata.py index d05ce26..66952c8 100644 --- a/concoursetools/metadata.py +++ b/concoursetools/metadata.py @@ -12,6 +12,7 @@ See the Concourse :concourse:`implementing-resource-types.resource-metadata` documentation for more information. """ + from __future__ import annotations import json @@ -44,9 +45,17 @@ class BuildMetadata: # pylint: disable=invalid-name These can still be accessed via :data:`os.environ`, but they are not supported by Concourse Tools. """ - def __init__(self, BUILD_ID: str, BUILD_TEAM_NAME: str, ATC_EXTERNAL_URL: str, BUILD_NAME: str | None = None, - BUILD_JOB_NAME: str | None = None, BUILD_PIPELINE_NAME: str | None = None, - BUILD_PIPELINE_INSTANCE_VARS: str | None = None): + + def __init__( + self, + BUILD_ID: str, + BUILD_TEAM_NAME: str, + ATC_EXTERNAL_URL: str, + BUILD_NAME: str | None = None, + BUILD_JOB_NAME: str | None = None, + BUILD_PIPELINE_NAME: str | None = None, + BUILD_PIPELINE_INSTANCE_VARS: str | None = None, + ): self.BUILD_ID = BUILD_ID self.BUILD_TEAM_NAME = BUILD_TEAM_NAME @@ -71,9 +80,11 @@ def BUILD_CREATED_BY(self) -> str: try: return os.environ["BUILD_CREATED_BY"] except KeyError as error: - raise PermissionError("The 'BUILD_CREATED_BY' variable has not been made available. This must be enabled " - "with the 'expose_build_created_by' variable within the resource schema: " - "https://concourse-ci.org/resources.html#schema.resource.expose_build_created_by") from error + raise PermissionError( + "The 'BUILD_CREATED_BY' variable has not been made available. This must be enabled " + "with the 'expose_build_created_by' variable within the resource schema: " + "https://concourse-ci.org/resources.html#schema.resource.expose_build_created_by" + ) from error @property def is_one_off_build(self) -> bool: @@ -92,7 +103,14 @@ def is_one_off_build(self) -> bool: The documentation insists that ``$BUILD_NAME`` will also not be set in the environment during a one-off build, but experimentation has shown this to be **false**. """ - return all(attr is None for attr in (self.BUILD_JOB_NAME, self.BUILD_PIPELINE_NAME, self.BUILD_PIPELINE_INSTANCE_VARS)) + return all( + attr is None + for attr in ( + self.BUILD_JOB_NAME, + self.BUILD_PIPELINE_NAME, + self.BUILD_PIPELINE_INSTANCE_VARS, + ) + ) @property def is_instanced_pipeline(self) -> bool: @@ -139,7 +157,9 @@ def instance_vars(self) -> dict[str, object]: } } """ - instance_vars: dict[str, object] = json.loads(self.BUILD_PIPELINE_INSTANCE_VARS or "{}") + instance_vars: dict[str, object] = json.loads( + self.BUILD_PIPELINE_INSTANCE_VARS or "{}" + ) return instance_vars def build_url(self) -> str: @@ -157,14 +177,21 @@ def build_url(self) -> str: if self.is_instanced_pipeline: flattened_instance_vars = _flatten_dict(self.instance_vars()) - query_string = "?" + "&".join(f"vars.{key}={quote(json.dumps(value))}" for key, value in flattened_instance_vars.items()) + query_string = "?" + "&".join( + f"vars.{key}={quote(json.dumps(value))}" + for key, value in flattened_instance_vars.items() + ) else: query_string = "" return f"{self.ATC_EXTERNAL_URL}/{quote(build_path)}{query_string}" - def format_string(self, string: str, additional_values: dict[str, str] | None = None, - ignore_missing: bool = False) -> str: + def format_string( + self, + string: str, + additional_values: dict[str, str] | None = None, + ignore_missing: bool = False, + ) -> str: """ Format a string with metadata using standard bash ``$`` notation. @@ -214,7 +241,11 @@ def format_string(self, string: str, additional_values: dict[str, str] | None = except PermissionError: pass - return template.safe_substitute(possible_values) if ignore_missing else template.substitute(possible_values) + return ( + template.safe_substitute(possible_values) + if ignore_missing + else template.substitute(possible_values) + ) @classmethod def from_env(cls) -> "BuildMetadata": diff --git a/concoursetools/mocking.py b/concoursetools/mocking.py index 323e4df..dba3242 100644 --- a/concoursetools/mocking.py +++ b/concoursetools/mocking.py @@ -2,6 +2,7 @@ """ Concourse Tools contains a number of utility functions for mocking various parts of the process for testing purposes. """ + from __future__ import annotations from collections.abc import Generator @@ -23,7 +24,11 @@ FolderDict = dict[str, Any] -def create_env_vars(one_off_build: bool = False, instance_vars: dict[str, str] | None = None, **env_vars: str) -> dict[str, str]: +def create_env_vars( + one_off_build: bool = False, + instance_vars: dict[str, str] | None = None, + **env_vars: str, +) -> dict[str, str]: """ Create fake environment variables for a Concourse stage. @@ -56,10 +61,12 @@ def create_env_vars(one_off_build: bool = False, instance_vars: dict[str, str] | if one_off_build: return env - env.update({ - "BUILD_JOB_NAME": "my-job", - "BUILD_PIPELINE_NAME": "my-pipeline", - }) + env.update( + { + "BUILD_JOB_NAME": "my-job", + "BUILD_PIPELINE_NAME": "my-pipeline", + } + ) if instance_vars is not None: env["BUILD_PIPELINE_INSTANCE_VARS"] = json.dumps(instance_vars) @@ -83,7 +90,13 @@ class TestBuildMetadata(BuildMetadata): :param instance_vars: Pass optional instance vars to emulate an instanced pipeline. :param env_vars: Pass additional environment variables, or overload the default ones. """ - def __init__(self, one_off_build: bool = False, instance_vars: dict[str, str] | None = None, **env_vars: str): + + def __init__( + self, + one_off_build: bool = False, + instance_vars: dict[str, str] | None = None, + **env_vars: str, + ): test_env_vars = create_env_vars(one_off_build, instance_vars, **env_vars) super().__init__(**test_env_vars) @@ -124,7 +137,14 @@ class TemporaryDirectoryState: ... FileNotFoundError: [Errno 2] No such file or directory: '.../folder_2/file_2' """ - def __init__(self, starting_state: FolderDict | None = None, max_depth: int = 2, encoding: str | None = None, **kwargs: Any): + + def __init__( + self, + starting_state: FolderDict | None = None, + max_depth: int = 2, + encoding: str | None = None, + **kwargs: Any, + ): self.starting_state = starting_state or {} self.max_depth = max_depth self.encoding = encoding @@ -153,22 +173,38 @@ def final_state(self) -> FolderDict: :raises RuntimeError: If the temporary directory is currently open. """ if self._final_state is None: - raise RuntimeError("Final state is not set whilst temporary directory is still open.") + raise RuntimeError( + "Final state is not set whilst temporary directory is still open." + ) return self._final_state def __enter__(self) -> "TemporaryDirectoryState": self._temp_dir = TemporaryDirectory(**self.temporary_directory_kwargs) - self._set_folder_from_dict(self.path, self.starting_state, encoding=self.encoding) + self._set_folder_from_dict( + self.path, self.starting_state, encoding=self.encoding + ) return self - def __exit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None) -> None: + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: if self._temp_dir is None: raise RuntimeError("Temporary directory missing from instance.") - self._final_state = self._get_folder_as_dict(self.path, self.max_depth, self.encoding) + self._final_state = self._get_folder_as_dict( + self.path, self.max_depth, self.encoding + ) self._temp_dir.__exit__(exc_type, exc_val, exc_tb) - def _get_folder_as_dict(self, folder_path: Path, max_depth: int = 2, encoding: str | None = None, - byte_limit: int = 16) -> FolderDict | Any: + def _get_folder_as_dict( + self, + folder_path: Path, + max_depth: int = 2, + encoding: str | None = None, + byte_limit: int = 16, + ) -> FolderDict | Any: """ Return the recursive contents of a folder as a nested dictionary. @@ -202,10 +238,14 @@ def _get_folder_as_dict(self, folder_path: Path, max_depth: int = 2, encoding: s first_chunk = rf.read(byte_limit) folder_dict[item.name] = first_chunk elif item.is_dir(): - folder_dict[item.name] = self._get_folder_as_dict(item, max_depth=max_depth-1, encoding=encoding) + folder_dict[item.name] = self._get_folder_as_dict( + item, max_depth=max_depth - 1, encoding=encoding + ) return folder_dict - def _set_folder_from_dict(self, folder_path: Path, folder_dict: FolderDict, encoding: str | None = None) -> None: + def _set_folder_from_dict( + self, folder_path: Path, folder_dict: FolderDict, encoding: str | None = None + ) -> None: """ Set the contents of a folder using a recursive dictionary. @@ -255,6 +295,7 @@ class StringIOWrapper: >>> output == "abc\ndef\n" True """ + def __init__(self) -> None: self.inner_io = StringIO() diff --git a/concoursetools/parsing.py b/concoursetools/parsing.py index b687525..dd3dc7a 100644 --- a/concoursetools/parsing.py +++ b/concoursetools/parsing.py @@ -3,12 +3,19 @@ Concourse Tools contains a number of simple functions for mapping between Python and the Concourse resource type paradigm. """ + from __future__ import annotations import json from typing import Any, cast -from concoursetools.typing import Metadata, MetadataPair, Params, ResourceConfig, VersionConfig +from concoursetools.typing import ( + Metadata, + MetadataPair, + Params, + ResourceConfig, + VersionConfig, +) def parse_check_payload(raw_json: str) -> tuple[ResourceConfig, VersionConfig | None]: @@ -114,7 +121,9 @@ def parse_metadata(metadata_pairs: list[MetadataPair]) -> Metadata: return {pair["name"]: pair["value"] for pair in metadata_pairs} -def format_check_output(version_configs: list[VersionConfig], **json_kwargs: Any) -> str: +def format_check_output( + version_configs: list[VersionConfig], **json_kwargs: Any +) -> str: """ Format :concourse:`check output ` as a JSON string. @@ -130,11 +139,16 @@ def format_check_output(version_configs: list[VersionConfig], **json_kwargs: Any { "ref": "7154fe" } ] """ - safe_version_configs = [{str(key): str(value) for key, value in version_config.items()} for version_config in version_configs] + safe_version_configs = [ + {str(key): str(value) for key, value in version_config.items()} + for version_config in version_configs + ] return json.dumps(safe_version_configs, **json_kwargs) -def format_in_out_output(version_config: VersionConfig, metadata: Metadata, **json_kwargs: Any) -> str: +def format_in_out_output( + version_config: VersionConfig, metadata: Metadata, **json_kwargs: Any +) -> str: """ Format :concourse:`in output ` or :concourse:`out output ` as a JSON string. @@ -154,7 +168,9 @@ def format_in_out_output(version_config: VersionConfig, metadata: Metadata, **js ] } """ - safe_version_config = {str(key): str(value) for key, value in version_config.items()} + safe_version_config = { + str(key): str(value) for key, value in version_config.items() + } safe_metadata = format_metadata(metadata) output = { "version": safe_version_config, @@ -170,10 +186,16 @@ def format_metadata(metadata: Metadata) -> list[MetadataPair]: :param metadata: A key-value mapping representing metadata. Keys and values should both be strings. :returns: A list of key-value pairs for processing in Concourse. """ - return [{"name": str(name), "value": str(value)} for name, value in metadata.items()] + return [ + {"name": str(name), "value": str(value)} for name, value in metadata.items() + ] -def format_check_input(resource_config: ResourceConfig, version_config: VersionConfig | None = None, **json_kwargs: Any) -> str: +def format_check_input( + resource_config: ResourceConfig, + version_config: VersionConfig | None = None, + **json_kwargs: Any, +) -> str: """ Format :concourse:`check input ` as a JSON string. @@ -199,7 +221,12 @@ def format_check_input(resource_config: ResourceConfig, version_config: VersionC return json.dumps(payload, **json_kwargs) -def format_in_input(resource_config: ResourceConfig, version_config: VersionConfig, params: Params | None = None, **json_kwargs: Any) -> str: +def format_in_input( + resource_config: ResourceConfig, + version_config: VersionConfig, + params: Params | None = None, + **json_kwargs: Any, +) -> str: """ Format :concourse:`in input ` as a JSON string. @@ -230,7 +257,9 @@ def format_in_input(resource_config: ResourceConfig, version_config: VersionConf return json.dumps(payload, **json_kwargs) -def format_out_input(resource_config: ResourceConfig, params: Params | None = None, **json_kwargs: Any) -> str: +def format_out_input( + resource_config: ResourceConfig, params: Params | None = None, **json_kwargs: Any +) -> str: """ Format :concourse:`out input ` as a JSON string. @@ -258,7 +287,9 @@ def format_out_input(resource_config: ResourceConfig, params: Params | None = No return json.dumps(payload, **json_kwargs) -def _extract_source_config_from_payload(payload: dict[str, dict[str, Any] | None]) -> dict[str, Any]: +def _extract_source_config_from_payload( + payload: dict[str, dict[str, Any] | None], +) -> dict[str, Any]: try: unsafe_source_config = payload["source"] except KeyError as error: @@ -275,14 +306,20 @@ def _extract_source_config_from_payload(payload: dict[str, dict[str, Any] | None return source_config -def _extract_version_config_from_payload(payload: dict[str, dict[str, Any] | None]) -> dict[str, Any]: +def _extract_version_config_from_payload( + payload: dict[str, dict[str, Any] | None], +) -> dict[str, Any]: unsafe_version_config = payload["version"] unsafe_version_config = cast(dict[str, Any], unsafe_version_config) - version_config = {str(key): str(value) for key, value in unsafe_version_config.items()} + version_config = { + str(key): str(value) for key, value in unsafe_version_config.items() + } return version_config -def _extract_param_config_from_payload(payload: dict[str, dict[str, Any] | None]) -> dict[str, Any]: +def _extract_param_config_from_payload( + payload: dict[str, dict[str, Any] | None], +) -> dict[str, Any]: unsafe_params_config = payload.get("params", {}) unsafe_params_config = cast(dict[str, Any], unsafe_params_config) params_config = {str(key): value for key, value in unsafe_params_config.items()} diff --git a/concoursetools/resource.py b/concoursetools/resource.py index c721668..08bd9a5 100644 --- a/concoursetools/resource.py +++ b/concoursetools/resource.py @@ -14,6 +14,7 @@ To learn more about how Concourse resource types are actually implemented under the hood, check out :concourse:`implementing-resource-types` in Concourse. """ + from __future__ import annotations from abc import ABC, abstractmethod @@ -82,6 +83,7 @@ def __init__(self, project_key, repo, file_path, host="https://github.com/"): The parameters need not be set as attributes if they can all be combined into a single class, such as an API wrapper or other construct. """ + def __init__(self, version_class: type[VersionT]): self.version_class = version_class @@ -99,7 +101,9 @@ def certs_dir(self) -> Path: return Path("/etc/ssl/certs") @abstractmethod - def fetch_new_versions(self, previous_version: VersionT | None = None) -> list[VersionT]: + def fetch_new_versions( + self, previous_version: VersionT | None = None + ) -> list[VersionT]: """ Fetch new versions of the resource. @@ -125,7 +129,9 @@ def fetch_new_versions(self, previous_version: VersionT | None = None) -> list[V """ @abstractmethod - def download_version(self, version: VersionT, destination_dir: Path, build_metadata: BuildMetadata) -> tuple[VersionT, Metadata]: + def download_version( + self, version: VersionT, destination_dir: Path, build_metadata: BuildMetadata + ) -> tuple[VersionT, Metadata]: """ Download a version and place its files within the resource directory in your pipeline. @@ -177,7 +183,9 @@ def download_version(self, version, destination_dir, build_metadata, """ @abstractmethod - def publish_new_version(self, sources_dir: Path, build_metadata: BuildMetadata) -> tuple[VersionT, Metadata]: + def publish_new_version( + self, sources_dir: Path, build_metadata: BuildMetadata + ) -> tuple[VersionT, Metadata]: """ Update a resource by publishing a new version. @@ -237,7 +245,9 @@ def check_main(cls) -> None: resource, previous_version = cls._parse_check_input() with contextlib.redirect_stdout(sys.stderr): new_versions = resource.fetch_new_versions(previous_version) - output = parsing.format_check_output([version.to_flat_dict() for version in new_versions]) + output = parsing.format_check_output( + [version.to_flat_dict() for version in new_versions] + ) _output(output) @classmethod @@ -253,7 +263,9 @@ def in_main(cls) -> None: resource, version, destination_dir, params = cls._parse_in_input() build_metadata = BuildMetadata.from_env() with contextlib.redirect_stdout(sys.stderr): - version, metadata = resource.download_version(version, destination_dir, build_metadata, **params) + version, metadata = resource.download_version( + version, destination_dir, build_metadata, **params + ) output = parsing.format_in_out_output(version.to_flat_dict(), metadata) _output(output) @@ -270,36 +282,48 @@ def out_main(cls) -> None: resource, sources_dir, params = cls._parse_out_input() build_metadata = BuildMetadata.from_env() with contextlib.redirect_stdout(sys.stderr): - version, metadata = resource.publish_new_version(sources_dir, build_metadata, **params) + version, metadata = resource.publish_new_version( + sources_dir, build_metadata, **params + ) output = parsing.format_in_out_output(version.to_flat_dict(), metadata) _output(output) @classmethod - def _parse_check_input(cls) -> tuple["ConcourseResource[VersionT]", VersionT | None]: + def _parse_check_input( + cls, + ) -> tuple["ConcourseResource[VersionT]", VersionT | None]: """Parse input from the command line.""" check_payload = sys.stdin.read() - resource_config, previous_version_config = parsing.parse_check_payload(check_payload) + resource_config, previous_version_config = parsing.parse_check_payload( + check_payload + ) resource = cls._from_resource_config(resource_config) if previous_version_config is None: previous_version = None else: - previous_version = resource.version_class.from_flat_dict(previous_version_config) + previous_version = resource.version_class.from_flat_dict( + previous_version_config + ) return resource, previous_version @classmethod - def _parse_in_input(cls) -> tuple["ConcourseResource[VersionT]", VersionT, Path, Params]: + def _parse_in_input( + cls, + ) -> tuple["ConcourseResource[VersionT]", VersionT, Path, Params]: """Parse input from the command line.""" in_payload = sys.stdin.read() try: destination_dir = Path(sys.argv[1]) except IndexError as error: - raise ValueError("Path to the destination directory for the resource " - "must be passed to the command line") from error + raise ValueError( + "Path to the destination directory for the resource " + "must be passed to the command line" + ) from error resource_config, version_config, params = parsing.parse_in_payload(in_payload) @@ -316,8 +340,10 @@ def _parse_out_input(cls) -> tuple["ConcourseResource[VersionT]", Path, Params]: try: sources_dir = Path(sys.argv[1]) except IndexError as error: - raise ValueError("Path to the directory containing the build's full set of sources " - "must be passed to the command line") from error + raise ValueError( + "Path to the directory containing the build's full set of sources " + "must be passed to the command line" + ) from error resource_config, params = parsing.parse_out_payload(out_payload) @@ -326,7 +352,9 @@ def _parse_out_input(cls) -> tuple["ConcourseResource[VersionT]", Path, Params]: return resource, sources_dir, params @classmethod - def _from_resource_config(cls, resource_config: ResourceConfig) -> "ConcourseResource[VersionT]": + def _from_resource_config( + cls, resource_config: ResourceConfig + ) -> "ConcourseResource[VersionT]": return cls(**resource_config) diff --git a/concoursetools/testing.py b/concoursetools/testing.py index 2e4d3e4..27f9e10 100644 --- a/concoursetools/testing.py +++ b/concoursetools/testing.py @@ -8,6 +8,7 @@ correct outputs. However, this does limit your ability to mock the script environment when your code relies on external factors. """ + from __future__ import annotations from abc import ABC, abstractmethod @@ -23,9 +24,28 @@ from concoursetools import BuildMetadata, ConcourseResource, Version from concoursetools.dockertools import MethodName, ScriptName, create_script_file -from concoursetools.mocking import StringIOWrapper, TemporaryDirectoryState, create_env_vars, mock_argv, mock_environ, mock_stdin -from concoursetools.parsing import format_check_input, format_in_input, format_out_input, parse_metadata -from concoursetools.typing import Metadata, MetadataPair, Params, ResourceConfig, VersionConfig, VersionT +from concoursetools.mocking import ( + StringIOWrapper, + TemporaryDirectoryState, + create_env_vars, + mock_argv, + mock_environ, + mock_stdin, +) +from concoursetools.parsing import ( + format_check_input, + format_in_input, + format_out_input, + parse_metadata, +) +from concoursetools.typing import ( + Metadata, + MetadataPair, + Params, + ResourceConfig, + VersionConfig, + VersionT, +) T = TypeVar("T") ContextManager = Generator[T, None, None] @@ -67,8 +87,14 @@ class TestResourceWrapper(ABC, Generic[VersionT]): ... assert metadata == {"team_name": "my-team"} ... assert debugging == "Downloading.\n" """ - def __init__(self, directory_dict: FolderDict | None = None, one_off_build: bool = False, - instance_vars: dict[str, str] | None = None, **env_vars: str) -> None: + + def __init__( + self, + directory_dict: FolderDict | None = None, + one_off_build: bool = False, + instance_vars: dict[str, str] | None = None, + **env_vars: str, + ) -> None: self.mocked_environ = create_env_vars(one_off_build, instance_vars, **env_vars) self.directory_dict = directory_dict @@ -105,7 +131,9 @@ def capture_debugging(self) -> ContextManager[StringIOWrapper]: yield self._debugging_output @contextmanager - def capture_directory_state(self, starting_state: FolderDict | None = None) -> ContextManager["TemporaryDirectoryState"]: + def capture_directory_state( + self, starting_state: FolderDict | None = None + ) -> ContextManager["TemporaryDirectoryState"]: """ Open a context manager to expose the internal state of the resource. @@ -130,8 +158,12 @@ def capture_directory_state(self, starting_state: FolderDict | None = None) -> C def _forbid_methods(self, *methods: Callable[..., object]) -> ContextManager[None]: try: for method in methods: + def new_method(*args: object, **kwargs: object) -> object: - raise RuntimeError(f"Cannot call {method.__name__} from within this context manager.") + raise RuntimeError( + f"Cannot call {method.__name__} from within this context manager." + ) + setattr(self, method.__name__, new_method) yield finally: @@ -152,16 +184,28 @@ class SimpleTestResourceWrapper(TestResourceWrapper[VersionT]): :param inner_resource: The resource to be wrapped. :param directory_dict: The initial state of the resource directory. See :class:`~concoursetools.mocking.TemporaryDirectoryState` :param one_off_build: Set to :data:`True` if you are testing a one-off build. + :param capture_output: Set to :data:`False` if you do not want to capture the output, i.e. for debugging. Default is :data:`True`. :param instance_vars: Pass optional instance vars to emulate an instanced pipeline. :param env_vars: Pass additional environment variables, or overload the default ones. """ - def __init__(self, inner_resource: ConcourseResource[VersionT], directory_dict: FolderDict | None = None, - one_off_build: bool = False, instance_vars: dict[str, str] | None = None, **env_vars: str) -> None: + + def __init__( + self, + inner_resource: ConcourseResource[VersionT], + directory_dict: FolderDict | None = None, + one_off_build: bool = False, + capture_output: bool = True, + instance_vars: dict[str, str] | None = None, + **env_vars: str, + ) -> None: super().__init__(directory_dict, one_off_build, instance_vars, **env_vars) + self.capture_output = capture_output self.inner_resource = inner_resource self.mocked_build_metadata = BuildMetadata(**self.mocked_environ) - def fetch_new_versions(self, previous_version: VersionT | None = None) -> list[VersionT]: + def fetch_new_versions( + self, previous_version: VersionT | None = None + ) -> list[VersionT]: """ Fetch new versions of the resource. @@ -171,10 +215,17 @@ def fetch_new_versions(self, previous_version: VersionT | None = None) -> list[V if the resource has never been run before. :returns: A list of new versions. """ - with self._debugging_output.capture_stdout_and_stderr(): - return self.inner_resource.fetch_new_versions(previous_version=previous_version) + if self.capture_output: + with self._debugging_output.capture_stdout_and_stderr(): + return self.inner_resource.fetch_new_versions( + previous_version=previous_version + ) + + return self.inner_resource.fetch_new_versions(previous_version=previous_version) - def download_version(self, version: VersionT, **params: object) -> tuple[VersionT, Metadata]: + def download_version( + self, version: VersionT, **params: object + ) -> tuple[VersionT, Metadata]: """ Download a version and place its files within the resource directory in your pipeline. @@ -185,9 +236,22 @@ def download_version(self, version: VersionT, **params: object) -> tuple[Version :param params: Additional keyword parameters passed to the inner resource. :returns: The version (most likely unchanged), and a dictionary of metadata. """ - with self._debugging_output.capture_stdout_and_stderr(): - with self._directory_state: - return self.inner_resource.download_version(version, self._directory_state.path, self.mocked_build_metadata, **params) + if self.capture_output: + with self._debugging_output.capture_stdout_and_stderr(): + with self._directory_state: + return self.inner_resource.download_version( + version, + self._directory_state.path, + self.mocked_build_metadata, + **params, + ) + with self._directory_state: + return self.inner_resource.download_version( + version, + self._directory_state.path, + self.mocked_build_metadata, + **params, + ) def publish_new_version(self, **params: object) -> tuple[VersionT, Metadata]: """ @@ -199,9 +263,17 @@ def publish_new_version(self, **params: object) -> tuple[VersionT, Metadata]: :param params: Additional keyword parameters passed to the inner resource. :returns: The new version, and a dictionary of metadata. """ - with self._debugging_output.capture_stdout_and_stderr(): - with self._directory_state: - return self.inner_resource.publish_new_version(self._directory_state.path, self.mocked_build_metadata, **params) + if self.capture_output: + with self._debugging_output.capture_stdout_and_stderr(): + with self._directory_state: + return self.inner_resource.publish_new_version( + self._directory_state.path, self.mocked_build_metadata, **params + ) + + with self._directory_state: + return self.inner_resource.publish_new_version( + self._directory_state.path, self.mocked_build_metadata, **params + ) class JSONTestResourceWrapper(TestResourceWrapper[VersionT]): @@ -218,14 +290,23 @@ class JSONTestResourceWrapper(TestResourceWrapper[VersionT]): :param instance_vars: Pass optional instance vars to emulate an instanced pipeline. :param env_vars: Pass additional environment variables, or overload the default ones. """ - def __init__(self, inner_resource_type: type[ConcourseResource[VersionT]], inner_resource_config: ResourceConfig, - directory_dict: FolderDict | None = None, one_off_build: bool = False, - instance_vars: dict[str, str] | None = None, **env_vars: str) -> None: + + def __init__( + self, + inner_resource_type: type[ConcourseResource[VersionT]], + inner_resource_config: ResourceConfig, + directory_dict: FolderDict | None = None, + one_off_build: bool = False, + instance_vars: dict[str, str] | None = None, + **env_vars: str, + ) -> None: super().__init__(directory_dict, one_off_build, instance_vars, **env_vars) self.inner_resource_type = inner_resource_type self.inner_resource_config = inner_resource_config - def fetch_new_versions(self, previous_version_config: VersionConfig | None = None) -> list[VersionConfig]: + def fetch_new_versions( + self, previous_version_config: VersionConfig | None = None + ) -> list[VersionConfig]: """ Fetch new versions of the resource. @@ -254,7 +335,9 @@ def fetch_new_versions(self, previous_version_config: VersionConfig | None = Non raise ValueError(f"Unexpected output: {stdout.strip()}") from error return version_configs - def download_version(self, version_config: VersionConfig, params: Params | None = None) -> tuple[VersionConfig, list[MetadataPair]]: + def download_version( + self, version_config: VersionConfig, params: Params | None = None + ) -> tuple[VersionConfig, list[MetadataPair]]: """ Download a version and place its files within the resource directory in your pipeline. @@ -270,7 +353,9 @@ def download_version(self, version_config: VersionConfig, params: Params | None with self._debugging_output.capture_stderr(): with self._directory_state: with mock_stdin(stdin): - with mock_argv("/opt/resource/in", str(self._directory_state.path)): + with mock_argv( + "/opt/resource/in", str(self._directory_state.path) + ): with mock_environ(self.mocked_environ): self.inner_resource_type.in_main() stdout = stdout_buffer.getvalue() @@ -283,7 +368,9 @@ def download_version(self, version_config: VersionConfig, params: Params | None metadata_pairs: list[MetadataPair] = output["metadata"] or [] return new_version_config, metadata_pairs - def publish_new_version(self, params: Params | None = None) -> tuple[VersionConfig, list[MetadataPair]]: + def publish_new_version( + self, params: Params | None = None + ) -> tuple[VersionConfig, list[MetadataPair]]: """ Update a resource by publishing a new version. @@ -298,7 +385,9 @@ def publish_new_version(self, params: Params | None = None) -> tuple[VersionConf with self._debugging_output.capture_stderr(): with self._directory_state: with mock_stdin(stdin): - with mock_argv("/opt/resource/out", str(self._directory_state.path)): + with mock_argv( + "/opt/resource/out", str(self._directory_state.path) + ): with mock_environ(self.mocked_environ): self.inner_resource_type.out_main() stdout = stdout_buffer.getvalue() @@ -329,15 +418,31 @@ class ConversionTestResourceWrapper(JSONTestResourceWrapper[VersionT]): :param instance_vars: Pass optional instance vars to emulate an instanced pipeline. :param env_vars: Pass additional environment variables, or overload the default ones. """ - def __init__(self, inner_resource_type: type[ConcourseResource[VersionT]], inner_resource_config: ResourceConfig, - directory_dict: FolderDict | None = None, one_off_build: bool = False, - instance_vars: dict[str, str] | None = None, **env_vars: str) -> None: - super().__init__(inner_resource_type, inner_resource_config, directory_dict, one_off_build, instance_vars, **env_vars) + + def __init__( + self, + inner_resource_type: type[ConcourseResource[VersionT]], + inner_resource_config: ResourceConfig, + directory_dict: FolderDict | None = None, + one_off_build: bool = False, + instance_vars: dict[str, str] | None = None, + **env_vars: str, + ) -> None: + super().__init__( + inner_resource_type, + inner_resource_config, + directory_dict, + one_off_build, + instance_vars, + **env_vars, + ) self.mocked_build_metadata = BuildMetadata(**self.mocked_environ) inner_resource = inner_resource_type(**inner_resource_config) self.inner_version_class = inner_resource.version_class - def fetch_new_versions(self, previous_version: VersionT | None = None) -> list[VersionT]: + def fetch_new_versions( + self, previous_version: VersionT | None = None + ) -> list[VersionT]: """ Fetch new versions of the resource. @@ -348,11 +453,18 @@ def fetch_new_versions(self, previous_version: VersionT | None = None) -> list[V if the resource has never been run before. :returns: A list of new versions. """ - previous_version_config = None if previous_version is None else previous_version.to_flat_dict() + previous_version_config = ( + None if previous_version is None else previous_version.to_flat_dict() + ) version_configs = super().fetch_new_versions(previous_version_config) - return [self.inner_version_class.from_flat_dict(version_config) for version_config in version_configs] - - def download_version(self, version: VersionT, **params: object) -> tuple[VersionT, Metadata]: + return [ + self.inner_version_class.from_flat_dict(version_config) + for version_config in version_configs + ] + + def download_version( + self, version: VersionT, **params: object + ) -> tuple[VersionT, Metadata]: """ Download a version and place its files within the resource directory in your pipeline. @@ -366,7 +478,9 @@ def download_version(self, version: VersionT, **params: object) -> tuple[Version :returns: The version (most likely unchanged), and a dictionary of metadata. """ version_config = version.to_flat_dict() - new_version_config, metadata_pairs = super().download_version(version_config, params or {}) + new_version_config, metadata_pairs = super().download_version( + version_config, params or {} + ) new_version = self.inner_version_class.from_flat_dict(new_version_config) metadata = parse_metadata(metadata_pairs) return new_version, metadata @@ -414,15 +528,29 @@ class FileTestResourceWrapper(TestResourceWrapper[Version]): .. caution:: If any of the paths for the scripts do not resolve, the corresponding methods will raise :class:`NotImplementedError`. """ - def __init__(self, inner_resource_config: ResourceConfig, check_script: Path | None = None, - in_script: Path | None = None, out_script: Path | None = None, - directory_dict: FolderDict | None = None, one_off_build: bool = False, - instance_vars: dict[str, str] | None = None, **env_vars: str) -> None: + + def __init__( + self, + inner_resource_config: ResourceConfig, + check_script: Path | None = None, + in_script: Path | None = None, + out_script: Path | None = None, + directory_dict: FolderDict | None = None, + one_off_build: bool = False, + instance_vars: dict[str, str] | None = None, + **env_vars: str, + ) -> None: super().__init__(directory_dict, one_off_build, instance_vars, **env_vars) self.inner_resource_config = inner_resource_config - self.check_script, self.in_script, self.out_script = check_script, in_script, out_script + self.check_script, self.in_script, self.out_script = ( + check_script, + in_script, + out_script, + ) - def fetch_new_versions(self, previous_version_config: VersionConfig | None = None) -> list[VersionConfig]: + def fetch_new_versions( + self, previous_version_config: VersionConfig | None = None + ) -> list[VersionConfig]: """ Fetch new versions of the resource. @@ -442,7 +570,9 @@ def fetch_new_versions(self, previous_version_config: VersionConfig | None = Non env["PYTHONPATH"] = f"{Path.cwd()}:$PYTHONPATH" stdin = format_check_input(self.inner_resource_config, previous_version_config) - stdout, stderr = run_script(self.check_script, additional_args=[], env=env, stdin=stdin) + stdout, stderr = run_script( + self.check_script, additional_args=[], env=env, stdin=stdin + ) self._debugging_output.inner_io.write(stderr) @@ -452,7 +582,9 @@ def fetch_new_versions(self, previous_version_config: VersionConfig | None = Non raise ValueError(f"Unexpected output: {stdout.strip()}") from error return version_configs - def download_version(self, version_config: VersionConfig, params: Params | None = None) -> tuple[VersionConfig, list[MetadataPair]]: + def download_version( + self, version_config: VersionConfig, params: Params | None = None + ) -> tuple[VersionConfig, list[MetadataPair]]: """ Download a version and place its files within the resource directory in your pipeline. @@ -470,8 +602,12 @@ def download_version(self, version_config: VersionConfig, params: Params | None stdin = format_in_input(self.inner_resource_config, version_config, params) with self._directory_state: - stdout, stderr = run_script(self.in_script, additional_args=[str(self._directory_state.path)], - env=env, stdin=stdin) + stdout, stderr = run_script( + self.in_script, + additional_args=[str(self._directory_state.path)], + env=env, + stdin=stdin, + ) self._debugging_output.inner_io.write(stderr) @@ -483,7 +619,9 @@ def download_version(self, version_config: VersionConfig, params: Params | None metadata_pairs: list[MetadataPair] = output["metadata"] or [] return new_version_config, metadata_pairs - def publish_new_version(self, params: Params | None = None) -> tuple[VersionConfig, list[MetadataPair]]: + def publish_new_version( + self, params: Params | None = None + ) -> tuple[VersionConfig, list[MetadataPair]]: """ Update a resource by publishing a new version. @@ -500,8 +638,12 @@ def publish_new_version(self, params: Params | None = None) -> tuple[VersionConf stdin = format_out_input(self.inner_resource_config, params) with self._directory_state: - stdout, stderr = run_script(self.out_script, additional_args=[str(self._directory_state.path)], - env=env, stdin=stdin) + stdout, stderr = run_script( + self.out_script, + additional_args=[str(self._directory_state.path)], + env=env, + stdin=stdin, + ) self._debugging_output.inner_io.write(stderr) @@ -514,9 +656,15 @@ def publish_new_version(self, params: Params | None = None) -> tuple[VersionConf return new_version_config, metadata_pairs @classmethod - def from_assets_dir(cls: type["FileTestResourceWrapper"], inner_resource_config: ResourceConfig, assets_dir: Path, - directory_dict: FolderDict | None = None, one_off_build: bool = False, - instance_vars: dict[str, str] | None = None, **env_vars: str) -> "FileTestResourceWrapper": + def from_assets_dir( + cls: type["FileTestResourceWrapper"], + inner_resource_config: ResourceConfig, + assets_dir: Path, + directory_dict: FolderDict | None = None, + one_off_build: bool = False, + instance_vars: dict[str, str] | None = None, + **env_vars: str, + ) -> "FileTestResourceWrapper": """ Create an instance from a single folder of asset files. @@ -530,9 +678,16 @@ def from_assets_dir(cls: type["FileTestResourceWrapper"], inner_resource_config: :param instance_vars: Pass optional instance vars to emulate an instanced pipeline. :param env_vars: Pass additional environment variables, or overload the default ones. """ - return cls(inner_resource_config, check_script=assets_dir / "check", in_script=assets_dir / "in", - out_script=assets_dir / "out", directory_dict=directory_dict, one_off_build=one_off_build, - instance_vars=instance_vars, **env_vars) + return cls( + inner_resource_config, + check_script=assets_dir / "check", + in_script=assets_dir / "in", + out_script=assets_dir / "out", + directory_dict=directory_dict, + one_off_build=one_off_build, + instance_vars=instance_vars, + **env_vars, + ) class FileConversionTestResourceWrapper(FileTestResourceWrapper, Generic[VersionT]): @@ -561,12 +716,29 @@ class FileConversionTestResourceWrapper(FileTestResourceWrapper, Generic[Version :param instance_vars: Pass optional instance vars to emulate an instanced pipeline. :param env_vars: Pass additional environment variables, or overload the default ones. """ - def __init__(self, inner_resource_type: type[ConcourseResource[VersionT]], inner_resource_config: ResourceConfig, - executable: str, permissions: int = 0o755, encoding: str | None = None, - directory_dict: FolderDict | None = None, one_off_build: bool = False, - instance_vars: dict[str, str] | None = None, **env_vars: str) -> None: - super().__init__(inner_resource_config, check_script=None, in_script=None, out_script=None, - directory_dict=directory_dict, one_off_build=one_off_build, instance_vars=instance_vars, **env_vars) + + def __init__( + self, + inner_resource_type: type[ConcourseResource[VersionT]], + inner_resource_config: ResourceConfig, + executable: str, + permissions: int = 0o755, + encoding: str | None = None, + directory_dict: FolderDict | None = None, + one_off_build: bool = False, + instance_vars: dict[str, str] | None = None, + **env_vars: str, + ) -> None: + super().__init__( + inner_resource_config, + check_script=None, + in_script=None, + out_script=None, + directory_dict=directory_dict, + one_off_build=one_off_build, + instance_vars=instance_vars, + **env_vars, + ) self.executable = executable self.permissions = permissions self.encoding = encoding @@ -574,7 +746,9 @@ def __init__(self, inner_resource_type: type[ConcourseResource[VersionT]], inner inner_resource = inner_resource_type(**inner_resource_config) self.inner_version_class = inner_resource.version_class - def fetch_new_versions(self, previous_version: VersionT | None = None) -> list[VersionT]: + def fetch_new_versions( + self, previous_version: VersionT | None = None + ) -> list[VersionT]: """ Fetch new versions of the resource. @@ -589,12 +763,19 @@ def fetch_new_versions(self, previous_version: VersionT | None = None) -> list[V if the resource has never been run before. :returns: A list of new versions. """ - previous_version_config = None if previous_version is None else previous_version.to_flat_dict() + previous_version_config = ( + None if previous_version is None else previous_version.to_flat_dict() + ) with self._temporarily_create_script_file("check", "check_main"): version_configs = super().fetch_new_versions(previous_version_config) - return [self.inner_version_class.from_flat_dict(version_config) for version_config in version_configs] - - def download_version(self, version: VersionT, **params: object) -> tuple[VersionT, Metadata]: + return [ + self.inner_version_class.from_flat_dict(version_config) + for version_config in version_configs + ] + + def download_version( + self, version: VersionT, **params: object + ) -> tuple[VersionT, Metadata]: """ Download a version and place its files within the resource directory in your pipeline. @@ -612,7 +793,9 @@ def download_version(self, version: VersionT, **params: object) -> tuple[Version """ version_config = version.to_flat_dict() with self._temporarily_create_script_file("in", "in_main"): - new_version_config, metadata_pairs = super().download_version(version_config, params or {}) + new_version_config, metadata_pairs = super().download_version( + version_config, params or {} + ) new_version = self.inner_version_class.from_flat_dict(new_version_config) metadata = parse_metadata(metadata_pairs) return new_version, metadata @@ -632,18 +815,29 @@ def publish_new_version(self, **params: object) -> tuple[VersionT, Metadata]: :returns: The new version, and a dictionary of metadata. """ with self._temporarily_create_script_file("out", "out_main"): - new_version_config, metadata_pairs = super().publish_new_version(params or {}) + new_version_config, metadata_pairs = super().publish_new_version( + params or {} + ) new_version = self.inner_version_class.from_flat_dict(new_version_config) metadata = parse_metadata(metadata_pairs) return new_version, metadata @contextmanager - def _temporarily_create_script_file(self, script_name: ScriptName, method_name: MethodName) -> ContextManager[None]: + def _temporarily_create_script_file( + self, script_name: ScriptName, method_name: MethodName + ) -> ContextManager[None]: attribute_name = f"{script_name}_script" try: with TemporaryDirectory() as temp_dir: script_path = Path(temp_dir) / script_name - create_script_file(script_path, self.inner_resource_type, method_name, self.executable, self.permissions, self.encoding) + create_script_file( + script_path, + self.inner_resource_type, + method_name, + self.executable, + self.permissions, + self.encoding, + ) setattr(self, attribute_name, script_path) yield finally: @@ -676,14 +870,23 @@ class DockerTestResourceWrapper(TestResourceWrapper[Version]): The working directory is explicitly set to ``/`` within the container to ensure that the resource is properly accounting for the paths it is passed. """ - def __init__(self, inner_resource_config: ResourceConfig, image: str, - directory_dict: FolderDict | None = None, one_off_build: bool = False, - instance_vars: dict[str, str] | None = None, **env_vars: str) -> None: + + def __init__( + self, + inner_resource_config: ResourceConfig, + image: str, + directory_dict: FolderDict | None = None, + one_off_build: bool = False, + instance_vars: dict[str, str] | None = None, + **env_vars: str, + ) -> None: super().__init__(directory_dict, one_off_build, instance_vars, **env_vars) self.inner_resource_config = inner_resource_config self.image = image - def fetch_new_versions(self, previous_version_config: VersionConfig | None = None) -> list[VersionConfig]: + def fetch_new_versions( + self, previous_version_config: VersionConfig | None = None + ) -> list[VersionConfig]: """ Fetch new versions of the resource. @@ -698,8 +901,15 @@ def fetch_new_versions(self, previous_version_config: VersionConfig | None = Non """ stdin = format_check_input(self.inner_resource_config, previous_version_config) with self._directory_state: - stdout, stderr = run_docker_container(self.image, "/opt/resource/check", additional_args=[], env={}, - cwd=Path("/"), stdin=stdin, hostname="resource") + stdout, stderr = run_docker_container( + self.image, + "/opt/resource/check", + additional_args=[], + env={}, + cwd=Path("/"), + stdin=stdin, + hostname="resource", + ) self._debugging_output.inner_io.write(stderr) @@ -709,7 +919,9 @@ def fetch_new_versions(self, previous_version_config: VersionConfig | None = Non raise ValueError(f"Unexpected output: {stdout.strip()}") from error return version_configs - def download_version(self, version_config: VersionConfig, params: Params | None = None) -> tuple[VersionConfig, list[MetadataPair]]: + def download_version( + self, version_config: VersionConfig, params: Params | None = None + ) -> tuple[VersionConfig, list[MetadataPair]]: """ Download a version and place its files within the resource directory in your pipeline. @@ -722,10 +934,16 @@ def download_version(self, version_config: VersionConfig, params: Params | None stdin = format_in_input(self.inner_resource_config, version_config, params) inner_temp_dir = f"/tmp/{secrets.token_hex(4)}" with self._directory_state: - stdout, stderr = run_docker_container(self.image, "/opt/resource/in", additional_args=[inner_temp_dir], - env=self.mocked_environ.copy(), cwd=Path("/"), stdin=stdin, - dir_mapping={self._directory_state.path: inner_temp_dir}, - hostname="resource") + stdout, stderr = run_docker_container( + self.image, + "/opt/resource/in", + additional_args=[inner_temp_dir], + env=self.mocked_environ.copy(), + cwd=Path("/"), + stdin=stdin, + dir_mapping={self._directory_state.path: inner_temp_dir}, + hostname="resource", + ) self._debugging_output.inner_io.write(stderr) @@ -737,7 +955,9 @@ def download_version(self, version_config: VersionConfig, params: Params | None metadata_pairs: list[MetadataPair] = output["metadata"] or [] return new_version_config, metadata_pairs - def publish_new_version(self, params: Params | None = None) -> tuple[VersionConfig, list[MetadataPair]]: + def publish_new_version( + self, params: Params | None = None + ) -> tuple[VersionConfig, list[MetadataPair]]: """ Update a resource by publishing a new version. @@ -749,10 +969,16 @@ def publish_new_version(self, params: Params | None = None) -> tuple[VersionConf stdin = format_out_input(self.inner_resource_config, params) inner_temp_dir = f"/tmp/{secrets.token_hex(4)}" with self._directory_state: - stdout, stderr = run_docker_container(self.image, "/opt/resource/out", additional_args=[inner_temp_dir], - env=self.mocked_environ.copy(), cwd=Path("/"), stdin=stdin, - dir_mapping={self._directory_state.path: inner_temp_dir}, - hostname="resource") + stdout, stderr = run_docker_container( + self.image, + "/opt/resource/out", + additional_args=[inner_temp_dir], + env=self.mocked_environ.copy(), + cwd=Path("/"), + stdin=stdin, + dir_mapping={self._directory_state.path: inner_temp_dir}, + hostname="resource", + ) self._debugging_output.inner_io.write(stderr) @@ -785,16 +1011,32 @@ class DockerConversionTestResourceWrapper(DockerTestResourceWrapper, Generic[Ver :param instance_vars: Pass optional instance vars to emulate an instanced pipeline. :param env_vars: Pass additional environment variables, or overload the default ones. """ - def __init__(self, inner_resource_type: type[ConcourseResource[VersionT]], inner_resource_config: ResourceConfig, - image: str, directory_dict: FolderDict | None = None, one_off_build: bool = False, - instance_vars: dict[str, str] | None = None, **env_vars: str) -> None: - super().__init__(inner_resource_config, image, directory_dict=directory_dict, one_off_build=one_off_build, - instance_vars=instance_vars, **env_vars) + + def __init__( + self, + inner_resource_type: type[ConcourseResource[VersionT]], + inner_resource_config: ResourceConfig, + image: str, + directory_dict: FolderDict | None = None, + one_off_build: bool = False, + instance_vars: dict[str, str] | None = None, + **env_vars: str, + ) -> None: + super().__init__( + inner_resource_config, + image, + directory_dict=directory_dict, + one_off_build=one_off_build, + instance_vars=instance_vars, + **env_vars, + ) self.inner_resource_type = inner_resource_type inner_resource = inner_resource_type(**inner_resource_config) self.inner_version_class = inner_resource.version_class - def fetch_new_versions(self, previous_version: VersionT | None = None) -> list[VersionT]: + def fetch_new_versions( + self, previous_version: VersionT | None = None + ) -> list[VersionT]: """ Fetch new versions of the resource. @@ -805,11 +1047,18 @@ def fetch_new_versions(self, previous_version: VersionT | None = None) -> list[V if the resource has never been run before. :returns: A list of new versions. """ - previous_version_config = None if previous_version is None else previous_version.to_flat_dict() + previous_version_config = ( + None if previous_version is None else previous_version.to_flat_dict() + ) version_configs = super().fetch_new_versions(previous_version_config) - return [self.inner_version_class.from_flat_dict(version_config) for version_config in version_configs] - - def download_version(self, version: VersionT, **params: object) -> tuple[VersionT, Metadata]: + return [ + self.inner_version_class.from_flat_dict(version_config) + for version_config in version_configs + ] + + def download_version( + self, version: VersionT, **params: object + ) -> tuple[VersionT, Metadata]: """ Download a version and place its files within the resource directory in your pipeline. @@ -822,7 +1071,9 @@ def download_version(self, version: VersionT, **params: object) -> tuple[Version :returns: The version (most likely unchanged), and a dictionary of metadata. """ version_config = version.to_flat_dict() - new_version_config, metadata_pairs = super().download_version(version_config, params or {}) + new_version_config, metadata_pairs = super().download_version( + version_config, params or {} + ) new_version = self.inner_version_class.from_flat_dict(new_version_config) metadata = parse_metadata(metadata_pairs) return new_version, metadata @@ -844,11 +1095,19 @@ def publish_new_version(self, **params: object) -> tuple[VersionT, Metadata]: return new_version, metadata -def run_docker_container(image: str, command: str, additional_args: list[str] | None = None, - env: dict[str, str] | None = None, cwd: Path | None = None, - stdin: str | None = None, rm: bool = True, interactive: bool = True, - dir_mapping: dict[Path, Path | str] | None = None, - hostname: str | None = None, local_only: bool = True) -> tuple[str, str]: +def run_docker_container( + image: str, + command: str, + additional_args: list[str] | None = None, + env: dict[str, str] | None = None, + cwd: Path | None = None, + stdin: str | None = None, + rm: bool = True, + interactive: bool = True, + dir_mapping: dict[Path, Path | str] | None = None, + hostname: str | None = None, + local_only: bool = True, +) -> tuple[str, str]: """ Run a command within the Docker container. @@ -916,8 +1175,13 @@ def run_docker_container(image: str, command: str, additional_args: list[str] | return run_command("docker", docker_args, stdin=stdin) -def run_script(script_path: Path, additional_args: list[str] | None = None, env: dict[str, str] | None = None, - cwd: Path | None = None, stdin: str | None = None) -> tuple[str, str]: +def run_script( + script_path: Path, + additional_args: list[str] | None = None, + env: dict[str, str] | None = None, + cwd: Path | None = None, + stdin: str | None = None, +) -> tuple[str, str]: """ Run an external script. @@ -937,8 +1201,13 @@ def run_script(script_path: Path, additional_args: list[str] | None = None, env: return run_command(str(script_path), additional_args, env=env, cwd=cwd, stdin=stdin) -def run_command(command: str, additional_args: list[str] | None = None, env: dict[str, str] | None = None, - cwd: Path | None = None, stdin: str | None = None) -> tuple[str, str]: +def run_command( + command: str, + additional_args: list[str] | None = None, + env: dict[str, str] | None = None, + cwd: Path | None = None, + stdin: str | None = None, +) -> tuple[str, str]: """ Run an external command. diff --git a/concoursetools/typing.py b/concoursetools/typing.py index 7a223c5..89ab032 100644 --- a/concoursetools/typing.py +++ b/concoursetools/typing.py @@ -7,6 +7,7 @@ class breaks the :wikipedia:`Liskov substitution principle`. If you wish to utilise type hinting in your development process, then please switch off the check for this type of method overloading. """ + from __future__ import annotations from collections.abc import Callable @@ -41,6 +42,7 @@ class MetadataPair(TypedDict): Restrictions on key and value types are determined by the `Go structure itself `_. """ + name: str value: str @@ -60,129 +62,114 @@ class MetadataPair(TypedDict): SortableVersionT = TypeVar("SortableVersionT", bound="SortableVersionProtocol") """Represents a generic :class:`~concoursetools.version.Version` subclass which is also :ref:`sortable `.""" -SortableVersionCovariantT = TypeVar("SortableVersionCovariantT", bound="SortableVersionProtocol", covariant=True) +SortableVersionCovariantT = TypeVar( + "SortableVersionCovariantT", bound="SortableVersionProtocol", covariant=True +) class VersionProtocol(Protocol): """Corresponds to a generic :class:`~concoursetools.version.Version` subclass.""" - def __repr__(self) -> str: - ... - def __eq__(self, other: object) -> bool: - ... + def __repr__(self) -> str: ... + + def __eq__(self, other: object) -> bool: ... - def __hash__(self) -> int: - ... + def __hash__(self) -> int: ... - def to_flat_dict(self) -> VersionConfig: - ... + def to_flat_dict(self) -> VersionConfig: ... @classmethod - def from_flat_dict(cls: type[VersionT], version_dict: VersionConfig) -> VersionT: - ... + def from_flat_dict( + cls: type[VersionT], version_dict: VersionConfig + ) -> VersionT: ... class SortableVersionProtocol(Protocol): """Corresponds to a generic :class:`~concoursetools.version.Version` subclass which is also :ref:`sortable `.""" - def __repr__(self) -> str: - ... - def __eq__(self, other: object) -> bool: - ... + def __repr__(self) -> str: ... - def __hash__(self) -> int: - ... + def __eq__(self, other: object) -> bool: ... - def to_flat_dict(self) -> VersionConfig: - ... + def __hash__(self) -> int: ... + + def to_flat_dict(self) -> VersionConfig: ... @classmethod - def from_flat_dict(cls: type[VersionT], version_dict: VersionConfig) -> VersionT: - ... + def from_flat_dict( + cls: type[VersionT], version_dict: VersionConfig + ) -> VersionT: ... - def __lt__(self, other: object) -> bool: - ... + def __lt__(self, other: object) -> bool: ... - def __le__(self, other: object) -> bool: - ... + def __le__(self, other: object) -> bool: ... class TypedVersionProtocol(Protocol): """Corresponds to a generic :class:`~concoursetools.version.TypedVersion` subclass.""" - def __repr__(self) -> str: - ... - def __eq__(self, other: object) -> bool: - ... + def __repr__(self) -> str: ... + + def __eq__(self, other: object) -> bool: ... - def __hash__(self) -> int: - ... + def __hash__(self) -> int: ... - def to_flat_dict(self) -> VersionConfig: - ... + def to_flat_dict(self) -> VersionConfig: ... @classmethod - def from_flat_dict(cls: type[VersionT], version_dict: VersionConfig) -> VersionT: - ... + def from_flat_dict( + cls: type[VersionT], version_dict: VersionConfig + ) -> VersionT: ... @classmethod - def _flatten_object(cls, obj: Any) -> str: - ... + def _flatten_object(cls, obj: Any) -> str: ... @classmethod - def _un_flatten_object(cls, type_: type[TypedVersionT], flat_obj: str) -> TypedVersionT: - ... + def _un_flatten_object( + cls, type_: type[TypedVersionT], flat_obj: str + ) -> TypedVersionT: ... @classmethod - def _get_attribute_type(cls, attribute_name: str) -> type[Any]: - ... + def _get_attribute_type(cls, attribute_name: str) -> type[Any]: ... @classmethod - def flatten(cls, func: Callable[[T], str]) -> Callable[[T], str]: - ... + def flatten(cls, func: Callable[[T], str]) -> Callable[[T], str]: ... @classmethod - def un_flatten(cls, func: Callable[[type[T], str], T]) -> Callable[[type[T], str], T]: - ... + def un_flatten( + cls, func: Callable[[type[T], str], T] + ) -> Callable[[type[T], str], T]: ... @staticmethod - def _flatten_default(obj: object) -> str: - ... + def _flatten_default(obj: object) -> str: ... @staticmethod - def _un_flatten_default(type_: type[T], flat_obj: str) -> T: - ... + def _un_flatten_default(type_: type[T], flat_obj: str) -> T: ... class MultiVersionProtocol(Protocol[SortableVersionCovariantT]): """Corresponds to a generic :class:`~concoursetools.additional.MultiVersion` subclass.""" - def __init__(self, versions: set[SortableVersionCovariantT]): - ... - def __repr__(self) -> str: - ... + def __init__(self, versions: set[SortableVersionCovariantT]): ... + + def __repr__(self) -> str: ... - def __eq__(self, other: object) -> bool: - ... + def __eq__(self, other: object) -> bool: ... - def __hash__(self) -> int: - ... + def __hash__(self) -> int: ... @property - def key(self) -> str: - ... + def key(self) -> str: ... @property - def sub_version_class(self) -> type[SortableVersionCovariantT]: - ... + def sub_version_class(self) -> type[SortableVersionCovariantT]: ... @property - def sub_version_data(self) -> list[VersionConfig]: - ... + def sub_version_data(self) -> list[VersionConfig]: ... - def to_flat_dict(self) -> VersionConfig: - ... + def to_flat_dict(self) -> VersionConfig: ... @classmethod - def from_flat_dict(cls: type[VersionT], version_dict: VersionConfig) -> VersionT: - ... + def from_flat_dict( + cls: type[VersionT], version_dict: VersionConfig + ) -> VersionT: ... diff --git a/concoursetools/version.py b/concoursetools/version.py index d79ab34..08b9d27 100644 --- a/concoursetools/version.py +++ b/concoursetools/version.py @@ -5,6 +5,7 @@ More information on versions can be found in the :concourse:`Concourse documentation `. """ + from __future__ import annotations from abc import ABC, abstractmethod @@ -55,8 +56,11 @@ def __init__(self, commit_hash): To change this behaviour, overload the :meth:`to_flat_dict` and :meth:`from_flat_dict` methods in the class. """ + def __repr__(self) -> str: - attr_string = ", ".join(f"{attr}={value!r}" for attr, value in vars(self).items()) + attr_string = ", ".join( + f"{attr}={value!r}" for attr, value in vars(self).items() + ) return f"{type(self).__name__}({attr_string})" def __eq__(self, other: object) -> bool: @@ -108,7 +112,11 @@ def to_flat_dict(self): "timestamp": str(int(self.date.timestamp())), } """ - return {str(key): str(value) for key, value in vars(self).items() if not key.startswith("_")} + return { + str(key): str(value) + for key, value in vars(self).items() + if not key.startswith("_") + } @classmethod def from_flat_dict(cls: type[VersionT], version_dict: VersionConfig) -> VersionT: @@ -177,6 +185,7 @@ class SortableVersionMixin(ABC): ... except AttributeError: ... return NotImplemented """ + @abstractmethod def __lt__(self, other: object) -> bool: pass @@ -213,6 +222,7 @@ class _TypeKeyDict(UserDict): # type: ignore[type-arg] In almost all circumstances, this is the same type (in user-defined classes, for example), but avoids an issue in which a type from the typing module is set instead of the class it represents. """ + def __getitem__(self, key: type[object]) -> object: for parent_class in key.mro(): try: @@ -222,7 +232,9 @@ def __getitem__(self, key: type[object]) -> object: raise KeyError(f"{key} not found in mapping") def __setitem__(self, key: type[object], item: object) -> None: - proper_key = key.mro()[0] # almost always the same, except for objects in typing + proper_key = key.mro()[ + 0 + ] # almost always the same, except for objects in typing return super().__setitem__(proper_key, item) @@ -257,8 +269,13 @@ class TypedVersion(Version): The full MRO of each object is looked up when calling the flatten and un-flatten functions, so any type which is a *subclass* of a registered type will still call the same functions, unless explicitly overwritten. """ - _flatten_functions: ClassVar[MutableMapping[type[Any], Callable[[Any], str]]] = _TypeKeyDict() - _un_flatten_functions: ClassVar[MutableMapping[type[Any], Callable[[type[Any], str], Any]]] = _TypeKeyDict() + + _flatten_functions: ClassVar[MutableMapping[type[Any], Callable[[Any], str]]] = ( + _TypeKeyDict() + ) + _un_flatten_functions: ClassVar[ + MutableMapping[type[Any], Callable[[type[Any], str], Any]] + ] = _TypeKeyDict() def __init_subclass__(cls) -> None: try: @@ -267,14 +284,25 @@ def __init_subclass__(cls) -> None: annotations = {} if len(annotations) == 0: - raise TypeError("Can't instantiate dataclass TypedVersion without any fields") + raise TypeError( + "Can't instantiate dataclass TypedVersion without any fields" + ) def to_flat_dict(self) -> VersionConfig: - return {str(key): self._flatten_object(value) for key, value in vars(self).items() if not key.startswith("_")} + return { + str(key): self._flatten_object(value) + for key, value in vars(self).items() + if not key.startswith("_") + } @classmethod - def from_flat_dict(cls: type[TypedVersionT], version_dict: VersionConfig) -> TypedVersionT: - un_flattened_kwargs = {key: cls._un_flatten_object(cls._get_attribute_type(key), value) for key, value in version_dict.items()} + def from_flat_dict( + cls: type[TypedVersionT], version_dict: VersionConfig + ) -> TypedVersionT: + un_flattened_kwargs = { + key: cls._un_flatten_object(cls._get_attribute_type(key), value) + for key, value in version_dict.items() + } return super().from_flat_dict(un_flattened_kwargs) @classmethod @@ -290,7 +318,9 @@ def _flatten_object(cls, obj: object) -> str: def _un_flatten_object(cls, type_: type[T], flat_obj: str) -> T: """Un-flatten an object from a string based on a destination type.""" try: # for some reason `cls._un_flatten_functions.get` fails on 3.12 - un_flatten_function: Callable[[type[T], str], T] = cls._un_flatten_functions[type_] + un_flatten_function: Callable[[type[T], str], T] = ( + cls._un_flatten_functions[type_] + ) except KeyError: un_flatten_function = cls._un_flatten_default return un_flatten_function(type_, flat_obj) @@ -323,7 +353,9 @@ def flatten(cls, func: Callable[[T], str]) -> Callable[[T], str]: return func @classmethod - def un_flatten(cls, func: Callable[[type[T], str], T]) -> Callable[[type[T], str], T]: + def un_flatten( + cls, func: Callable[[type[T], str], T] + ) -> Callable[[type[T], str], T]: """ Register a function for un-flattening a string to a specific type. diff --git a/docs/source/conf.py b/docs/source/conf.py index 921b4e4..185846a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -59,13 +59,17 @@ autodoc_member_order = "bysource" autodoc_custom_types = { - concoursetools.cli.parser.CLIFunctionT: format_annotation(Callable[..., None], sphinx.config.Config()), + concoursetools.cli.parser.CLIFunctionT: format_annotation( + Callable[..., None], sphinx.config.Config() + ), concoursetools.importing.T: ":class:`object`", concoursetools.typing.VersionT: ":class:`~concoursetools.version.Version`", concoursetools.typing.SortableVersionT: ":class:`~concoursetools.version.Version`", } -suppress_warnings = ["config.cache"] # https://github.com/sphinx-doc/sphinx/issues/12300#issuecomment-2062238457 +suppress_warnings = [ + "config.cache" +] # https://github.com/sphinx-doc/sphinx/issues/12300#issuecomment-2062238457 def typehints_formatter(annotation: Any, config: sphinx.config.Config) -> str | None: @@ -79,7 +83,9 @@ def typehints_formatter(annotation: Any, config: sphinx.config.Config) -> str | ("py:class", "concoursetools.typing.SortableVersionT"), ] -linkcheck_report_timeouts_as_broken = False # silences a warning: https://github.com/sphinx-doc/sphinx/issues/11868 +linkcheck_report_timeouts_as_broken = ( + False # silences a warning: https://github.com/sphinx-doc/sphinx/issues/11868 +) linkcheck_anchors_ignore_for_url = [ "https://github.com/.*", ] @@ -118,6 +124,6 @@ def typehints_formatter(annotation: Any, config: sphinx.config.Config) -> str | "name": "PyPI", "url": "https://pypi.org/project/concoursetools/", "class": "fa-brands fa-python fa-2x", - } + }, ], } diff --git a/docs/source/extensions/cli.py b/docs/source/extensions/cli.py index 3ebd701..cb061c3 100644 --- a/docs/source/extensions/cli.py +++ b/docs/source/extensions/cli.py @@ -2,6 +2,7 @@ """ Sphinx extension for documenting the custom CLI. """ + from __future__ import annotations from typing import Literal @@ -23,6 +24,7 @@ class CLIDirective(SphinxDirective): """ Directive for listing CLI commands in a table. """ + required_arguments = 1 # The import path of the CLI option_spec = { "align": _align_directive, @@ -34,18 +36,35 @@ def run(self) -> list[nodes.Node]: """ align: Literal["left", "center", "right"] | None = self.options.get("align") - headers = [nodes.entry("", nodes.paragraph("", header)) for header in ["Command", "Description"]] + headers = [ + nodes.entry("", nodes.paragraph("", header)) + for header in ["Command", "Description"] + ] - import_string, = self.arguments + (import_string,) = self.arguments cli = self.import_cli(import_string) rows: list[list[nodes.entry]] = [] for command_name, command in cli.commands.items(): - rows.append([ - nodes.entry("", nodes.paragraph("", "", nodes.reference("", "", nodes.literal("", command_name), refid=f"cli.{command_name}"))), - nodes.entry("", nodes.paragraph("", command.description or "")), - ]) + rows.append( + [ + nodes.entry( + "", + nodes.paragraph( + "", + "", + nodes.reference( + "", + "", + nodes.literal("", command_name), + refid=f"cli.{command_name}", + ), + ), + ), + nodes.entry("", nodes.paragraph("", command.description or "")), + ] + ) table = self.create_table(headers, rows, align=align) @@ -53,7 +72,9 @@ def run(self) -> list[nodes.Node]: for command_name, command in cli.commands.items(): command_section = nodes.section(ids=[f"cli.{command_name}"]) - title = nodes.title(f"cli.{command_name}", "", nodes.literal("", command_name)) + title = nodes.title( + f"cli.{command_name}", "", nodes.literal("", command_name) + ) command_section.append(title) if command.description is not None: @@ -63,8 +84,12 @@ def run(self) -> list[nodes.Node]: command_section.append(usage_block) for positional in command.positional_arguments: - alias_paragraph = nodes.paragraph("", "", nodes.literal("", positional.name)) - description_paragraph = nodes.paragraph("", positional.description or "") + alias_paragraph = nodes.paragraph( + "", "", nodes.literal("", positional.name) + ) + description_paragraph = nodes.paragraph( + "", positional.description or "" + ) description_paragraph.set_class("cli-option-description") command_section.extend([alias_paragraph, description_paragraph]) @@ -82,8 +107,12 @@ def run(self) -> list[nodes.Node]: return nodes_to_return - def create_table(self, headers: list[nodes.entry], rows: list[list[nodes.entry]], - align: Literal["left", "center", "right"] | None = None) -> nodes.table: + def create_table( + self, + headers: list[nodes.entry], + rows: list[list[nodes.entry]], + align: Literal["left", "center", "right"] | None = None, + ) -> nodes.table: table = nodes.table() if align is not None: table["align"] = align diff --git a/docs/source/extensions/concourse.py b/docs/source/extensions/concourse.py index 45cda9d..4f93861 100644 --- a/docs/source/extensions/concourse.py +++ b/docs/source/extensions/concourse.py @@ -10,6 +10,7 @@ Set ``concourse_base_url`` in ``conf.py`` to change the URL used. It must contain "{target}" to be populated. """ + from __future__ import annotations from urllib.parse import quote @@ -25,8 +26,15 @@ DEFAULT_BASE_URL = "https://concourse-ci.org/{target}" -def make_concourse_link(name: str, rawtext: str, text: str, lineno: int, inliner: Inliner, options: dict[str, object] = {}, - content: list[str] = []) -> tuple[list[nodes.reference], list[system_message]]: +def make_concourse_link( + name: str, + rawtext: str, + text: str, + lineno: int, + inliner: Inliner, + options: dict[str, object] = {}, + content: list[str] = [], +) -> tuple[list[nodes.reference], list[system_message]]: """ Add a link to the given article on Concourse CI. diff --git a/docs/source/extensions/linecount.py b/docs/source/extensions/linecount.py index a541cc1..1215d46 100644 --- a/docs/source/extensions/linecount.py +++ b/docs/source/extensions/linecount.py @@ -7,6 +7,7 @@ Paths should be relative to the directory of the current file, not the source directory. This is the same path passed to literalinclude. """ + from __future__ import annotations from pathlib import Path @@ -19,8 +20,15 @@ __all__ = ("count_lines", "setup") -def count_lines(name: str, rawtext: str, text: str, lineno: int, inliner: Inliner, options: dict[str, object] = {}, - content: list[str] = []) -> tuple[list[nodes.Node], list[system_message]]: +def count_lines( + name: str, + rawtext: str, + text: str, + lineno: int, + inliner: Inliner, + options: dict[str, object] = {}, + content: list[str] = [], +) -> tuple[list[nodes.Node], list[system_message]]: """ Add a text node containing the number of lines. diff --git a/docs/source/extensions/wikipedia.py b/docs/source/extensions/wikipedia.py index 90aed1c..37d39b7 100644 --- a/docs/source/extensions/wikipedia.py +++ b/docs/source/extensions/wikipedia.py @@ -8,6 +8,7 @@ Set ``wikipedia_base_url`` in ``conf.py`` to change the URL used. It must contain "{target}" to be populated. It can also contain "{lang}" to allow the language code (e.g. "de") to be injected with the ``wikipedia_lang`` variable. """ + from __future__ import annotations import re @@ -25,8 +26,15 @@ RE_WIKI_LANG = re.compile(":(.*?):(.*)") -def make_wikipedia_link(name: str, rawtext: str, text: str, lineno: int, inliner: Inliner, options: dict[str, object] = {}, - content: list[str] = []) -> tuple[list[nodes.reference], list[system_message]]: +def make_wikipedia_link( + name: str, + rawtext: str, + text: str, + lineno: int, + inliner: Inliner, + options: dict[str, object] = {}, + content: list[str] = [], +) -> tuple[list[nodes.reference], list[system_message]]: """ Add a link to the given article on :wikipedia:`Wikipedia`. @@ -47,7 +55,7 @@ def make_wikipedia_link(name: str, rawtext: str, text: str, lineno: int, inliner text = nodes.unescape(text) # type: ignore[attr-defined] has_explicit, title, target = split_explicit_title(text) - if (match := RE_WIKI_LANG.match(target)): + if match := RE_WIKI_LANG.match(target): lang, target = match.groups() if not has_explicit: title = target diff --git a/docs/source/extensions/xkcd.py b/docs/source/extensions/xkcd.py index 6810e24..f5ed2cd 100644 --- a/docs/source/extensions/xkcd.py +++ b/docs/source/extensions/xkcd.py @@ -12,6 +12,7 @@ Set ``xkcd_endpoint`` in ``conf.py`` to change the URL used. """ + from __future__ import annotations from typing import Any @@ -34,6 +35,7 @@ class XkcdDirective(SphinxDirective): """ Directive for xkcd comics. """ + required_arguments = 1 # The comic number option_spec = { "height": directives.length_or_unitless, @@ -52,14 +54,20 @@ def run(self) -> list[nodes.Node]: """ Process the content of the shield directive. """ - comic_number, = self.arguments - comic_info = self.get_comic_info(int(comic_number), self.config["xkcd_endpoint"]) + (comic_number,) = self.arguments + comic_info = self.get_comic_info( + int(comic_number), self.config["xkcd_endpoint"] + ) caption = self.options.pop("caption", "Relevant xkcd") set_classes(self.options) - image_node = nodes.image(self.block_text, uri=directives.uri(comic_info["img"]), - alt=comic_info["alt"], **self.options) + image_node = nodes.image( + self.block_text, + uri=directives.uri(comic_info["img"]), + alt=comic_info["alt"], + **self.options, + ) self.add_name(image_node) reference_node = nodes.reference("", "", refuri=comic_info["link"]) @@ -72,7 +80,9 @@ def run(self) -> list[nodes.Node]: figure_node += caption_node return [figure_node] - def get_comic_info(self, comic_number: int, endpoint: str = DEFAULT_XKCD) -> dict[str, Any]: + def get_comic_info( + self, comic_number: int, endpoint: str = DEFAULT_XKCD + ) -> dict[str, Any]: comic_link = f"{endpoint}/{comic_number}/" try: response = requests.get(f"{endpoint}/{comic_number}/info.0.json") @@ -89,7 +99,9 @@ def get_comic_info(self, comic_number: int, endpoint: str = DEFAULT_XKCD) -> dic latest_json: dict[str, Any] = latest_response.json() most_recent_comic: int = latest_json["num"] if most_recent_comic < comic_number: - raise ValueError(f"You asked for xkcd #{comic_number}, but the most recent available comic is #{most_recent_comic}") + raise ValueError( + f"You asked for xkcd #{comic_number}, but the most recent available comic is #{most_recent_comic}" + ) else: raise else: @@ -98,8 +110,15 @@ def get_comic_info(self, comic_number: int, endpoint: str = DEFAULT_XKCD) -> dic return response_json -def make_xkcd_link(name: str, rawtext: str, text: str, lineno: int, inliner: Inliner, - options: dict[str, object] = {}, content: list[str] = []) -> tuple[list[nodes.reference], list[nodes.system_message]]: +def make_xkcd_link( + name: str, + rawtext: str, + text: str, + lineno: int, + inliner: Inliner, + options: dict[str, object] = {}, + content: list[str] = [], +) -> tuple[list[nodes.reference], list[nodes.system_message]]: """ Add a link to a page on the xkcd website. diff --git a/examples/build_status.py b/examples/build_status.py index adad7af..1c747b8 100644 --- a/examples/build_status.py +++ b/examples/build_status.py @@ -33,6 +33,7 @@ class BitbucketOAuth(AuthBase): """ Adds the correct auth token for OAuth access to bitbucket.com. """ + def __init__(self, access_token: str): self.access_token = access_token @@ -41,7 +42,9 @@ def __call__(self, request: requests.Request) -> requests.Request: return request @classmethod - def from_client_credentials(cls, client_id: str, client_secret: str) -> "BitbucketOAuth": + def from_client_credentials( + cls, client_id: str, client_secret: str + ) -> "BitbucketOAuth": token_auth = HTTPBasicAuth(client_id, client_secret) url = "https://bitbucket.org/site/oauth2/access_token" @@ -59,19 +62,27 @@ class Version(TypedVersion): class Resource(OutOnlyConcourseResource[Version]): - - def __init__(self, repository: str | None = None, endpoint: str | None = None, - username: str | None = None, password: str | None = None, - client_id: str | None = None, client_secret: str | None = None, - verify_ssl: bool = True, driver: str = "Bitbucket Server", - debug: bool = False) -> None: + def __init__( + self, + repository: str | None = None, + endpoint: str | None = None, + username: str | None = None, + password: str | None = None, + client_id: str | None = None, + client_secret: str | None = None, + verify_ssl: bool = True, + driver: str = "Bitbucket Server", + debug: bool = False, + ) -> None: super().__init__(Version) try: self.driver = Driver(driver) except ValueError: possible_values = {enum.value for enum in Driver._member_map_.values()} - raise ValueError(f"Driver must be one of the following: " - f"{possible_values}, not {driver!r}") + raise ValueError( + f"Driver must be one of the following: " + f"{possible_values}, not {driver!r}" + ) self.auth = create_auth(username, password, client_id, client_secret) @@ -91,19 +102,28 @@ def __init__(self, repository: str | None = None, endpoint: str | None = None, if repository is None: raise ValueError("Must set repository when using Bitbucket Cloud.") - def publish_new_version(self, sources_dir: Path, build_metadata: BuildMetadata, - repository: str, build_status: str, key: str | None = None, - name: str | None = None, build_url: str | None = None, - description: str | None = None, - commit_hash: str | None = None) -> tuple[Version, Metadata]: + def publish_new_version( + self, + sources_dir: Path, + build_metadata: BuildMetadata, + repository: str, + build_status: str, + key: str | None = None, + name: str | None = None, + build_url: str | None = None, + description: str | None = None, + commit_hash: str | None = None, + ) -> tuple[Version, Metadata]: self.debug("--DEBUG MODE--") try: status = BuildStatus[build_status] except KeyError: possible_values = set(BuildStatus._member_names_) - raise ValueError(f"Build status must be one of the following: " - f"{possible_values}, not {build_status!r}") + raise ValueError( + f"Build status must be one of the following: " + f"{possible_values}, not {build_status!r}" + ) if commit_hash is None: if repository is None: @@ -113,8 +133,16 @@ def publish_new_version(self, sources_dir: Path, build_metadata: BuildMetadata, git_path = repo_path / ".git" if mercurial_path.exists(): - command = ["hg", "R", str(repo_path), "log", - "--rev", ".", "--template", r"{node}"] + command = [ + "hg", + "R", + str(repo_path), + "log", + "--rev", + ".", + "--template", + r"{node}", + ] elif git_path.exists(): command = ["git", "-C", str(repo_path), "rev-parse", "HEAD"] else: @@ -126,11 +154,17 @@ def publish_new_version(self, sources_dir: Path, build_metadata: BuildMetadata, build_url = build_url or build_metadata.build_url() - key = key or build_metadata.BUILD_JOB_NAME or f"one-off-build-{build_metadata.BUILD_ID}" + key = ( + key + or build_metadata.BUILD_JOB_NAME + or f"one-off-build-{build_metadata.BUILD_ID}" + ) self.debug(f"Build URL: {build_url}") - description = description or f"Concourse CI build, hijack as #{build_metadata.BUILD_ID}" + description = ( + description or f"Concourse CI build, hijack as #{build_metadata.BUILD_ID}" + ) if name is None: if build_metadata.is_one_off_build: @@ -159,7 +193,9 @@ def publish_new_version(self, sources_dir: Path, build_metadata: BuildMetadata, self.debug(f"Set build status: {data}") - response = requests.post(post_url, json=data, auth=self.auth, verify=self.verify_ssl) + response = requests.post( + post_url, json=data, auth=self.auth, verify=self.verify_ssl + ) self.debug(f"Request result: {response.json()}") @@ -174,10 +210,12 @@ def debug(self, *args: object, colour: str = Colour.CYAN, **kwargs: Any) -> None colour_print(*args, colour=colour, **kwargs) -def create_auth(username: str | None = None, - password: str | None = None, - client_id: str | None = None, - client_secret: str | None = None) -> AuthBase: +def create_auth( + username: str | None = None, + password: str | None = None, + client_id: str | None = None, + client_secret: str | None = None, +) -> AuthBase: if username is not None and password is not None: auth: AuthBase = HTTPBasicAuth(username, password) elif client_id is not None and client_secret is not None: diff --git a/examples/github_branches.py b/examples/github_branches.py index 74df115..42725d0 100644 --- a/examples/github_branches.py +++ b/examples/github_branches.py @@ -16,8 +16,13 @@ class BranchVersion(TypedVersion): class Resource(MultiVersionConcourseResource[BranchVersion]): # type: ignore[type-var] - def __init__(self, owner: str, repo: str, regex: str = ".*", - endpoint: str = "https://api.github.com") -> None: + def __init__( + self, + owner: str, + repo: str, + regex: str = ".*", + endpoint: str = "https://api.github.com", + ) -> None: """ Initialise self. @@ -42,5 +47,8 @@ def fetch_latest_sub_versions(self) -> set[BranchVersion]: message = branches_info["message"] raise RuntimeError(message) from error - return {BranchVersion(branch_name) for branch_name in branch_names - if self.regex.fullmatch(branch_name)} + return { + BranchVersion(branch_name) + for branch_name in branch_names + if self.regex.fullmatch(branch_name) + } diff --git a/examples/pipeline.py b/examples/pipeline.py index 1b960b0..c62b2e3 100644 --- a/examples/pipeline.py +++ b/examples/pipeline.py @@ -15,7 +15,6 @@ class DatetimeSafeJSONEncoder(json.JSONEncoder): - def default(self, o: object) -> object: if isinstance(o, datetime): return o.isoformat() @@ -28,8 +27,9 @@ class ExecutionVersion(TypedVersion): class PipelineResource(ConcourseResource[ExecutionVersion]): - - def __init__(self, pipeline: str, statuses: list[str] = ["Succeeded", "Stopped", "Failed"]) -> None: + def __init__( + self, pipeline: str, statuses: list[str] = ["Succeeded", "Stopped", "Failed"] + ) -> None: super().__init__(ExecutionVersion) # arn:aws:sagemaker:::pipeline: _, _, _, region, _, _, pipeline_name = pipeline.split(":") @@ -37,7 +37,9 @@ def __init__(self, pipeline: str, statuses: list[str] = ["Succeeded", "Stopped", self.pipeline_name = pipeline_name self.statuses = statuses - def fetch_new_versions(self, previous_version: ExecutionVersion | None = None) -> list[ExecutionVersion]: + def fetch_new_versions( + self, previous_version: ExecutionVersion | None = None + ) -> list[ExecutionVersion]: potential_versions = iter(self._yield_potential_execution_versions()) if previous_version is None: try: @@ -58,18 +60,27 @@ def fetch_new_versions(self, previous_version: ExecutionVersion | None = None) - new_versions.reverse() return new_versions - def download_version(self, version: ExecutionVersion, destination_dir: Path, - build_metadata: BuildMetadata, download_pipeline: bool = True, - metadata_file: str = "metadata.json", - pipeline_file: str = "pipeline.json") -> tuple[ExecutionVersion, dict[str, str]]: - response = self._client.describe_pipeline_execution(PipelineExecutionArn=version.execution_arn) + def download_version( + self, + version: ExecutionVersion, + destination_dir: Path, + build_metadata: BuildMetadata, + download_pipeline: bool = True, + metadata_file: str = "metadata.json", + pipeline_file: str = "pipeline.json", + ) -> tuple[ExecutionVersion, dict[str, str]]: + response = self._client.describe_pipeline_execution( + PipelineExecutionArn=version.execution_arn + ) response.pop("ResponseMetadata") metadata_path = destination_dir / metadata_file metadata_path.write_text(json.dumps(response, cls=DatetimeSafeJSONEncoder)) if download_pipeline: - pipeline_response = self._client.describe_pipeline_definition_for_execution(PipelineExecutionArn=version.execution_arn) + pipeline_response = self._client.describe_pipeline_definition_for_execution( + PipelineExecutionArn=version.execution_arn + ) pipeline_path = destination_dir / pipeline_file pipeline_path.write_text(pipeline_response["PipelineDefinition"]) @@ -87,11 +98,18 @@ def download_version(self, version: ExecutionVersion, destination_dir: Path, return version, metadata - def publish_new_version(self, sources_dir: Path, build_metadata: BuildMetadata, - display_name: str | None = None, description: str | None = None, - parameters: dict[str, str] = {}) -> tuple[ExecutionVersion, dict[str, str]]: - default_description = (f"Execution from build #{build_metadata.BUILD_ID} " - f"of pipeline {build_metadata.BUILD_PIPELINE_NAME}") + def publish_new_version( + self, + sources_dir: Path, + build_metadata: BuildMetadata, + display_name: str | None = None, + description: str | None = None, + parameters: dict[str, str] = {}, + ) -> tuple[ExecutionVersion, dict[str, str]]: + default_description = ( + f"Execution from build #{build_metadata.BUILD_ID} " + f"of pipeline {build_metadata.BUILD_PIPELINE_NAME}" + ) kwargs: dict[str, object] = { "PipelineName": self.pipeline_name, "PipelineExecutionDescription": description or default_description, @@ -101,10 +119,13 @@ def publish_new_version(self, sources_dir: Path, build_metadata: BuildMetadata, kwargs["PipelineExecutionDisplayName"] = display_name if parameters: - kwargs["PipelineParameters"] = [{"Name": name, "Value": value} - for name, value in parameters.items()] - metadata = {f"Parameter: {parameter}": value - for parameter, value in parameters.items()} + kwargs["PipelineParameters"] = [ + {"Name": name, "Value": value} for name, value in parameters.items() + ] + metadata = { + f"Parameter: {parameter}": value + for parameter, value in parameters.items() + } else: metadata = {} @@ -113,7 +134,9 @@ def publish_new_version(self, sources_dir: Path, build_metadata: BuildMetadata, new_version = ExecutionVersion(execution_arn) return new_version, metadata - def _yield_potential_execution_versions(self) -> Generator[ExecutionVersion, None, None]: + def _yield_potential_execution_versions( + self, + ) -> Generator[ExecutionVersion, None, None]: kwargs = { "PipelineName": self.pipeline_name, "SortOrder": "Descending", @@ -132,4 +155,6 @@ def _yield_potential_execution_versions(self) -> Generator[ExecutionVersion, Non except KeyError: break - response = self._client.list_pipeline_executions(**kwargs, NextToken=next_token) + response = self._client.list_pipeline_executions( + **kwargs, NextToken=next_token + ) diff --git a/examples/s3.py b/examples/s3.py index 918a9d7..42bb13e 100644 --- a/examples/s3.py +++ b/examples/s3.py @@ -15,6 +15,7 @@ class S3SignedURLConcourseResource(InOnlyConcourseResource): """ A Concourse resource type for generating pre-signed URLs for items in S3 buckets. """ + def __init__(self, bucket_name: str, region_name: str) -> None: """ Initialise self. @@ -24,26 +25,32 @@ def __init__(self, bucket_name: str, region_name: str) -> None: """ super().__init__() self.bucket_name = bucket_name - self.client = boto3.client("s3", region_name=region_name, - config=Config(signature_version="s3v4")) - - def download_data(self, destination_dir: Path, build_metadata: BuildMetadata, - file_path: str, expires_in: dict[str, float], - file_name: str | None = None, - url_file: str = "url") -> dict[str, str]: + self.client = boto3.client( + "s3", region_name=region_name, config=Config(signature_version="s3v4") + ) + + def download_data( + self, + destination_dir: Path, + build_metadata: BuildMetadata, + file_path: str, + expires_in: dict[str, float], + file_name: str | None = None, + url_file: str = "url", + ) -> dict[str, str]: params = { "Bucket": self.bucket_name, "Key": file_path, } if file_name is not None: # https://stackoverflow.com/a/2612795 - content_disposition = f"attachment; filename=\"{file_name}\"" + content_disposition = f'attachment; filename="{file_name}"' params["ResponseContentDisposition"] = content_disposition expiry_seconds = int(timedelta(**expires_in).total_seconds()) - url = self.client.generate_presigned_url(ClientMethod="get_object", - Params=params, - ExpiresIn=expiry_seconds) + url = self.client.generate_presigned_url( + ClientMethod="get_object", Params=params, ExpiresIn=expiry_seconds + ) url_file_path = destination_dir / url_file url_file_path.write_text(url) diff --git a/examples/secrets.py b/examples/secrets.py index 0f4fc40..525ec40 100644 --- a/examples/secrets.py +++ b/examples/secrets.py @@ -16,7 +16,6 @@ class DatetimeSafeJSONEncoder(json_package.JSONEncoder): - def default(self, o: object) -> object: if isinstance(o, datetime): return o.isoformat() @@ -32,6 +31,7 @@ class Resource(TriggerOnChangeConcourseResource[SecretVersion]): """ :param secret: The full Amazon Resource Name (ARN) of the secret. """ + def __init__(self, secret: str) -> None: super().__init__(SecretVersion) self.secret = secret @@ -42,8 +42,9 @@ def __init__(self, secret: str) -> None: def fetch_latest_version(self) -> SecretVersion: try: - response = self._client.list_secret_version_ids(SecretId=self.secret, - IncludeDeprecated=False) + response = self._client.list_secret_version_ids( + SecretId=self.secret, IncludeDeprecated=False + ) except ClientError: raise ValueError(f"Cannot find secret: {self.secret!r}") @@ -55,16 +56,24 @@ def fetch_latest_version(self) -> SecretVersion: return SecretVersion(version_id) raise RuntimeError("No current version of the secret could be found.") - def download_version(self, version: SecretVersion, destination_dir: Path, - build_metadata: BuildMetadata, value: bool = False, - metadata_file: str = "metadata.json", - value_file: str = "value") -> tuple[SecretVersion, dict[str, str]]: - meta_response: dict[str, Any] = self._client.describe_secret(SecretId=self.secret) + def download_version( + self, + version: SecretVersion, + destination_dir: Path, + build_metadata: BuildMetadata, + value: bool = False, + metadata_file: str = "metadata.json", + value_file: str = "value", + ) -> tuple[SecretVersion, dict[str, str]]: + meta_response: dict[str, Any] = self._client.describe_secret( + SecretId=self.secret + ) meta_response.pop("ResponseMetadata") metadata_path = destination_dir / metadata_file - metadata_path.write_text(json_package.dumps(meta_response, - cls=DatetimeSafeJSONEncoder)) + metadata_path.write_text( + json_package.dumps(meta_response, cls=DatetimeSafeJSONEncoder) + ) if value: value_response = self._client.get_secret_value(SecretId=self.secret) @@ -80,26 +89,30 @@ def download_version(self, version: SecretVersion, destination_dir: Path, return version, {} - def publish_new_version(self, sources_dir: Path, - build_metadata: BuildMetadata, string: str | None = None, - file: str | None = None, - json: dict[str, str] | None = None) -> tuple[SecretVersion, dict[str, str]]: + def publish_new_version( + self, + sources_dir: Path, + build_metadata: BuildMetadata, + string: str | None = None, + file: str | None = None, + json: dict[str, str] | None = None, + ) -> tuple[SecretVersion, dict[str, str]]: if json is not None: string = json_package.dumps(json) if string is not None: - response = self._client.put_secret_value(SecretId=self.secret, - SecretString=string) + response = self._client.put_secret_value( + SecretId=self.secret, SecretString=string + ) elif file is not None: file_path = sources_dir / file file_contents = file_path.read_bytes() - response = self._client.put_secret_value(SecretId=self.secret, - SecretBinary=file_contents) + response = self._client.put_secret_value( + SecretId=self.secret, SecretBinary=file_contents + ) else: raise ValueError("Missing new value for the secret.") version_id = response["VersionId"] - metadata = { - "Version Staging Labels": ", ".join(response["VersionStages"]) - } + metadata = {"Version Staging Labels": ", ".join(response["VersionStages"])} return SecretVersion(version_id), metadata diff --git a/examples/xkcd.py b/examples/xkcd.py index d0d5331..ef0182f 100644 --- a/examples/xkcd.py +++ b/examples/xkcd.py @@ -27,7 +27,6 @@ def __lt__(self, other: object) -> bool: class XKCDResource(SelfOrganisingConcourseResource[ComicVersion]): - def __init__(self, url: str = "https://xkcd.com"): super().__init__(ComicVersion) self.url = url @@ -38,9 +37,15 @@ def fetch_all_versions(self) -> set[ComicVersion]: feed_data = response.text return {ComicVersion(comic_id) for comic_id in yield_comic_ids(feed_data)} - def download_version(self, version: ComicVersion, destination_dir: Path, - build_metadata: BuildMetadata, image: bool = True, - link: bool = True, alt: bool = True) -> tuple[ComicVersion, dict[str, str]]: + def download_version( + self, + version: ComicVersion, + destination_dir: Path, + build_metadata: BuildMetadata, + image: bool = True, + link: bool = True, + alt: bool = True, + ) -> tuple[ComicVersion, dict[str, str]]: comic_info_url = f"{self.url}/{version.comic_id}/info.0.json" response = requests.get(comic_info_url) info = response.json() @@ -48,8 +53,9 @@ def download_version(self, version: ComicVersion, destination_dir: Path, title = info["title"] url = f"{self.url}/{version.comic_id}/" - upload_date = datetime(year=int(info["year"]), month=int(info["month"]), - day=int(info["day"])) + upload_date = datetime( + year=int(info["year"]), month=int(info["month"]), day=int(info["day"]) + ) metadata = { "Title": title, "Uploaded": upload_date.strftime(r"%d/%m/%Y"), @@ -76,7 +82,9 @@ def download_version(self, version: ComicVersion, destination_dir: Path, return version, metadata - def publish_new_version(self, sources_dir: Path, build_metadata: BuildMetadata) -> tuple[ComicVersion, dict[str, str]]: + def publish_new_version( + self, sources_dir: Path, build_metadata: BuildMetadata + ) -> tuple[ComicVersion, dict[str, str]]: raise NotImplementedError diff --git a/setup.py b/setup.py index 1c6ab71..127a98e 100644 --- a/setup.py +++ b/setup.py @@ -2,6 +2,7 @@ """ Setup for the concoursetools Python package. """ + from setuptools import setup if __name__ == "__main__": diff --git a/tests/resource.py b/tests/resource.py index 86693f8..6f655e9 100644 --- a/tests/resource.py +++ b/tests/resource.py @@ -2,6 +2,7 @@ """ Contains a test resource. """ + from __future__ import annotations from copy import copy @@ -14,28 +15,33 @@ class TestVersion(concoursetools.Version): - def __init__(self, ref: str): self.ref = ref class TestResource(concoursetools.ConcourseResource[TestVersion]): - def __init__(self, uri: str, branch: str = "main", private_key: str | None = None): super().__init__(TestVersion) self.uri = uri self.branch = branch self.private_key = private_key - def fetch_new_versions(self, previous_version: TestVersion | None = None) -> list[TestVersion]: + def fetch_new_versions( + self, previous_version: TestVersion | None = None + ) -> list[TestVersion]: if previous_version: print("Previous version found.") return [TestVersion("7154fe")] else: return [TestVersion(ref) for ref in ("61cbef", "d74e01", "7154fe")] - def download_version(self, version: TestVersion, destination_dir: Path, build_metadata: concoursetools.BuildMetadata, - file_name: str = "README.txt") -> tuple[TestVersion, Metadata]: + def download_version( + self, + version: TestVersion, + destination_dir: Path, + build_metadata: concoursetools.BuildMetadata, + file_name: str = "README.txt", + ) -> tuple[TestVersion, Metadata]: print("Downloading.") readme_path = destination_dir / file_name readme_path.write_text(f"Downloaded README for ref {version.ref}.\n") @@ -44,8 +50,13 @@ def download_version(self, version: TestVersion, destination_dir: Path, build_me } return version, metadata - def publish_new_version(self, sources_dir: Path, build_metadata: concoursetools.BuildMetadata, repo: str, - ref_file: str = "ref.txt") -> tuple[TestVersion, Metadata]: + def publish_new_version( + self, + sources_dir: Path, + build_metadata: concoursetools.BuildMetadata, + repo: str, + ref_file: str = "ref.txt", + ) -> tuple[TestVersion, Metadata]: ref_path = sources_dir / repo / ref_file ref = ref_path.read_text() print("Uploading.") @@ -59,7 +70,9 @@ class ConcourseMockVersion(concoursetools.TypedVersion): ConcourseMockVersion._flatten_functions = copy(ConcourseMockVersion._flatten_functions) -ConcourseMockVersion._un_flatten_functions = copy(ConcourseMockVersion._un_flatten_functions) +ConcourseMockVersion._un_flatten_functions = copy( + ConcourseMockVersion._un_flatten_functions +) @ConcourseMockVersion.flatten @@ -73,16 +86,23 @@ def _(_type: type[bool], obj: str) -> bool: class ConcourseMockResource(concoursetools.ConcourseResource[ConcourseMockVersion]): - def __init__(self, **kwargs: object) -> None: super().__init__(ConcourseMockVersion) - def fetch_new_versions(self, previous_version: ConcourseMockVersion | None = None) -> list[ConcourseMockVersion]: + def fetch_new_versions( + self, previous_version: ConcourseMockVersion | None = None + ) -> list[ConcourseMockVersion]: raise NotImplementedError - def download_version(self, version: ConcourseMockVersion, destination_dir: Path, - build_metadata: BuildMetadata) -> tuple[ConcourseMockVersion, Metadata]: + def download_version( + self, + version: ConcourseMockVersion, + destination_dir: Path, + build_metadata: BuildMetadata, + ) -> tuple[ConcourseMockVersion, Metadata]: raise NotImplementedError - def publish_new_version(self, sources_dir: Path, build_metadata: BuildMetadata) -> tuple[ConcourseMockVersion, Metadata]: + def publish_new_version( + self, sources_dir: Path, build_metadata: BuildMetadata + ) -> tuple[ConcourseMockVersion, Metadata]: raise NotImplementedError diff --git a/tests/test_additional.py b/tests/test_additional.py index 0977600..27984f2 100644 --- a/tests/test_additional.py +++ b/tests/test_additional.py @@ -10,9 +10,16 @@ import urllib.request from concoursetools import BuildMetadata, ConcourseResource -from concoursetools.additional import (DatetimeVersion, InOnlyConcourseResource, MultiVersionConcourseResource, OutOnlyConcourseResource, - SelfOrganisingConcourseResource, TriggerOnChangeConcourseResource, _create_multi_version_class, - combine_resource_types) +from concoursetools.additional import ( + DatetimeVersion, + InOnlyConcourseResource, + MultiVersionConcourseResource, + OutOnlyConcourseResource, + SelfOrganisingConcourseResource, + TriggerOnChangeConcourseResource, + _create_multi_version_class, + combine_resource_types, +) from concoursetools.testing import JSONTestResourceWrapper, SimpleTestResourceWrapper from concoursetools.typing import Metadata, VersionConfig from concoursetools.version import SortableVersionMixin, Version @@ -20,7 +27,6 @@ class SortableTestVersion(TestVersion, SortableVersionMixin): - def __lt__(self, other: object) -> bool: if not isinstance(other, type(self)): return NotImplemented @@ -28,23 +34,31 @@ def __lt__(self, other: object) -> bool: class OrganisingResource(SelfOrganisingConcourseResource[SortableTestVersion]): - def __init__(self) -> None: super().__init__(SortableTestVersion) - def download_version(self, version: SortableTestVersion, destination_dir: Path, - build_metadata: BuildMetadata) -> tuple[SortableTestVersion, Metadata]: + def download_version( + self, + version: SortableTestVersion, + destination_dir: Path, + build_metadata: BuildMetadata, + ) -> tuple[SortableTestVersion, Metadata]: return version, {} - def publish_new_version(self, sources_dir: Path, build_metadata: BuildMetadata) -> tuple[SortableTestVersion, Metadata]: + def publish_new_version( + self, sources_dir: Path, build_metadata: BuildMetadata + ) -> tuple[SortableTestVersion, Metadata]: return SortableTestVersion(""), {} def fetch_all_versions(self) -> set[SortableTestVersion]: - return {SortableTestVersion("222"), SortableTestVersion("333"), SortableTestVersion("111")} + return { + SortableTestVersion("222"), + SortableTestVersion("333"), + SortableTestVersion("111"), + } class FileVersion(Version): - def __init__(self, files: set[str]) -> None: self.files = files @@ -57,22 +71,24 @@ def from_flat_dict(cls, version_dict: VersionConfig) -> "FileVersion": class TriggerResource(TriggerOnChangeConcourseResource[FileVersion]): - def __init__(self) -> None: super().__init__(FileVersion) def fetch_latest_version(self) -> FileVersion: return FileVersion({"file.txt", "image.png"}) - def download_version(self, version: FileVersion, destination_dir: Path, build_metadata: BuildMetadata) -> tuple[FileVersion, Metadata]: + def download_version( + self, version: FileVersion, destination_dir: Path, build_metadata: BuildMetadata + ) -> tuple[FileVersion, Metadata]: raise NotImplementedError - def publish_new_version(self, sources_dir: Path, build_metadata: BuildMetadata) -> tuple[FileVersion, Metadata]: + def publish_new_version( + self, sources_dir: Path, build_metadata: BuildMetadata + ) -> tuple[FileVersion, Metadata]: raise NotImplementedError class OrganisingTests(TestCase): - def test_no_previous(self) -> None: resource = OrganisingResource() versions = resource.fetch_new_versions(None) @@ -108,7 +124,6 @@ def fetch_all_versions(self) -> set[SortableTestVersion]: class TriggerTests(TestCase): - def setUp(self) -> None: """Code to run before each test.""" self.resource = TriggerResource() @@ -136,14 +151,12 @@ def test_with_more(self) -> None: class GitRepoSubVersionUnsortable(Version): - def __init__(self, project: str, repo: str) -> None: self.project = project self.repo = repo class GitRepoSubVersion(GitRepoSubVersionUnsortable, SortableVersionMixin): - def __lt__(self, other: object) -> bool: if not isinstance(other, type(self)): return NotImplemented @@ -151,45 +164,72 @@ def __lt__(self, other: object) -> bool: class GitRepoMultiVersionResource(MultiVersionConcourseResource[GitRepoSubVersion]): - def __init__(self) -> None: super().__init__("repos", GitRepoSubVersion) def fetch_latest_sub_versions(self) -> set[GitRepoSubVersion]: - return {GitRepoSubVersion("XYZ", "testing"), GitRepoSubVersion("XYZ", "repo"), GitRepoSubVersion("ABC", "alphabet")} + return { + GitRepoSubVersion("XYZ", "testing"), + GitRepoSubVersion("XYZ", "repo"), + GitRepoSubVersion("ABC", "alphabet"), + } class MultiVersionTests(TestCase): """ Tests for the MultiVersionConcourseResource class. """ + def setUp(self) -> None: - self.multi_version_class = _create_multi_version_class("repos", GitRepoSubVersion) + self.multi_version_class = _create_multi_version_class( + "repos", GitRepoSubVersion + ) def test_flattening(self) -> None: - sub_versions = {GitRepoSubVersion("XYZ", "testing"), GitRepoSubVersion("XYZ", "repo"), GitRepoSubVersion("ABC", "alphabet")} - sorted_versions = [GitRepoSubVersion("ABC", "alphabet"), GitRepoSubVersion("XYZ", "repo"), GitRepoSubVersion("XYZ", "testing")] + sub_versions = { + GitRepoSubVersion("XYZ", "testing"), + GitRepoSubVersion("XYZ", "repo"), + GitRepoSubVersion("ABC", "alphabet"), + } + sorted_versions = [ + GitRepoSubVersion("ABC", "alphabet"), + GitRepoSubVersion("XYZ", "repo"), + GitRepoSubVersion("XYZ", "testing"), + ] multi_version = self.multi_version_class(sub_versions) flat = multi_version.to_flat_dict() - expected_payload = json.dumps([sub_version.to_flat_dict() for sub_version in sorted_versions]) - self.assertDictEqual(flat, { - "repos": expected_payload, - }) + expected_payload = json.dumps( + [sub_version.to_flat_dict() for sub_version in sorted_versions] + ) + self.assertDictEqual( + flat, + { + "repos": expected_payload, + }, + ) self.assertEqual(self.multi_version_class.from_flat_dict(flat), multi_version) def test_flattening_unsortable(self) -> None: sub_versions = { GitRepoSubVersionUnsortable("XYZ", "testing"), GitRepoSubVersionUnsortable("XYZ", "repo"), - GitRepoSubVersionUnsortable("ABC", "alphabet") + GitRepoSubVersionUnsortable("ABC", "alphabet"), } multi_version = self.multi_version_class(sub_versions) # type: ignore[arg-type] with self.assertRaises(TypeError): multi_version.to_flat_dict() def test_resource_download(self) -> None: - sub_versions = {GitRepoSubVersion("XYZ", "testing"), GitRepoSubVersion("XYZ", "repo"), GitRepoSubVersion("ABC", "alphabet")} - sorted_versions = [GitRepoSubVersion("ABC", "alphabet"), GitRepoSubVersion("XYZ", "repo"), GitRepoSubVersion("XYZ", "testing")] + sub_versions = { + GitRepoSubVersion("XYZ", "testing"), + GitRepoSubVersion("XYZ", "repo"), + GitRepoSubVersion("ABC", "alphabet"), + } + sorted_versions = [ + GitRepoSubVersion("ABC", "alphabet"), + GitRepoSubVersion("XYZ", "repo"), + GitRepoSubVersion("XYZ", "testing"), + ] multi_version = self.multi_version_class(sub_versions) resource = GitRepoMultiVersionResource() @@ -199,17 +239,22 @@ def test_resource_download(self) -> None: wrapper.download_version(multi_version) self.assertEqual(debugging, "") - expected_payload = json.dumps([sub_version.to_flat_dict() for sub_version in sorted_versions]) - self.assertDictEqual(directory_state.final_state, {"repos.json": expected_payload}) + expected_payload = json.dumps( + [sub_version.to_flat_dict() for sub_version in sorted_versions] + ) + self.assertDictEqual( + directory_state.final_state, {"repos.json": expected_payload} + ) class ImageDownloadResource(InOnlyConcourseResource): - def __init__(self, image_url: str) -> None: super().__init__() self.image_url = image_url - def download_data(self, destination_dir: Path, build_metadata: BuildMetadata, name: str = "image") -> Metadata: + def download_data( + self, destination_dir: Path, build_metadata: BuildMetadata, name: str = "image" + ) -> Metadata: image_path = destination_dir / name request = urllib.request.Request(url=self.image_url) @@ -229,54 +274,64 @@ class InOnlyTests(TestCase): """ Tests for the InOnlyConcourseResource class. """ + def test_data_download(self) -> None: - resource = ImageDownloadResource(image_url="https://www.gchq.gov.uk/files/favicon.ico") + resource = ImageDownloadResource( + image_url="https://www.gchq.gov.uk/files/favicon.ico" + ) wrapper = SimpleTestResourceWrapper(resource) with wrapper.capture_debugging() as debugging: with wrapper.capture_directory_state() as directory_state: wrapper.download_version(DatetimeVersion.now(), name="favicon.ico") self.assertEqual(debugging, "") - self.assertDictEqual(directory_state.final_state, { - "favicon.ico": b"\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x01\x00 \x00w%", - }) + self.assertDictEqual( + directory_state.final_state, + { + "favicon.ico": b"\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x01\x00 \x00w%", + }, + ) class VersionA(Version): - def __init__(self, a: str) -> None: self.a = a class VersionB(Version): - def __init__(self, b: str) -> None: self.b = b class ResourceA(ConcourseResource[VersionA]): - def __init__(self, something_a: str) -> None: super().__init__(VersionA) self.something = something_a - def fetch_new_versions(self, previous_version: VersionA | None = None) -> list[VersionA]: + def fetch_new_versions( + self, previous_version: VersionA | None = None + ) -> list[VersionA]: return [VersionA("")] - def download_version(self, version: VersionA, destination_dir: Path, build_metadata: BuildMetadata) -> tuple[VersionA, Metadata]: + def download_version( + self, version: VersionA, destination_dir: Path, build_metadata: BuildMetadata + ) -> tuple[VersionA, Metadata]: return version, {"a": ""} - def publish_new_version(self, sources_dir: Path, build_metadata: BuildMetadata) -> tuple[VersionA, Metadata]: + def publish_new_version( + self, sources_dir: Path, build_metadata: BuildMetadata + ) -> tuple[VersionA, Metadata]: return VersionA(""), {} class ResourceB(OutOnlyConcourseResource[VersionB]): - def __init__(self, something_b: str) -> None: super().__init__(VersionB) self.something = something_b - def publish_new_version(self, sources_dir: Path, build_metadata: BuildMetadata) -> tuple[VersionB, Metadata]: + def publish_new_version( + self, sources_dir: Path, build_metadata: BuildMetadata + ) -> tuple[VersionB, Metadata]: return VersionB(""), {} @@ -284,7 +339,6 @@ def publish_new_version(self, sources_dir: Path, build_metadata: BuildMetadata) class MultiResourceTests(TestCase): - def test_check_a(self) -> None: wrapper = JSONTestResourceWrapper(ResourceA, {"something_a": ""}) version_configs = wrapper.fetch_new_versions() @@ -296,12 +350,16 @@ def test_check_b(self) -> None: self.assertListEqual(version_configs, []) def test_check_combined_delegate_a(self) -> None: - wrapper = JSONTestResourceWrapper(CombinedResource, {"something_a": "", "resource": "A"}) + wrapper = JSONTestResourceWrapper( + CombinedResource, {"something_a": "", "resource": "A"} + ) version_configs = wrapper.fetch_new_versions() self.assertListEqual(version_configs, [{"a": ""}]) def test_check_combined_delegate_b(self) -> None: - wrapper = JSONTestResourceWrapper(CombinedResource, {"something_b": "", "resource": "B"}) + wrapper = JSONTestResourceWrapper( + CombinedResource, {"something_b": "", "resource": "B"} + ) version_configs = wrapper.fetch_new_versions() self.assertListEqual(version_configs, []) @@ -311,12 +369,16 @@ def test_missing_flag(self) -> None: wrapper.fetch_new_versions() def test_unexpected_resource(self) -> None: - wrapper = JSONTestResourceWrapper(CombinedResource, {"something_a": "", "resource": "C"}) + wrapper = JSONTestResourceWrapper( + CombinedResource, {"something_a": "", "resource": "C"} + ) with self.assertRaises(KeyError): wrapper.fetch_new_versions() def test_mismatched_params(self) -> None: - wrapper = JSONTestResourceWrapper(CombinedResource, {"something_b": "", "resource": "A"}) + wrapper = JSONTestResourceWrapper( + CombinedResource, {"something_b": "", "resource": "A"} + ) with self.assertRaises(TypeError): wrapper.fetch_new_versions() diff --git a/tests/test_cli_commands.py b/tests/test_cli_commands.py index d708ba0..35db8ff 100644 --- a/tests/test_cli_commands.py +++ b/tests/test_cli_commands.py @@ -18,6 +18,7 @@ class AssetTests(unittest.TestCase): """ Tests for creation of the asset files. """ + def setUp(self) -> None: """Code to run before each test.""" self._temp_dir = TemporaryDirectory() @@ -45,13 +46,16 @@ def test_asset_scripts(self) -> None: self.assertEqual(new_stdout.getvalue(), "") self.assertTrue(asset_dir.exists()) - self.assertSetEqual({path.name for path in asset_dir.iterdir()}, {"check", "in", "out"}) + self.assertSetEqual( + {path.name for path in asset_dir.iterdir()}, {"check", "in", "out"} + ) class DockerfileTests(unittest.TestCase): """ Tests for creating the Dockerfile. """ + def setUp(self) -> None: """Code to run before each test.""" self._temp_dir = TemporaryDirectory() @@ -64,7 +68,9 @@ def setUp(self) -> None: self.dockerfile_path = self.temp_dir / "Dockerfile" self.assertFalse(self.dockerfile_path.exists()) - self.current_python_string = f"{sys.version_info.major}.{sys.version_info.minor}" + self.current_python_string = ( + f"{sys.version_info.major}.{sys.version_info.minor}" + ) os.chdir(self.temp_dir) @@ -107,6 +113,7 @@ class LegacyTests(unittest.TestCase): """ Tests for the legacy CLI. """ + def setUp(self) -> None: """Code to run before each test.""" self._temp_dir = TemporaryDirectory() @@ -119,7 +126,9 @@ def setUp(self) -> None: self.dockerfile_path = self.temp_dir / "Dockerfile" self.assertFalse(self.dockerfile_path.exists()) - self.current_python_string = f"{sys.version_info.major}.{sys.version_info.minor}" + self.current_python_string = ( + f"{sys.version_info.major}.{sys.version_info.minor}" + ) os.chdir(self.temp_dir) @@ -133,12 +142,18 @@ def test_docker(self) -> None: with redirect_stdout(new_stdout): cli.invoke(["legacy", ".", "--docker"]) - self.assertEqual(new_stdout.getvalue(), colourise(textwrap.dedent(""" + self.assertEqual( + new_stdout.getvalue(), + colourise( + textwrap.dedent(""" The legacy CLI has been deprecated. Please refer to the documentation or help pages for the up to date CLI. This CLI will be removed in version 0.10.0, or in version 1.0.0, whichever is sooner. - """), colour=Colour.RED)) + """), + colour=Colour.RED, + ), + ) dockerfile_contents = self.dockerfile_path.read_text() expected_contents = textwrap.dedent(f""" @@ -166,12 +181,20 @@ def test_asset_scripts(self) -> None: with redirect_stdout(new_stdout): cli.invoke(["legacy", "assets", "-c", "TestResource"]) - self.assertEqual(new_stdout.getvalue(), colourise(textwrap.dedent(""" + self.assertEqual( + new_stdout.getvalue(), + colourise( + textwrap.dedent(""" The legacy CLI has been deprecated. Please refer to the documentation or help pages for the up to date CLI. This CLI will be removed in version 0.10.0, or in version 1.0.0, whichever is sooner. - """), colour=Colour.RED)) + """), + colour=Colour.RED, + ), + ) self.assertTrue(asset_dir.exists()) - self.assertSetEqual({path.name for path in asset_dir.iterdir()}, {"check", "in", "out"}) + self.assertSetEqual( + {path.name for path in asset_dir.iterdir()}, {"check", "in", "out"} + ) diff --git a/tests/test_cli_parsing.py b/tests/test_cli_parsing.py index 0e2e187..a510bd4 100644 --- a/tests/test_cli_parsing.py +++ b/tests/test_cli_parsing.py @@ -9,34 +9,51 @@ import unittest.mock from concoursetools import __version__ -from concoursetools.cli.parser import _ANNOTATIONS_TO_TYPES, _CURRENT_PYTHON_VERSION, CLI, Docstring +from concoursetools.cli.parser import ( + _ANNOTATIONS_TO_TYPES, + _CURRENT_PYTHON_VERSION, + CLI, + Docstring, +) class ParsingTests(unittest.TestCase): - def test_all_defaults(self) -> None: args, kwargs = test_cli.commands["first_command"].parse_args(["abcd", "123"]) self.assertListEqual(args, ["abcd", 123]) - self.assertDictEqual(kwargs, { - "option_1": "value", - "option_2": False, - }) + self.assertDictEqual( + kwargs, + { + "option_1": "value", + "option_2": False, + }, + ) def test_parsing_args(self) -> None: - args, kwargs = test_cli.commands["first_command"].parse_args(["abcd", "123", "--option-1", "new_value", "--option-2"]) + args, kwargs = test_cli.commands["first_command"].parse_args( + ["abcd", "123", "--option-1", "new_value", "--option-2"] + ) self.assertListEqual(args, ["abcd", 123]) - self.assertDictEqual(kwargs, { - "option_1": "new_value", - "option_2": True, - }) + self.assertDictEqual( + kwargs, + { + "option_1": "new_value", + "option_2": True, + }, + ) def test_parsing_shorter_args(self) -> None: - args, kwargs = test_cli.commands["first_command"].parse_args(["abcd", "123", "-o", "new_value", "--option-2"]) + args, kwargs = test_cli.commands["first_command"].parse_args( + ["abcd", "123", "-o", "new_value", "--option-2"] + ) self.assertListEqual(args, ["abcd", 123]) - self.assertDictEqual(kwargs, { - "option_1": "new_value", - "option_2": True, - }) + self.assertDictEqual( + kwargs, + { + "option_1": "new_value", + "option_2": True, + }, + ) def test_missing_positional(self) -> None: with self.assertCLIError(): @@ -54,12 +71,13 @@ def assertCLIError(self) -> Generator[None, None, None]: class InvokeTests(unittest.TestCase): - def test_defaults(self) -> None: new_stdout = StringIO() with redirect_stdout(new_stdout): test_cli.invoke(["first_command", "abcd", "123"]) - self.assertEqual(new_stdout.getvalue(), textwrap.dedent(""" + self.assertEqual( + new_stdout.getvalue(), + textwrap.dedent(""" { "positional": { "1": "abcd", @@ -70,13 +88,25 @@ def test_defaults(self) -> None: "2": false } } - """).lstrip()) + """).lstrip(), + ) def test_args(self) -> None: new_stdout = StringIO() with redirect_stdout(new_stdout): - test_cli.invoke(["first_command", "abcd", "123", "--option-1", "new_value", "--option-2"]) - self.assertEqual(new_stdout.getvalue(), textwrap.dedent(""" + test_cli.invoke( + [ + "first_command", + "abcd", + "123", + "--option-1", + "new_value", + "--option-2", + ] + ) + self.assertEqual( + new_stdout.getvalue(), + textwrap.dedent(""" { "positional": { "1": "abcd", @@ -87,13 +117,15 @@ def test_args(self) -> None: "2": true } } - """).lstrip()) + """).lstrip(), + ) class HelpTests(unittest.TestCase): """ Tests for the --help CLI option. """ + maxDiff = None def test_version(self) -> None: @@ -108,7 +140,9 @@ def test_generic_help(self) -> None: with redirect_stdout(new_stdout): test_cli.invoke(["--help"]) - self.assertEqual(new_stdout.getvalue(), textwrap.dedent(""" + self.assertEqual( + new_stdout.getvalue(), + textwrap.dedent(""" Usage: python3 -m concoursetools [OPTIONS] @@ -120,14 +154,17 @@ def test_generic_help(self) -> None: first_command Invoke a test command. second_command Invoke a second test command. - """).lstrip()) + """).lstrip(), + ) def test_generic_help_no_arguments(self) -> None: new_stdout = StringIO() with redirect_stdout(new_stdout): test_cli.invoke([]) - self.assertEqual(new_stdout.getvalue(), textwrap.dedent(""" + self.assertEqual( + new_stdout.getvalue(), + textwrap.dedent(""" Usage: python3 -m concoursetools [OPTIONS] @@ -139,7 +176,8 @@ def test_generic_help_no_arguments(self) -> None: first_command Invoke a test command. second_command Invoke a second test command. - """).lstrip()) + """).lstrip(), + ) def first_command_help(self) -> None: with self._mock_terminal_width(120): @@ -150,7 +188,9 @@ def first_command_help(self) -> None: with redirect_stdout(new_stdout): test_cli.invoke(["first_command", "--help"]) - self.assertEqual(new_stdout.getvalue(), textwrap.dedent(""" + self.assertEqual( + new_stdout.getvalue(), + textwrap.dedent(""" Usage: python3 -m concoursetools first_command [OPTIONS] @@ -165,7 +205,8 @@ def first_command_help(self) -> None: -o, --option-1 The first optional argument. --option-2 The second optional argument. - """).lstrip()) + """).lstrip(), + ) def test_narrow_help_string(self) -> None: with self._mock_terminal_width(80): @@ -176,7 +217,9 @@ def test_narrow_help_string(self) -> None: with redirect_stdout(new_stdout): test_cli.invoke(["first_command", "--help"]) - self.assertEqual(new_stdout.getvalue(), textwrap.dedent(""" + self.assertEqual( + new_stdout.getvalue(), + textwrap.dedent(""" Usage: python3 -m concoursetools first_command [OPTIONS] @@ -191,7 +234,8 @@ def test_narrow_help_string(self) -> None: -o, --option-1 The first optional argument. --option-2 The second optional argument. - """).lstrip()) + """).lstrip(), + ) def test_narrower_help_string(self) -> None: with self._mock_terminal_width(40): @@ -202,7 +246,9 @@ def test_narrower_help_string(self) -> None: with redirect_stdout(new_stdout): test_cli.invoke(["first_command", "--help"]) - self.assertEqual(new_stdout.getvalue(), textwrap.dedent(""" + self.assertEqual( + new_stdout.getvalue(), + textwrap.dedent(""" Usage: python3 -m concoursetools \\ first_command \\ @@ -223,13 +269,16 @@ def test_narrower_help_string(self) -> None: --option-2 The second optional argument. - """).lstrip()) + """).lstrip(), + ) @staticmethod @contextmanager def _mock_terminal_width(new_width: int) -> Generator[None, None, None]: _, current_height = shutil.get_terminal_size() - with unittest.mock.patch("shutil.get_terminal_size", lambda: (new_width, current_height)): + with unittest.mock.patch( + "shutil.get_terminal_size", lambda: (new_width, current_height) + ): yield @@ -237,6 +286,7 @@ class AnnotationTests(unittest.TestCase): """ Tests for the _ANNOTATIONS_TO_TYPE mapping. """ + def test_strings(self) -> None: self.assertEqual(_ANNOTATIONS_TO_TYPES["str"], str) self.assertEqual(_ANNOTATIONS_TO_TYPES["bool"], bool) @@ -272,8 +322,8 @@ class DocstringTests(unittest.TestCase): """ Tests for the docstring parser. """ - def test_empty(self) -> None: + def test_empty(self) -> None: def func() -> None: pass @@ -283,7 +333,6 @@ def func() -> None: self.assertDictEqual(docstring.parameters, {}) def test_one_line(self) -> None: - def func() -> None: """A simple function.""" pass @@ -294,7 +343,6 @@ def func() -> None: self.assertDictEqual(docstring.parameters, {}) def test_with_params(self) -> None: - def func() -> None: """ A simple function. @@ -307,13 +355,15 @@ def func() -> None: docstring = Docstring.from_object(func) self.assertEqual(docstring.first_line, "A simple function.") self.assertEqual(docstring.description, "") - self.assertDictEqual(docstring.parameters, { - "param_1": "A simple parameter.", - "param_2": "A more complex parameter with a description that spans multiple lines.", - }) + self.assertDictEqual( + docstring.parameters, + { + "param_1": "A simple parameter.", + "param_2": "A more complex parameter with a description that spans multiple lines.", + }, + ) def test_multiline_with_params(self) -> None: - def func() -> None: """ A simple function. @@ -328,14 +378,19 @@ def func() -> None: docstring = Docstring.from_object(func) self.assertEqual(docstring.first_line, "A simple function.") - self.assertEqual(docstring.description, "This is a more complex description that\nspans multiple lines.") - self.assertDictEqual(docstring.parameters, { - "param_1": "A simple parameter.", - "param_2": "A more complex parameter with a description that spans multiple lines.", - }) + self.assertEqual( + docstring.description, + "This is a more complex description that\nspans multiple lines.", + ) + self.assertDictEqual( + docstring.parameters, + { + "param_1": "A simple parameter.", + "param_2": "A more complex parameter with a description that spans multiple lines.", + }, + ) def test_with_only_params(self) -> None: - def func() -> None: """ :param param_1: A simple parameter. @@ -346,17 +401,22 @@ def func() -> None: docstring = Docstring.from_object(func) self.assertEqual(docstring.first_line, "") self.assertEqual(docstring.description, "") - self.assertDictEqual(docstring.parameters, { - "param_1": "A simple parameter.", - "param_2": "A more complex parameter with a description that spans multiple lines.", - }) + self.assertDictEqual( + docstring.parameters, + { + "param_1": "A simple parameter.", + "param_2": "A more complex parameter with a description that spans multiple lines.", + }, + ) test_cli = CLI() @test_cli.register(allow_short={"option_1"}) -def first_command(arg_1: str, arg_2: int, /, *, option_1: str = "value", option_2: bool = False) -> None: +def first_command( + arg_1: str, arg_2: int, /, *, option_1: str = "value", option_2: bool = False +) -> None: """ Invoke a test command. @@ -370,10 +430,7 @@ def first_command(arg_1: str, arg_2: int, /, *, option_1: str = "value", option_ "1": arg_1, "2": arg_2, }, - "optional": { - "1": option_1, - "2": option_2 - } + "optional": {"1": option_1, "2": option_2}, } print(json.dumps(command_json, indent=2)) diff --git a/tests/test_dockertools.py b/tests/test_dockertools.py index 2c4f31a..38013da 100644 --- a/tests/test_dockertools.py +++ b/tests/test_dockertools.py @@ -2,6 +2,7 @@ """ Tests for the dockertools module. """ + from pathlib import Path import shutil import sys @@ -16,6 +17,7 @@ class DockerTests(TestCase): """ Tests for the creation of the Dockerfile. """ + def setUp(self) -> None: """Code to run before each test.""" self._temp_dir = TemporaryDirectory() @@ -27,7 +29,9 @@ def setUp(self) -> None: self.dockerfile_path = self.temp_dir / "Dockerfile" self.assertFalse(self.dockerfile_path.exists()) - self.current_python_string = f"{sys.version_info.major}.{sys.version_info.minor}" + self.current_python_string = ( + f"{sys.version_info.major}.{sys.version_info.minor}" + ) def tearDown(self) -> None: """Code to run after each test.""" @@ -58,7 +62,12 @@ def test_basic_config(self) -> None: self.assertEqual(dockerfile_contents, expected_contents) def test_basic_config_custom_image_and_tag(self) -> None: - cli_commands.dockerfile(str(self.temp_dir), resource_file="concourse.py", image="node", tag="lts-slim") + cli_commands.dockerfile( + str(self.temp_dir), + resource_file="concourse.py", + image="node", + tag="lts-slim", + ) dockerfile_contents = self.dockerfile_path.read_text() expected_contents = textwrap.dedent(""" FROM node:lts-slim @@ -82,7 +91,11 @@ def test_basic_config_custom_image_and_tag(self) -> None: self.assertEqual(dockerfile_contents, expected_contents) def test_basic_config_pip_args(self) -> None: - cli_commands.dockerfile(str(self.temp_dir), resource_file="concourse.py", pip_args="--trusted-host pypi.org") + cli_commands.dockerfile( + str(self.temp_dir), + resource_file="concourse.py", + pip_args="--trusted-host pypi.org", + ) dockerfile_contents = self.dockerfile_path.read_text() expected_contents = textwrap.dedent(f""" FROM python:{self.current_python_string} @@ -106,7 +119,9 @@ def test_basic_config_pip_args(self) -> None: self.assertEqual(dockerfile_contents, expected_contents) def test_basic_config_no_venv(self) -> None: - cli_commands.dockerfile(str(self.temp_dir), resource_file="concourse.py", no_venv=True) + cli_commands.dockerfile( + str(self.temp_dir), resource_file="concourse.py", no_venv=True + ) dockerfile_contents = self.dockerfile_path.read_text() expected_contents = textwrap.dedent(f""" FROM python:{self.current_python_string} @@ -126,7 +141,9 @@ def test_basic_config_no_venv(self) -> None: self.assertEqual(dockerfile_contents, expected_contents) def test_basic_config_with_suffix(self) -> None: - cli_commands.dockerfile(str(self.temp_dir), resource_file="concourse.py", suffix="slim") + cli_commands.dockerfile( + str(self.temp_dir), resource_file="concourse.py", suffix="slim" + ) dockerfile_contents = self.dockerfile_path.read_text() expected_contents = textwrap.dedent(f""" FROM python:{self.current_python_string}-slim @@ -174,7 +191,9 @@ def test_basic_config_with_different_name(self) -> None: self.assertEqual(dockerfile_contents, expected_contents) def test_basic_config_with_class_name_and_executable(self) -> None: - cli_commands.dockerfile(str(self.temp_dir), class_name="MyResource", executable="/usr/bin/python3") + cli_commands.dockerfile( + str(self.temp_dir), class_name="MyResource", executable="/usr/bin/python3" + ) dockerfile_contents = self.dockerfile_path.read_text() expected_contents = textwrap.dedent(f""" FROM python:{self.current_python_string} @@ -223,7 +242,9 @@ def test_netrc_config(self) -> None: self.assertEqual(dockerfile_contents, expected_contents) def test_rsa_config(self) -> None: - cli_commands.dockerfile(str(self.temp_dir), resource_file="concourse.py", include_rsa=True) + cli_commands.dockerfile( + str(self.temp_dir), resource_file="concourse.py", include_rsa=True + ) dockerfile_contents = self.dockerfile_path.read_text() expected_contents = textwrap.dedent(f""" FROM python:{self.current_python_string} @@ -249,7 +270,9 @@ def test_rsa_config(self) -> None: self.assertEqual(dockerfile_contents, expected_contents) def test_dev_config(self) -> None: - cli_commands.dockerfile(str(self.temp_dir), resource_file="concourse.py", dev=True) + cli_commands.dockerfile( + str(self.temp_dir), resource_file="concourse.py", dev=True + ) dockerfile_contents = self.dockerfile_path.read_text() expected_contents = textwrap.dedent(f""" FROM python:{self.current_python_string} diff --git a/tests/test_examples.py b/tests/test_examples.py index af1bcab..39470db 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -19,9 +19,11 @@ from moto.sagemaker.responses import TYPE_RESPONSE, SageMakerResponse import requests # noqa: F401 except ImportError: - allowed_to_skip = ("CI" not in os.environ) + allowed_to_skip = "CI" not in os.environ if allowed_to_skip: - raise unittest.SkipTest("Cannot proceed without example dependencies - see 'requirements-tests.txt'") + raise unittest.SkipTest( + "Cannot proceed without example dependencies - see 'requirements-tests.txt'" + ) raise from concoursetools.mocking import TestBuildMetadata @@ -39,6 +41,7 @@ class MockedTextResponse: """ Represents a mocked requests.Response object containing a body. """ + def __init__(self, data: str, status_code: int = 200) -> None: self._data = data self._status_code = status_code @@ -61,6 +64,7 @@ class MockedJSONResponse: """ Represents a mocked requests.Response object containing valid JSON within the body. """ + def __init__(self, json_data: Any, status_code: int = 200) -> None: self._json_data = json_data self._status_code = status_code @@ -73,75 +77,83 @@ def json(self) -> Any: class BranchesTests(unittest.TestCase): - def setUp(self) -> None: """Code to run before each test.""" - self.mock_response = MockedJSONResponse([ - { - "name": "issue/95", - "commit": { - "sha": "82b43a4701c7954d8564e3bb601c3ca3344dd395", - "url": "https://api.github.com/repos/concourse/github-release-resource/commits/82b43a4701c7954d8564e3bb601c3ca3344dd395", + self.mock_response = MockedJSONResponse( + [ + { + "name": "issue/95", + "commit": { + "sha": "82b43a4701c7954d8564e3bb601c3ca3344dd395", + "url": "https://api.github.com/repos/concourse/github-release-resource/commits/82b43a4701c7954d8564e3bb601c3ca3344dd395", + }, + "protected": False, }, - "protected": False, - }, - { - "name": "master", - "commit": { - "sha": "daa864f0ae9df1cdd6debc86d722f07205ee5d37", - "url": "https://api.github.com/repos/concourse/github-release-resource/commits/daa864f0ae9df1cdd6debc86d722f07205ee5d37", + { + "name": "master", + "commit": { + "sha": "daa864f0ae9df1cdd6debc86d722f07205ee5d37", + "url": "https://api.github.com/repos/concourse/github-release-resource/commits/daa864f0ae9df1cdd6debc86d722f07205ee5d37", + }, + "protected": False, }, - "protected": False, - }, - { - "name": "release/6.7.x", - "commit": { - "sha": "114374e2c37a20ef8d0465b13cfe1a6e262a6f9b", - "url": "https://api.github.com/repos/concourse/github-release-resource/commits/114374e2c37a20ef8d0465b13cfe1a6e262a6f9b", + { + "name": "release/6.7.x", + "commit": { + "sha": "114374e2c37a20ef8d0465b13cfe1a6e262a6f9b", + "url": "https://api.github.com/repos/concourse/github-release-resource/commits/114374e2c37a20ef8d0465b13cfe1a6e262a6f9b", + }, + "protected": False, }, - "protected": False, - }, - { - "name": "version", - "commit": { - "sha": "2161540c7c8334eff873ce81fe085511a7225bdf", - "url": "https://api.github.com/repos/concourse/github-release-resource/commits/2161540c7c8334eff873ce81fe085511a7225bdf", + { + "name": "version", + "commit": { + "sha": "2161540c7c8334eff873ce81fe085511a7225bdf", + "url": "https://api.github.com/repos/concourse/github-release-resource/commits/2161540c7c8334eff873ce81fe085511a7225bdf", + }, + "protected": False, }, - "protected": False, - }, - { - "name": "version-lts", - "commit": { - "sha": "4f0de5025befd20fed73758b16ea2d2d6b19c82e", - "url": "https://api.github.com/repos/concourse/github-release-resource/commits/4f0de5025befd20fed73758b16ea2d2d6b19c82e", + { + "name": "version-lts", + "commit": { + "sha": "4f0de5025befd20fed73758b16ea2d2d6b19c82e", + "url": "https://api.github.com/repos/concourse/github-release-resource/commits/4f0de5025befd20fed73758b16ea2d2d6b19c82e", + }, + "protected": False, }, - "protected": False, - }, - ]) + ] + ) def test_subversions(self) -> None: resource = BranchResource("concourse", "github-release-resource") with unittest.mock.patch("requests.get", self.mock_response.get): - branches = {version.name for version in resource.fetch_latest_sub_versions()} - self.assertSetEqual(branches, {"issue/95", "master", "release/6.7.x", "version", "version-lts"}) + branches = { + version.name for version in resource.fetch_latest_sub_versions() + } + self.assertSetEqual( + branches, {"issue/95", "master", "release/6.7.x", "version", "version-lts"} + ) def test_subversions_with_regex(self) -> None: resource = BranchResource("concourse", "github-release-resource", "release/.*") with unittest.mock.patch("requests.get", self.mock_response.get): - branches = {version.name for version in resource.fetch_latest_sub_versions()} + branches = { + version.name for version in resource.fetch_latest_sub_versions() + } self.assertSetEqual(branches, {"release/6.7.x"}) def test_subversions_with_regex_whole_match(self) -> None: """Test that partial matches are not sufficient.""" resource = BranchResource("concourse", "github-release-resource", "lts") with unittest.mock.patch("requests.get", self.mock_response.get): - branches = {version.name for version in resource.fetch_latest_sub_versions()} + branches = { + version.name for version in resource.fetch_latest_sub_versions() + } self.assertSetEqual(branches, set()) @mock_aws class S3Tests(unittest.TestCase): - def setUp(self) -> None: """Code to run before each test.""" super().setUp() @@ -160,7 +172,12 @@ def test_download(self) -> None: self.assertFalse(url_path.exists()) expires_in: dict[str, float] = {"minutes": 30, "seconds": 15} - self.resource.download_data(self.destination_dir, self.build_metadata, "folder/file.txt", expires_in=expires_in) + self.resource.download_data( + self.destination_dir, + self.build_metadata, + "folder/file.txt", + expires_in=expires_in, + ) self.assertTrue(url_path.exists()) raw_url = url_path.read_text() @@ -177,8 +194,13 @@ def test_download_with_different_location(self) -> None: self.assertFalse(url_path.exists()) expires_in: dict[str, float] = {"minutes": 30, "seconds": 15} - self.resource.download_data(self.destination_dir, self.build_metadata, "folder/file.txt", expires_in=expires_in, - url_file="url-new") + self.resource.download_data( + self.destination_dir, + self.build_metadata, + "folder/file.txt", + expires_in=expires_in, + url_file="url-new", + ) self.assertTrue(url_path.exists()) @@ -187,8 +209,13 @@ def test_download_with_file_name(self) -> None: self.assertFalse(url_path.exists()) expires_in: dict[str, float] = {"minutes": 30, "seconds": 15} - self.resource.download_data(self.destination_dir, self.build_metadata, "folder/file.txt", expires_in=expires_in, - file_name="file.txt") + self.resource.download_data( + self.destination_dir, + self.build_metadata, + "folder/file.txt", + expires_in=expires_in, + file_name="file.txt", + ) self.assertTrue(url_path.exists()) raw_url = url_path.read_text() @@ -198,11 +225,13 @@ def test_download_with_file_name(self) -> None: self.assertEqual(url.hostname, "my-bucket.s3.amazonaws.com") self.assertEqual(url.path, "/folder/file.txt") self.assertEqual(parsed_query["X-Amz-Expires"], str(30 * 60 + 15)) - self.assertEqual(parsed_query["response-content-disposition"], "attachment; filename=\"file.txt\"") + self.assertEqual( + parsed_query["response-content-disposition"], + 'attachment; filename="file.txt"', + ) class XKCDCheckTests(unittest.TestCase): - def setUp(self) -> None: """Code to run before each test.""" self.resource = XKCDResource() @@ -216,7 +245,9 @@ def tearDown(self) -> None: def test_fetch_all_versions(self) -> None: versions = self.resource.fetch_all_versions() - self.assertSetEqual(versions, {ComicVersion(comic_id) for comic_id in {2809, 2810, 2811, 2812}}) + self.assertSetEqual( + versions, {ComicVersion(comic_id) for comic_id in {2809, 2810, 2811, 2812}} + ) def test_fetch_new_versions_no_previous(self) -> None: versions = self.resource.fetch_new_versions() @@ -228,7 +259,6 @@ def test_fetch_new_versions_with_previous(self) -> None: class XKCDInTests(unittest.TestCase): - def setUp(self) -> None: """Code to run before each test.""" self.version = ComicVersion(2812) @@ -258,14 +288,21 @@ def test_file_downloads(self) -> None: "URL": "https://xkcd.com/2812/", } self.assertDictEqual(metadata, expected_metadata) - self.assertEqual(folder_state["alt.txt"], "Getting the utility people to run transmission lines to Earth is " - "expensive, but it will pay for itself in no time.") + self.assertEqual( + folder_state["alt.txt"], + "Getting the utility people to run transmission lines to Earth is " + "expensive, but it will pay for itself in no time.", + ) self.assertEqual(folder_state["link.txt"], "https://xkcd.com/2812/") - self.assertDictEqual(self.expected_response, json.loads(folder_state["info.json"])) + self.assertDictEqual( + self.expected_response, json.loads(folder_state["info.json"]) + ) image_contents = folder_state["image.png"] hashed_image_contents = hashlib.sha1(image_contents).hexdigest() - self.assertEqual(hashed_image_contents, "22f545ac6b50163ce39bac49094c3f64e0858403") + self.assertEqual( + hashed_image_contents, "22f545ac6b50163ce39bac49094c3f64e0858403" + ) def test_file_downloads_without_files(self) -> None: with self.wrapper.capture_directory_state() as directory_state: @@ -278,10 +315,11 @@ def test_file_downloads_without_files(self) -> None: @mock_aws class SecretsCheckTests(unittest.TestCase): - def setUp(self) -> None: """Code to run before each test.""" - self.resource = SecretsResource("arn:aws:secretsmanager:eu-west-1::secret:my-secret") + self.resource = SecretsResource( + "arn:aws:secretsmanager:eu-west-1::secret:my-secret" + ) client = self.resource._client self.string_secret = client.create_secret( @@ -301,8 +339,9 @@ def test_fetch_secret_no_version(self) -> None: self.assertListEqual(versions, [self.initial_version]) def test_fetch_secret_old_version(self) -> None: - response = self.resource._client.put_secret_value(SecretId=self.resource.secret, - SecretString=json.dumps({"value": "xyz"})) + response = self.resource._client.put_secret_value( + SecretId=self.resource.secret, SecretString=json.dumps({"value": "xyz"}) + ) new_version = SecretVersion(response["VersionId"]) versions = self.resource.fetch_new_versions(self.initial_version) self.assertListEqual(versions, [self.initial_version, new_version]) @@ -312,17 +351,20 @@ def test_fetch_secret_same_version(self) -> None: self.assertListEqual(versions, [self.initial_version]) def test_missing_secret(self) -> None: - resource = SecretsResource("arn:aws:secretsmanager:eu-west-1::secret:missing") + resource = SecretsResource( + "arn:aws:secretsmanager:eu-west-1::secret:missing" + ) with self.assertRaises(Exception): resource.fetch_new_versions() @mock_aws class SecretsInTests(unittest.TestCase): - def setUp(self) -> None: """Code to run before each test.""" - self.resource = SecretsResource("arn:aws:secretsmanager:eu-west-1::secret:my-secret") + self.resource = SecretsResource( + "arn:aws:secretsmanager:eu-west-1::secret:my-secret" + ) client = self.resource._client self.string_secret = client.create_secret( @@ -358,26 +400,37 @@ def test_getting_metadata_only(self) -> None: wrapper = SimpleTestResourceWrapper(self.resource) with wrapper.capture_directory_state() as directory_state: wrapper.download_version(self.version) - self.assertDictEqual(directory_state.final_state, { - "metadata.json": json.dumps(self.expected_metadata, cls=DatetimeSafeJSONEncoder), - }) + self.assertDictEqual( + directory_state.final_state, + { + "metadata.json": json.dumps( + self.expected_metadata, cls=DatetimeSafeJSONEncoder + ), + }, + ) def test_getting_metadata_and_value(self) -> None: wrapper = SimpleTestResourceWrapper(self.resource) with wrapper.capture_directory_state() as directory_state: wrapper.download_version(self.version, value=True) - self.assertDictEqual(directory_state.final_state, { - "metadata.json": json.dumps(self.expected_metadata, cls=DatetimeSafeJSONEncoder), - "value": json.dumps({"value": "abc"}), - }) + self.assertDictEqual( + directory_state.final_state, + { + "metadata.json": json.dumps( + self.expected_metadata, cls=DatetimeSafeJSONEncoder + ), + "value": json.dumps({"value": "abc"}), + }, + ) @mock_aws class SecretsOutTests(unittest.TestCase): - def setUp(self) -> None: """Code to run before each test.""" - self.resource = SecretsResource("arn:aws:secretsmanager:eu-west-1::secret:my-secret") + self.resource = SecretsResource( + "arn:aws:secretsmanager:eu-west-1::secret:my-secret" + ) client = self.resource._client self.string_secret = client.create_secret( @@ -403,7 +456,9 @@ def test_updating_with_string(self) -> None: def test_updating_with_json(self) -> None: wrapper = SimpleTestResourceWrapper(self.resource) - new_version, metadata = wrapper.publish_new_version(json={"new-key": "new-value"}) + new_version, metadata = wrapper.publish_new_version( + json={"new-key": "new-value"} + ) latest_version = self.resource.fetch_latest_version() self.assertEqual(new_version, latest_version) self.assertNotEqual(new_version, self.version) @@ -425,7 +480,6 @@ def test_updating_with_file(self) -> None: @mock_aws class PipelineCheckTests(unittest.TestCase): - def setUp(self) -> None: """Code to run before each test.""" pipeline_name = "my-pipeline" @@ -437,19 +491,23 @@ def setUp(self) -> None: "Version": "2020-12-01", "Metadata": {}, "Parameters": [], - "Steps": [{ - "Name": "MyCondition", - "Type": "Condition", - "Arguments": { - "Conditions": [{ - "Type": "LessThanOrEqualTo", - "LeftValue": 3.0, - "RightValue": 6.0, - }], - "IfSteps": [], - "ElseSteps": [] - }, - }], + "Steps": [ + { + "Name": "MyCondition", + "Type": "Condition", + "Arguments": { + "Conditions": [ + { + "Type": "LessThanOrEqualTo", + "LeftValue": 3.0, + "RightValue": 6.0, + } + ], + "IfSteps": [], + "ElseSteps": [], + }, + } + ], } self.pipeline = client.create_pipeline( @@ -458,37 +516,54 @@ def setUp(self) -> None: PipelineDefinition=json.dumps(pipeline_definition), ) - responses = (client.start_pipeline_execution(PipelineName=pipeline_name) for _ in range(5)) + responses = ( + client.start_pipeline_execution(PipelineName=pipeline_name) + for _ in range(5) + ) execution_arns = [response["PipelineExecutionArn"] for response in responses] - self.execution_arns = execution_arns[::-1] # reverse these to force moto to yield in descending order + self.execution_arns = execution_arns[ + ::-1 + ] # reverse these to force moto to yield in descending order def test_fetch_secret_no_version(self) -> None: - version, = self.resource.fetch_new_versions() + (version,) = self.resource.fetch_new_versions() expected_version = ExecutionVersion(self.execution_arns[-1]) self.assertEqual(version, expected_version) def test_fetch_secret_no_available_versions(self) -> None: - with unittest.mock.patch("moto.sagemaker.responses.SageMakerResponse.list_pipeline_executions", - _new_response_list_pipeline_executions_empty): + with unittest.mock.patch( + "moto.sagemaker.responses.SageMakerResponse.list_pipeline_executions", + _new_response_list_pipeline_executions_empty, + ): versions = self.resource.fetch_new_versions() self.assertListEqual(versions, []) def test_fetch_secret_no_version_with_pending(self) -> None: - fake_pipeline_executions: list[FakePipelineExecution] = FakePipelineExecution.instances - execution_mapping = {execution.pipeline_execution_arn: execution for execution in fake_pipeline_executions} + fake_pipeline_executions: list[FakePipelineExecution] = ( + FakePipelineExecution.instances + ) + execution_mapping = { + execution.pipeline_execution_arn: execution + for execution in fake_pipeline_executions + } for execution_arn in self.execution_arns[3:]: execution = execution_mapping[execution_arn] execution.pipeline_execution_status = "Executing" - version, = self.resource.fetch_new_versions() + (version,) = self.resource.fetch_new_versions() expected_version = ExecutionVersion(self.execution_arns[2]) self.assertEqual(version, expected_version) def test_fetch_secret_no_version_with_pending_and_all_statuses(self) -> None: - fake_pipeline_executions: list[FakePipelineExecution] = FakePipelineExecution.instances - execution_mapping = {execution.pipeline_execution_arn: execution for execution in fake_pipeline_executions} + fake_pipeline_executions: list[FakePipelineExecution] = ( + FakePipelineExecution.instances + ) + execution_mapping = { + execution.pipeline_execution_arn: execution + for execution in fake_pipeline_executions + } for execution_arn in self.execution_arns[3:]: execution = execution_mapping[execution_arn] @@ -496,13 +571,13 @@ def test_fetch_secret_no_version_with_pending_and_all_statuses(self) -> None: self.resource.statuses.append("Executing") - version, = self.resource.fetch_new_versions() + (version,) = self.resource.fetch_new_versions() expected_version = ExecutionVersion(self.execution_arns[-1]) self.assertEqual(version, expected_version) def test_fetch_secret_latest_version(self) -> None: - previous_version, = self.resource.fetch_new_versions() - version, = self.resource.fetch_new_versions(previous_version) + (previous_version,) = self.resource.fetch_new_versions() + (version,) = self.resource.fetch_new_versions(previous_version) expected_version = ExecutionVersion(self.execution_arns[-1]) self.assertEqual(version, expected_version) @@ -513,37 +588,44 @@ def test_fetch_secret_old_version(self) -> None: self.assertListEqual(versions, expected_versions) def test_missing_pipeline(self) -> None: - resource = PipelineResource("arn:aws:sagemaker:eu-west-1::pipeline:missing") + resource = PipelineResource( + "arn:aws:sagemaker:eu-west-1::pipeline:missing" + ) with self.assertRaises(Exception): resource.fetch_new_versions() @mock_aws class PipelineInTests(unittest.TestCase): - def setUp(self) -> None: """Code to run before each test.""" pipeline_name = "my-pipeline" - resource = PipelineResource("arn:aws:sagemaker:eu-west-1::pipeline:my-pipeline") + resource = PipelineResource( + "arn:aws:sagemaker:eu-west-1::pipeline:my-pipeline" + ) client = resource._client self.pipeline_definition = { "Version": "2020-12-01", "Metadata": {}, "Parameters": [], - "Steps": [{ - "Name": "MyCondition", - "Type": "Condition", - "Arguments": { - "Conditions": [{ - "Type": "LessThanOrEqualTo", - "LeftValue": 3.0, - "RightValue": 6.0, - }], - "IfSteps": [], - "ElseSteps": [] - }, - }], + "Steps": [ + { + "Name": "MyCondition", + "Type": "Condition", + "Arguments": { + "Conditions": [ + { + "Type": "LessThanOrEqualTo", + "LeftValue": 3.0, + "RightValue": 6.0, + } + ], + "IfSteps": [], + "ElseSteps": [], + }, + } + ], } self.pipeline = client.create_pipeline( @@ -555,15 +637,20 @@ def setUp(self) -> None: response = client.start_pipeline_execution(PipelineName=pipeline_name) minimum_execution_arn = response["PipelineExecutionArn"] - response = client.start_pipeline_execution(PipelineName=pipeline_name, PipelineExecutionDisplayName="My Pipeline", - PipelineExecutionDescription="My important pipeline") + response = client.start_pipeline_execution( + PipelineName=pipeline_name, + PipelineExecutionDisplayName="My Pipeline", + PipelineExecutionDescription="My important pipeline", + ) maximum_execution_arn = response["PipelineExecutionArn"] self.version_minimum = ExecutionVersion(minimum_execution_arn) self.version_maximum = ExecutionVersion(maximum_execution_arn) def test_download_minimum_info(self) -> None: - resource = PipelineResource(pipeline="arn:aws:sagemaker:eu-west-1::pipeline:my-pipeline") + resource = PipelineResource( + pipeline="arn:aws:sagemaker:eu-west-1::pipeline:my-pipeline" + ) wrapper = SimpleTestResourceWrapper(resource) with wrapper.capture_directory_state() as directory_state: _, metadata = wrapper.download_version(self.version_minimum) @@ -581,7 +668,9 @@ def test_download_minimum_info(self) -> None: self.assertDictEqual(pipeline_config, self.pipeline_definition) def test_download_maximum_info(self) -> None: - resource = PipelineResource(pipeline="arn:aws:sagemaker:eu-west-1::pipeline:my-pipeline") + resource = PipelineResource( + pipeline="arn:aws:sagemaker:eu-west-1::pipeline:my-pipeline" + ) wrapper = SimpleTestResourceWrapper(resource) _, metadata = wrapper.download_version(self.version_maximum) @@ -595,13 +684,20 @@ def test_download_maximum_info(self) -> None: self.assertDictEqual(metadata, expected_metadata) def test_download_failed_execution(self) -> None: - fake_pipeline_executions: list[FakePipelineExecution] = FakePipelineExecution.instances - execution_mapping = {execution.pipeline_execution_arn: execution for execution in fake_pipeline_executions} + fake_pipeline_executions: list[FakePipelineExecution] = ( + FakePipelineExecution.instances + ) + execution_mapping = { + execution.pipeline_execution_arn: execution + for execution in fake_pipeline_executions + } fake_execution = execution_mapping[self.version_minimum.execution_arn] fake_execution.pipeline_execution_status = "Failed" - resource = PipelineResource(pipeline="arn:aws:sagemaker:eu-west-1::pipeline:my-pipeline") + resource = PipelineResource( + pipeline="arn:aws:sagemaker:eu-west-1::pipeline:my-pipeline" + ) wrapper = SimpleTestResourceWrapper(resource) _, metadata = wrapper.download_version(self.version_minimum) @@ -614,7 +710,9 @@ def test_download_failed_execution(self) -> None: self.assertDictEqual(metadata, expected_metadata) def test_download_no_pipeline(self) -> None: - resource = PipelineResource(pipeline="arn:aws:sagemaker:eu-west-1::pipeline:my-pipeline") + resource = PipelineResource( + pipeline="arn:aws:sagemaker:eu-west-1::pipeline:my-pipeline" + ) wrapper = SimpleTestResourceWrapper(resource) with wrapper.capture_directory_state() as directory_state: wrapper.download_version(self.version_minimum, download_pipeline=False) @@ -626,30 +724,35 @@ def test_download_no_pipeline(self) -> None: @mock_aws class PipelineOutTests(unittest.TestCase): - def setUp(self) -> None: """Code to run before each test.""" pipeline_name = "my-pipeline" - self.resource = PipelineResource("arn:aws:sagemaker:eu-west-1::pipeline:my-pipeline") + self.resource = PipelineResource( + "arn:aws:sagemaker:eu-west-1::pipeline:my-pipeline" + ) client = self.resource._client self.pipeline_definition = { "Version": "2020-12-01", "Metadata": {}, "Parameters": [], - "Steps": [{ - "Name": "MyCondition", - "Type": "Condition", - "Arguments": { - "Conditions": [{ - "Type": "LessThanOrEqualTo", - "LeftValue": 3.0, - "RightValue": 6.0, - }], - "IfSteps": [], - "ElseSteps": [] - }, - }], + "Steps": [ + { + "Name": "MyCondition", + "Type": "Condition", + "Arguments": { + "Conditions": [ + { + "Type": "LessThanOrEqualTo", + "LeftValue": 3.0, + "RightValue": 6.0, + } + ], + "IfSteps": [], + "ElseSteps": [], + }, + } + ], } self.pipeline = client.create_pipeline( @@ -659,19 +762,31 @@ def setUp(self) -> None: ) def test_execution_creation(self) -> None: - fake_pipeline_executions: list[FakePipelineExecution] = FakePipelineExecution.instances - initial_execution_mapping = {execution.pipeline_execution_arn: execution for execution in fake_pipeline_executions} + fake_pipeline_executions: list[FakePipelineExecution] = ( + FakePipelineExecution.instances + ) + initial_execution_mapping = { + execution.pipeline_execution_arn: execution + for execution in fake_pipeline_executions + } - resource = PipelineResource(pipeline="arn:aws:sagemaker:eu-west-1::pipeline:my-pipeline") + resource = PipelineResource( + pipeline="arn:aws:sagemaker:eu-west-1::pipeline:my-pipeline" + ) wrapper = SimpleTestResourceWrapper(resource) new_version, _ = wrapper.publish_new_version() - execution_mapping = {execution.pipeline_execution_arn: execution for execution in fake_pipeline_executions} + execution_mapping = { + execution.pipeline_execution_arn: execution + for execution in fake_pipeline_executions + } self.assertNotIn(new_version.execution_arn, initial_execution_mapping) self.assertIn(new_version.execution_arn, execution_mapping) -def _new_response_list_pipeline_executions_empty(self: SageMakerResponse) -> TYPE_RESPONSE: +def _new_response_list_pipeline_executions_empty( + self: SageMakerResponse, +) -> TYPE_RESPONSE: response: dict[str, object] = { "PipelineExecutionSummaries": [], } diff --git a/tests/test_importing.py b/tests/test_importing.py index 6ed5705..eab7a84 100644 --- a/tests/test_importing.py +++ b/tests/test_importing.py @@ -2,6 +2,7 @@ """ Tests for the dockertools module. """ + from collections.abc import Generator from contextlib import contextmanager import inspect @@ -14,8 +15,13 @@ from unittest import TestCase from concoursetools import ConcourseResource, additional -from concoursetools.importing import (edit_sys_path, file_path_to_import_path, import_classes_from_module, import_py_file, - import_single_class_from_module) +from concoursetools.importing import ( + edit_sys_path, + file_path_to_import_path, + import_classes_from_module, + import_py_file, + import_single_class_from_module, +) from tests import resource as test_resource @@ -23,6 +29,7 @@ class BasicTests(TestCase): """ Tests for the utility functions. """ + def test_import_path_creation(self) -> None: file_path = Path("path/to/python.py") import_path = file_path_to_import_path(file_path) @@ -147,7 +154,9 @@ def test_edit_sys_path(self) -> None: self.assertNotIn(temp_dir_1, sys.path) self.assertNotIn(temp_dir_2, sys.path) - with edit_sys_path(prepend=[Path(temp_dir_1)], append=[Path(temp_dir_2)]): + with edit_sys_path( + prepend=[Path(temp_dir_1)], append=[Path(temp_dir_2)] + ): self.assertEqual(sys.path[0], temp_dir_1) self.assertEqual(sys.path[-1], temp_dir_2) self.assertListEqual(sys.path[1:-1], original_sys_path) @@ -157,7 +166,9 @@ def test_edit_sys_path(self) -> None: def test_importing_classes(self) -> None: file_path = Path(additional.__file__).relative_to(Path.cwd()) - resource_classes = import_classes_from_module(file_path, parent_class=ConcourseResource) # type: ignore[type-abstract] + resource_classes = import_classes_from_module( + file_path, parent_class=ConcourseResource + ) # type: ignore[type-abstract] expected = { "InOnlyConcourseResource": additional.InOnlyConcourseResource, "OutOnlyConcourseResource": additional.OutOnlyConcourseResource, @@ -177,8 +188,11 @@ def test_importing_class_no_name(self) -> None: def test_importing_class_with_name(self) -> None: file_path = Path(test_resource.__file__).relative_to(Path.cwd()) - resource_class = import_single_class_from_module(file_path, parent_class=ConcourseResource, # type: ignore[type-abstract] - class_name=test_resource.TestResource.__name__) + resource_class = import_single_class_from_module( + file_path, + parent_class=ConcourseResource, # type: ignore[type-abstract] + class_name=test_resource.TestResource.__name__, + ) self.assertClassEqual(resource_class, test_resource.TestResource) def test_importing_class_multiple_options(self) -> None: @@ -189,8 +203,11 @@ def test_importing_class_multiple_options(self) -> None: def test_importing_class_multiple_options_specify_name(self) -> None: file_path = Path(additional.__file__).relative_to(Path.cwd()) parent_class = additional.InOnlyConcourseResource - resource_class = import_single_class_from_module(file_path, parent_class=ConcourseResource, # type: ignore[type-abstract] - class_name=parent_class.__name__) + resource_class = import_single_class_from_module( + file_path, + parent_class=ConcourseResource, # type: ignore[type-abstract] + class_name=parent_class.__name__, + ) self.assertClassEqual(resource_class, parent_class) def assertClassEqual(self, class_1: type[object], class_2: type[object]) -> None: diff --git a/tests/test_metadata.py b/tests/test_metadata.py index dba05ec..b6fc67a 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -12,6 +12,7 @@ class MetadataTests(TestCase): """ Tests for the BuildMetadata class. """ + def test_normal_build(self) -> None: metadata = BuildMetadata( BUILD_ID="12345678", @@ -25,7 +26,10 @@ def test_normal_build(self) -> None: self.assertFalse(metadata.is_one_off_build) self.assertFalse(metadata.is_instanced_pipeline) self.assertDictEqual(metadata.instance_vars(), {}) - self.assertEqual(metadata.build_url(), "https://ci.myconcourse.com/teams/my-team/pipelines/my-pipeline/jobs/my-job/builds/42") + self.assertEqual( + metadata.build_url(), + "https://ci.myconcourse.com/teams/my-team/pipelines/my-pipeline/jobs/my-job/builds/42", + ) def test_normal_build_from_env(self) -> None: env = create_env_vars() @@ -35,7 +39,10 @@ def test_normal_build_from_env(self) -> None: self.assertFalse(metadata.is_one_off_build) self.assertFalse(metadata.is_instanced_pipeline) self.assertDictEqual(metadata.instance_vars(), {}) - self.assertEqual(metadata.build_url(), "https://ci.myconcourse.com/teams/my-team/pipelines/my-pipeline/jobs/my-job/builds/42") + self.assertEqual( + metadata.build_url(), + "https://ci.myconcourse.com/teams/my-team/pipelines/my-pipeline/jobs/my-job/builds/42", + ) def test_instanced_pipeline_build(self) -> None: metadata = BuildMetadata( @@ -45,12 +52,14 @@ def test_instanced_pipeline_build(self) -> None: BUILD_JOB_NAME="my-job", BUILD_NAME="42", BUILD_PIPELINE_NAME="my-pipeline", - BUILD_PIPELINE_INSTANCE_VARS="{\"key1\":\"value1\",\"key2\":\"value2\"}", + BUILD_PIPELINE_INSTANCE_VARS='{"key1":"value1","key2":"value2"}', ) self.assertFalse(metadata.is_one_off_build) self.assertTrue(metadata.is_instanced_pipeline) - self.assertDictEqual(metadata.instance_vars(), {"key1": "value1", "key2": "value2"}) + self.assertDictEqual( + metadata.instance_vars(), {"key1": "value1", "key2": "value2"} + ) url = "https://ci.myconcourse.com/teams/my-team/pipelines/my-pipeline/jobs/my-job/builds/42?vars.key1=%22value1%22&vars.key2=%22value2%22" self.assertEqual(metadata.build_url(), url) @@ -89,8 +98,10 @@ def test_nested_instanced_pipeline_build(self) -> None: self.assertFalse(metadata.is_one_off_build) self.assertTrue(metadata.is_instanced_pipeline) self.assertDictEqual(metadata.instance_vars(), instance_vars) - url = (r"https://ci.myconcourse.com/teams/my-team/pipelines/my-pipeline/jobs/my-job/builds/42" - r"?vars.branch=%22feature-v8%22&vars.version.from=%223.0.0%22&vars.version.main=2&vars.version.to=%222.0.0%22") + url = ( + r"https://ci.myconcourse.com/teams/my-team/pipelines/my-pipeline/jobs/my-job/builds/42" + r"?vars.branch=%22feature-v8%22&vars.version.from=%223.0.0%22&vars.version.main=2&vars.version.to=%222.0.0%22" + ) self.assertEqual(metadata.build_url(), url) def test_one_off_build(self) -> None: @@ -104,7 +115,9 @@ def test_one_off_build(self) -> None: self.assertTrue(metadata.is_one_off_build) self.assertFalse(metadata.is_instanced_pipeline) self.assertDictEqual(metadata.instance_vars(), {}) - self.assertEqual(metadata.build_url(), "https://ci.myconcourse.com/builds/12345678") + self.assertEqual( + metadata.build_url(), "https://ci.myconcourse.com/builds/12345678" + ) def test_one_off_build_from_env(self) -> None: env = create_env_vars(one_off_build=True) @@ -114,16 +127,14 @@ def test_one_off_build_from_env(self) -> None: self.assertTrue(metadata.is_one_off_build) self.assertFalse(metadata.is_instanced_pipeline) self.assertDictEqual(metadata.instance_vars(), {}) - self.assertEqual(metadata.build_url(), "https://ci.myconcourse.com/builds/12345678") + self.assertEqual( + metadata.build_url(), "https://ci.myconcourse.com/builds/12345678" + ) def test_flattening_nested_dict(self) -> None: nested_dict = { "branch": "feature-v8", - "version": { - "from": "3.0.0", - "main": 2, - "to": "2.0.0" - }, + "version": {"from": "3.0.0", "main": 2, "to": "2.0.0"}, } flattened_dict = { "branch": "feature-v8", @@ -157,6 +168,7 @@ class MetadataFormattingTests(TestCase): """ Tests for the BuildMetadata.format_string method. """ + def setUp(self) -> None: """Code to run before each test.""" self.metadata = TestBuildMetadata() @@ -166,12 +178,18 @@ def test_no_interpolation(self) -> None: self.assertEqual(new_string, "This is a normal string.") def test_simple_interpolation(self) -> None: - new_string = self.metadata.format_string("The build id is $BUILD_ID and the job name is $BUILD_JOB_NAME.") - self.assertEqual(new_string, "The build id is 12345678 and the job name is my-job.") + new_string = self.metadata.format_string( + "The build id is $BUILD_ID and the job name is $BUILD_JOB_NAME." + ) + self.assertEqual( + new_string, "The build id is 12345678 and the job name is my-job." + ) def test_interpolation_with_one_off(self) -> None: metadata = TestBuildMetadata(one_off_build=True) - new_string = metadata.format_string("The build id is $BUILD_ID and the job name is $BUILD_JOB_NAME.") + new_string = metadata.format_string( + "The build id is $BUILD_ID and the job name is $BUILD_JOB_NAME." + ) self.assertEqual(new_string, "The build id is 12345678 and the job name is .") def test_interpolation_incorrect_value(self) -> None: @@ -179,10 +197,15 @@ def test_interpolation_incorrect_value(self) -> None: self.metadata.format_string("The build id is $OTHER.") def test_interpolation_incorrect_value_ignore_missing(self) -> None: - new_string = self.metadata.format_string("The build id is $OTHER.", ignore_missing=True) + new_string = self.metadata.format_string( + "The build id is $OTHER.", ignore_missing=True + ) self.assertEqual(new_string, "The build id is $OTHER.") def test_interpolation_with_additional(self) -> None: - new_string = self.metadata.format_string("The build id is $OTHER.", additional_values={"OTHER": "value"}, - ignore_missing=True) + new_string = self.metadata.format_string( + "The build id is $OTHER.", + additional_values={"OTHER": "value"}, + ignore_missing=True, + ) self.assertEqual(new_string, "The build id is value.") diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 84dc635..2f67030 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -4,7 +4,13 @@ from typing import Any, cast from unittest import TestCase -from concoursetools.parsing import format_check_output, format_in_out_output, parse_check_payload, parse_in_payload, parse_out_payload +from concoursetools.parsing import ( + format_check_output, + format_in_out_output, + parse_check_payload, + parse_in_payload, + parse_out_payload, +) from concoursetools.typing import VersionConfig @@ -12,6 +18,7 @@ class CheckParsingTests(TestCase): """ Tests that Concourse JSON is being properly parsed. """ + def test_check_step_with_version(self) -> None: config = textwrap.dedent(""" { @@ -26,12 +33,15 @@ def test_check_step_with_version(self) -> None: """).strip() resource, version = parse_check_payload(config) - self.assertDictEqual(resource, { - "uri": "git://some-uri", - "branch": "develop", - "private_key": "...", - "merges": True, - }) + self.assertDictEqual( + resource, + { + "uri": "git://some-uri", + "branch": "develop", + "private_key": "...", + "merges": True, + }, + ) self.assertIsNotNone(version) version = cast(VersionConfig, version) @@ -49,11 +59,14 @@ def test_check_step_no_version(self) -> None: """).strip() resource, version = parse_check_payload(config) - self.assertDictEqual(resource, { - "uri": "git://some-uri", - "branch": "develop", - "private_key": "...", - }) + self.assertDictEqual( + resource, + { + "uri": "git://some-uri", + "branch": "develop", + "private_key": "...", + }, + ) self.assertIsNone(version) @@ -94,25 +107,32 @@ def test_check_step_broken_version(self) -> None: """).strip() resource, version = parse_check_payload(config) - self.assertDictEqual(resource, { - "uri": "git://some-uri", - "branch": "develop", - "private_key": "...", - }) + self.assertDictEqual( + resource, + { + "uri": "git://some-uri", + "branch": "develop", + "private_key": "...", + }, + ) self.assertIsNotNone(version) version = cast(VersionConfig, version) - self.assertDictEqual(version, { - "ref": "61cbef", - "is_merge": "True", - }) + self.assertDictEqual( + version, + { + "ref": "61cbef", + "is_merge": "True", + }, + ) class InParsingTests(TestCase): """ Tests that Concourse JSON is being properly parsed. """ + def test_in_step_with_params(self) -> None: config = textwrap.dedent(""" { @@ -128,12 +148,15 @@ def test_in_step_with_params(self) -> None: """).strip() resource, version, params = parse_in_payload(config) - self.assertDictEqual(resource, { - "uri": "git://some-uri", - "branch": "develop", - "private_key": "...", - "merges": True, - }) + self.assertDictEqual( + resource, + { + "uri": "git://some-uri", + "branch": "develop", + "private_key": "...", + "merges": True, + }, + ) self.assertDictEqual(version, {"ref": "61cbef"}) self.assertDictEqual(params, {"shallow": True}) @@ -151,11 +174,14 @@ def test_in_step_no_params(self) -> None: """).strip() resource, version, params = parse_in_payload(config) - self.assertDictEqual(resource, { - "uri": "git://some-uri", - "branch": "develop", - "private_key": "...", - }) + self.assertDictEqual( + resource, + { + "uri": "git://some-uri", + "branch": "develop", + "private_key": "...", + }, + ) self.assertDictEqual(version, {"ref": "61cbef"}) self.assertDictEqual(params, {}) @@ -199,6 +225,7 @@ class OutParsingTests(TestCase): """ Tests that Concourse JSON is being properly parsed. """ + def test_out_step_with_params(self) -> None: config = textwrap.dedent(""" { @@ -216,17 +243,23 @@ def test_out_step_with_params(self) -> None: """).strip() resource, params = parse_out_payload(config) - self.assertDictEqual(resource, { - "uri": "git://some-uri", - "branch": "develop", - "private_key": "...", - "merges": True, - }) + self.assertDictEqual( + resource, + { + "uri": "git://some-uri", + "branch": "develop", + "private_key": "...", + "merges": True, + }, + ) - self.assertDictEqual(params, { - "repo": "repo", - "force": False, - }) + self.assertDictEqual( + params, + { + "repo": "repo", + "force": False, + }, + ) def test_out_step_no_params(self) -> None: config = textwrap.dedent(""" @@ -240,11 +273,14 @@ def test_out_step_no_params(self) -> None: """).strip() resource, params = parse_out_payload(config) - self.assertDictEqual(resource, { - "uri": "git://some-uri", - "branch": "develop", - "private_key": "...", - }) + self.assertDictEqual( + resource, + { + "uri": "git://some-uri", + "branch": "develop", + "private_key": "...", + }, + ) self.assertDictEqual(params, {}) @@ -275,20 +311,27 @@ class FormatTests(TestCase): """ Tests for the formatting of strings to pass to Concourse. """ + def test_check_format(self) -> None: versions = [ {"ref": "61cbef"}, {"ref": "d74e01"}, {"ref": "7154fe"}, ] - expected_output = re.sub(r"\s", "", """ + expected_output = re.sub( + r"\s", + "", + """ [ { "ref": "61cbef" }, { "ref": "d74e01" }, { "ref": "7154fe" } ] - """) - self.assertEqual(expected_output, format_check_output(versions, separators=(",", ":"))) + """, + ) + self.assertEqual( + expected_output, format_check_output(versions, separators=(",", ":")) + ) def test_check_format_not_strings(self) -> None: versions: list[dict[str, Any]] = [ @@ -296,14 +339,20 @@ def test_check_format_not_strings(self) -> None: {"ref": True}, {"ref": None}, ] - expected_output = re.sub(r"\s", "", """ + expected_output = re.sub( + r"\s", + "", + """ [ { "ref": "100" }, { "ref": "True" }, { "ref": "None" } ] - """) - self.assertEqual(expected_output, format_check_output(versions, separators=(",", ":"))) + """, + ) + self.assertEqual( + expected_output, format_check_output(versions, separators=(",", ":")) + ) def test_in_out_format(self) -> None: version = {"ref": "61cebf"} @@ -311,7 +360,10 @@ def test_in_out_format(self) -> None: "commit": "61cebf", "author": "HulkHogan", } - expected_output = re.sub(r"\s", "", """ + expected_output = re.sub( + r"\s", + "", + """ { "version": { "ref": "61cebf" }, "metadata": [ @@ -319,8 +371,12 @@ def test_in_out_format(self) -> None: { "name": "author", "value": "HulkHogan" } ] } - """) - self.assertEqual(expected_output, format_in_out_output(version, metadata, separators=(",", ":"))) + """, + ) + self.assertEqual( + expected_output, + format_in_out_output(version, metadata, separators=(",", ":")), + ) def test_in_out_format_not_strings(self) -> None: version: dict[str, Any] = {"ref": True} @@ -328,7 +384,10 @@ def test_in_out_format_not_strings(self) -> None: "commit": 100, "author": "HulkHogan", } - expected_output = re.sub(r"\s", "", """ + expected_output = re.sub( + r"\s", + "", + """ { "version": { "ref": "True" }, "metadata": [ @@ -336,5 +395,9 @@ def test_in_out_format_not_strings(self) -> None: { "name": "author", "value": "HulkHogan" } ] } - """) - self.assertEqual(expected_output, format_in_out_output(version, metadata, separators=(",", ":"))) + """, + ) + self.assertEqual( + expected_output, + format_in_out_output(version, metadata, separators=(",", ":")), + ) diff --git a/tests/test_resource.py b/tests/test_resource.py index 9931c0a..d7b9429 100644 --- a/tests/test_resource.py +++ b/tests/test_resource.py @@ -9,14 +9,25 @@ import concoursetools from concoursetools.cli import commands as cli_commands from concoursetools.colour import colourise -from concoursetools.testing import (ConversionTestResourceWrapper, DockerConversionTestResourceWrapper, DockerTestResourceWrapper, - FileConversionTestResourceWrapper, FileTestResourceWrapper, JSONTestResourceWrapper, SimpleTestResourceWrapper, - run_command) -from tests.resource import ConcourseMockResource, ConcourseMockVersion, TestResource, TestVersion +from concoursetools.testing import ( + ConversionTestResourceWrapper, + DockerConversionTestResourceWrapper, + DockerTestResourceWrapper, + FileConversionTestResourceWrapper, + FileTestResourceWrapper, + JSONTestResourceWrapper, + SimpleTestResourceWrapper, + run_command, +) +from tests.resource import ( + ConcourseMockResource, + ConcourseMockVersion, + TestResource, + TestVersion, +) class SimpleWrapperTests(TestCase): - def setUp(self) -> None: """Code to run before each test.""" self.resource = TestResource("git://some-uri", "develop", "...") @@ -50,7 +61,10 @@ def test_check_step_with_directory_state_capture(self) -> None: def test_check_step_without_version(self) -> None: with self.wrapper.capture_debugging() as debugging: new_versions = self.wrapper.fetch_new_versions() - self.assertListEqual(new_versions, [TestVersion("61cbef"), TestVersion("d74e01"), TestVersion("7154fe")]) + self.assertListEqual( + new_versions, + [TestVersion("61cbef"), TestVersion("d74e01"), TestVersion("7154fe")], + ) self.assertEqual(debugging, "") def test_in_step_no_directory_state(self) -> None: @@ -65,7 +79,10 @@ def test_in_step_no_params(self) -> None: with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state() as directory_state: _, metadata = self.wrapper.download_version(version) - self.assertDictEqual(directory_state.final_state, {"README.txt": "Downloaded README for ref 61cbef.\n"}) + self.assertDictEqual( + directory_state.final_state, + {"README.txt": "Downloaded README for ref 61cbef.\n"}, + ) self.assertDictEqual(metadata, {"team_name": "my-team"}) self.assertEqual(debugging, "Downloading.\n") @@ -73,8 +90,13 @@ def test_in_step_with_params(self) -> None: version = TestVersion("61cbef") with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state() as directory_state: - _, metadata = self.wrapper.download_version(version, file_name="README.md") - self.assertDictEqual(directory_state.final_state, {"README.md": "Downloaded README for ref 61cbef.\n"}) + _, metadata = self.wrapper.download_version( + version, file_name="README.md" + ) + self.assertDictEqual( + directory_state.final_state, + {"README.md": "Downloaded README for ref 61cbef.\n"}, + ) self.assertDictEqual(metadata, {"team_name": "my-team"}) self.assertEqual(debugging, "Downloading.\n") @@ -104,7 +126,6 @@ def test_out_step_missing_params(self) -> None: class JSONWrapperTests(TestCase): - def setUp(self) -> None: """Code to run before each test.""" config = { @@ -142,7 +163,10 @@ def test_check_step_with_directory_state_capture(self) -> None: def test_check_step_without_version(self) -> None: with self.wrapper.capture_debugging() as debugging: new_version_configs = self.wrapper.fetch_new_versions() - self.assertListEqual(new_version_configs, [{"ref": "61cbef"}, {"ref": "d74e01"}, {"ref": "7154fe"}]) + self.assertListEqual( + new_version_configs, + [{"ref": "61cbef"}, {"ref": "d74e01"}, {"ref": "7154fe"}], + ) self.assertEqual(debugging, "") def test_in_step_no_directory_state(self) -> None: @@ -150,7 +174,9 @@ def test_in_step_no_directory_state(self) -> None: with self.wrapper.capture_debugging() as debugging: _, metadata_pairs = self.wrapper.download_version(version_config) - self.assertListEqual(metadata_pairs, [{"name": "team_name", "value": "my-team"}]) + self.assertListEqual( + metadata_pairs, [{"name": "team_name", "value": "my-team"}] + ) self.assertEqual(debugging, "Downloading.\n") def test_in_step_no_params(self) -> None: @@ -158,8 +184,13 @@ def test_in_step_no_params(self) -> None: with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state() as directory_state: _, metadata_pairs = self.wrapper.download_version(version_config) - self.assertDictEqual(directory_state.final_state, {"README.txt": "Downloaded README for ref 61cbef.\n"}) - self.assertListEqual(metadata_pairs, [{"name": "team_name", "value": "my-team"}]) + self.assertDictEqual( + directory_state.final_state, + {"README.txt": "Downloaded README for ref 61cbef.\n"}, + ) + self.assertListEqual( + metadata_pairs, [{"name": "team_name", "value": "my-team"}] + ) self.assertEqual(debugging, "Downloading.\n") def test_in_step_with_params(self) -> None: @@ -167,9 +198,16 @@ def test_in_step_with_params(self) -> None: params = {"file_name": "README.md"} with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state() as directory_state: - _, metadata_pairs = self.wrapper.download_version(version_config, params=params) - self.assertDictEqual(directory_state.final_state, {"README.md": "Downloaded README for ref 61cbef.\n"}) - self.assertListEqual(metadata_pairs, [{"name": "team_name", "value": "my-team"}]) + _, metadata_pairs = self.wrapper.download_version( + version_config, params=params + ) + self.assertDictEqual( + directory_state.final_state, + {"README.md": "Downloaded README for ref 61cbef.\n"}, + ) + self.assertListEqual( + metadata_pairs, [{"name": "team_name", "value": "my-team"}] + ) self.assertEqual(debugging, "Downloading.\n") def test_in_step_with_incorrect_params(self) -> None: @@ -189,7 +227,9 @@ def test_out_step_with_params(self) -> None: with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state(directory): - version_config, metadata_pairs = self.wrapper.publish_new_version(params=params) + version_config, metadata_pairs = self.wrapper.publish_new_version( + params=params + ) self.assertDictEqual(version_config, {"ref": "61cbef"}) self.assertListEqual(metadata_pairs, []) @@ -201,7 +241,6 @@ def test_out_step_missing_params(self) -> None: class ConversionWrapperTests(TestCase): - def setUp(self) -> None: """Code to run before each test.""" config = { @@ -239,7 +278,10 @@ def test_check_step_with_directory_state_capture(self) -> None: def test_check_step_without_version(self) -> None: with self.wrapper.capture_debugging() as debugging: new_versions = self.wrapper.fetch_new_versions() - self.assertListEqual(new_versions, [TestVersion("61cbef"), TestVersion("d74e01"), TestVersion("7154fe")]) + self.assertListEqual( + new_versions, + [TestVersion("61cbef"), TestVersion("d74e01"), TestVersion("7154fe")], + ) self.assertEqual(debugging, "") def test_in_step_no_directory_state(self) -> None: @@ -254,7 +296,10 @@ def test_in_step_no_params(self) -> None: with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state() as directory_state: _, metadata = self.wrapper.download_version(version) - self.assertDictEqual(directory_state.final_state, {"README.txt": "Downloaded README for ref 61cbef.\n"}) + self.assertDictEqual( + directory_state.final_state, + {"README.txt": "Downloaded README for ref 61cbef.\n"}, + ) self.assertDictEqual(metadata, {"team_name": "my-team"}) self.assertEqual(debugging, "Downloading.\n") @@ -262,8 +307,13 @@ def test_in_step_with_params(self) -> None: version = TestVersion("61cbef") with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state() as directory_state: - _, metadata = self.wrapper.download_version(version, file_name="README.md") - self.assertDictEqual(directory_state.final_state, {"README.md": "Downloaded README for ref 61cbef.\n"}) + _, metadata = self.wrapper.download_version( + version, file_name="README.md" + ) + self.assertDictEqual( + directory_state.final_state, + {"README.md": "Downloaded README for ref 61cbef.\n"}, + ) self.assertDictEqual(metadata, {"team_name": "my-team"}) self.assertEqual(debugging, "Downloading.\n") @@ -293,19 +343,24 @@ def test_out_step_missing_params(self) -> None: class FileWrapperTests(TestCase): - def setUp(self) -> None: """Code to run before each test.""" self.temp_dir = TemporaryDirectory() - cli_commands.assets(self.temp_dir.name, resource_file="tests/resource.py", - class_name=TestResource.__name__, executable="/usr/bin/env python3") + cli_commands.assets( + self.temp_dir.name, + resource_file="tests/resource.py", + class_name=TestResource.__name__, + executable="/usr/bin/env python3", + ) config = { "uri": "git://some-uri", "branch": "develop", "private_key": "...", } - self.wrapper = FileTestResourceWrapper.from_assets_dir(config, Path(self.temp_dir.name)) + self.wrapper = FileTestResourceWrapper.from_assets_dir( + config, Path(self.temp_dir.name) + ) def tearDown(self) -> None: """Code to run after each test.""" @@ -339,7 +394,10 @@ def test_check_step_with_directory_state_capture(self) -> None: def test_check_step_without_version(self) -> None: with self.wrapper.capture_debugging() as debugging: new_version_configs = self.wrapper.fetch_new_versions() - self.assertListEqual(new_version_configs, [{"ref": "61cbef"}, {"ref": "d74e01"}, {"ref": "7154fe"}]) + self.assertListEqual( + new_version_configs, + [{"ref": "61cbef"}, {"ref": "d74e01"}, {"ref": "7154fe"}], + ) self.assertEqual(debugging, "") def test_in_step_no_directory_state(self) -> None: @@ -347,7 +405,9 @@ def test_in_step_no_directory_state(self) -> None: with self.wrapper.capture_debugging() as debugging: _, metadata_pairs = self.wrapper.download_version(version_config) - self.assertListEqual(metadata_pairs, [{"name": "team_name", "value": "my-team"}]) + self.assertListEqual( + metadata_pairs, [{"name": "team_name", "value": "my-team"}] + ) self.assertEqual(debugging, "Downloading.\n") def test_in_step_no_params(self) -> None: @@ -355,8 +415,13 @@ def test_in_step_no_params(self) -> None: with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state() as directory_state: _, metadata_pairs = self.wrapper.download_version(version_config) - self.assertDictEqual(directory_state.final_state, {"README.txt": "Downloaded README for ref 61cbef.\n"}) - self.assertListEqual(metadata_pairs, [{"name": "team_name", "value": "my-team"}]) + self.assertDictEqual( + directory_state.final_state, + {"README.txt": "Downloaded README for ref 61cbef.\n"}, + ) + self.assertListEqual( + metadata_pairs, [{"name": "team_name", "value": "my-team"}] + ) self.assertEqual(debugging, "Downloading.\n") def test_in_step_with_params(self) -> None: @@ -364,9 +429,16 @@ def test_in_step_with_params(self) -> None: params = {"file_name": "README.md"} with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state() as directory_state: - _, metadata_pairs = self.wrapper.download_version(version_config, params=params) - self.assertDictEqual(directory_state.final_state, {"README.md": "Downloaded README for ref 61cbef.\n"}) - self.assertListEqual(metadata_pairs, [{"name": "team_name", "value": "my-team"}]) + _, metadata_pairs = self.wrapper.download_version( + version_config, params=params + ) + self.assertDictEqual( + directory_state.final_state, + {"README.md": "Downloaded README for ref 61cbef.\n"}, + ) + self.assertListEqual( + metadata_pairs, [{"name": "team_name", "value": "my-team"}] + ) self.assertEqual(debugging, "Downloading.\n") def test_in_step_with_incorrect_params(self) -> None: @@ -386,7 +458,9 @@ def test_out_step_with_params(self) -> None: with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state(directory): - version_config, metadata_pairs = self.wrapper.publish_new_version(params=params) + version_config, metadata_pairs = self.wrapper.publish_new_version( + params=params + ) self.assertDictEqual(version_config, {"ref": "61cbef"}) self.assertListEqual(metadata_pairs, []) @@ -398,7 +472,6 @@ def test_out_step_missing_params(self) -> None: class FileConversionWrapperTests(TestCase): - def setUp(self) -> None: """Code to run before each test.""" config = { @@ -406,7 +479,9 @@ def setUp(self) -> None: "branch": "develop", "private_key": "...", } - self.wrapper = FileConversionTestResourceWrapper(TestResource, config, executable="/usr/bin/env python3") + self.wrapper = FileConversionTestResourceWrapper( + TestResource, config, executable="/usr/bin/env python3" + ) def test_check_step_with_version_no_debugging(self) -> None: version = TestVersion("61cbef") @@ -436,7 +511,10 @@ def test_check_step_with_directory_state_capture(self) -> None: def test_check_step_without_version(self) -> None: with self.wrapper.capture_debugging() as debugging: new_versions = self.wrapper.fetch_new_versions() - self.assertListEqual(new_versions, [TestVersion("61cbef"), TestVersion("d74e01"), TestVersion("7154fe")]) + self.assertListEqual( + new_versions, + [TestVersion("61cbef"), TestVersion("d74e01"), TestVersion("7154fe")], + ) self.assertEqual(debugging, "") def test_in_step_no_directory_state(self) -> None: @@ -451,7 +529,10 @@ def test_in_step_no_params(self) -> None: with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state() as directory_state: _, metadata = self.wrapper.download_version(version) - self.assertDictEqual(directory_state.final_state, {"README.txt": "Downloaded README for ref 61cbef.\n"}) + self.assertDictEqual( + directory_state.final_state, + {"README.txt": "Downloaded README for ref 61cbef.\n"}, + ) self.assertDictEqual(metadata, {"team_name": "my-team"}) self.assertEqual(debugging, "Downloading.\n") @@ -459,8 +540,13 @@ def test_in_step_with_params(self) -> None: version = TestVersion("61cbef") with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state() as directory_state: - _, metadata = self.wrapper.download_version(version, file_name="README.md") - self.assertDictEqual(directory_state.final_state, {"README.md": "Downloaded README for ref 61cbef.\n"}) + _, metadata = self.wrapper.download_version( + version, file_name="README.md" + ) + self.assertDictEqual( + directory_state.final_state, + {"README.md": "Downloaded README for ref 61cbef.\n"}, + ) self.assertDictEqual(metadata, {"team_name": "my-team"}) self.assertEqual(debugging, "Downloading.\n") @@ -538,7 +624,10 @@ def test_check_step_with_directory_state_capture(self) -> None: def test_check_step_without_version(self) -> None: with self.wrapper.capture_debugging() as debugging: new_version_configs = self.wrapper.fetch_new_versions() - self.assertListEqual(new_version_configs, [{"ref": "61cbef"}, {"ref": "d74e01"}, {"ref": "7154fe"}]) + self.assertListEqual( + new_version_configs, + [{"ref": "61cbef"}, {"ref": "d74e01"}, {"ref": "7154fe"}], + ) self.assertEqual(debugging, "") def test_in_step_no_directory_state(self) -> None: @@ -546,7 +635,9 @@ def test_in_step_no_directory_state(self) -> None: with self.wrapper.capture_debugging() as debugging: _, metadata_pairs = self.wrapper.download_version(version_config) - self.assertListEqual(metadata_pairs, [{"name": "team_name", "value": "my-team"}]) + self.assertListEqual( + metadata_pairs, [{"name": "team_name", "value": "my-team"}] + ) self.assertEqual(debugging, "Downloading.\n") def test_in_step_no_params(self) -> None: @@ -554,8 +645,13 @@ def test_in_step_no_params(self) -> None: with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state() as directory_state: _, metadata_pairs = self.wrapper.download_version(version_config) - self.assertDictEqual(directory_state.final_state, {"README.txt": "Downloaded README for ref 61cbef.\n"}) - self.assertListEqual(metadata_pairs, [{"name": "team_name", "value": "my-team"}]) + self.assertDictEqual( + directory_state.final_state, + {"README.txt": "Downloaded README for ref 61cbef.\n"}, + ) + self.assertListEqual( + metadata_pairs, [{"name": "team_name", "value": "my-team"}] + ) self.assertEqual(debugging, "Downloading.\n") def test_in_step_with_params(self) -> None: @@ -563,9 +659,16 @@ def test_in_step_with_params(self) -> None: params = {"file_name": "README.md"} with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state() as directory_state: - _, metadata_pairs = self.wrapper.download_version(version_config, params=params) - self.assertDictEqual(directory_state.final_state, {"README.md": "Downloaded README for ref 61cbef.\n"}) - self.assertListEqual(metadata_pairs, [{"name": "team_name", "value": "my-team"}]) + _, metadata_pairs = self.wrapper.download_version( + version_config, params=params + ) + self.assertDictEqual( + directory_state.final_state, + {"README.md": "Downloaded README for ref 61cbef.\n"}, + ) + self.assertListEqual( + metadata_pairs, [{"name": "team_name", "value": "my-team"}] + ) self.assertEqual(debugging, "Downloading.\n") def test_in_step_with_incorrect_params(self) -> None: @@ -585,7 +688,9 @@ def test_out_step_with_params(self) -> None: with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state(directory): - version_config, metadata_pairs = self.wrapper.publish_new_version(params=params) + version_config, metadata_pairs = self.wrapper.publish_new_version( + params=params + ) self.assertDictEqual(version_config, {"ref": "61cbef"}) self.assertListEqual(metadata_pairs, []) @@ -621,7 +726,9 @@ def setUp(self) -> None: "branch": "develop", "private_key": "...", } - self.wrapper = DockerConversionTestResourceWrapper(TestResource, config, self.image) + self.wrapper = DockerConversionTestResourceWrapper( + TestResource, config, self.image + ) def test_check_step_with_version_no_debugging(self) -> None: version = TestVersion("61cbef") @@ -651,7 +758,10 @@ def test_check_step_with_directory_state_capture(self) -> None: def test_check_step_without_version(self) -> None: with self.wrapper.capture_debugging() as debugging: new_versions = self.wrapper.fetch_new_versions() - self.assertListEqual(new_versions, [TestVersion("61cbef"), TestVersion("d74e01"), TestVersion("7154fe")]) + self.assertListEqual( + new_versions, + [TestVersion("61cbef"), TestVersion("d74e01"), TestVersion("7154fe")], + ) self.assertEqual(debugging, "") def test_in_step_no_directory_state(self) -> None: @@ -666,7 +776,10 @@ def test_in_step_no_params(self) -> None: with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state() as directory_state: _, metadata = self.wrapper.download_version(version) - self.assertDictEqual(directory_state.final_state, {"README.txt": "Downloaded README for ref 61cbef.\n"}) + self.assertDictEqual( + directory_state.final_state, + {"README.txt": "Downloaded README for ref 61cbef.\n"}, + ) self.assertDictEqual(metadata, {"team_name": "my-team"}) self.assertEqual(debugging, "Downloading.\n") @@ -674,8 +787,13 @@ def test_in_step_with_params(self) -> None: version = TestVersion("61cbef") with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state() as directory_state: - _, metadata = self.wrapper.download_version(version, file_name="README.md") - self.assertDictEqual(directory_state.final_state, {"README.md": "Downloaded README for ref 61cbef.\n"}) + _, metadata = self.wrapper.download_version( + version, file_name="README.md" + ) + self.assertDictEqual( + directory_state.final_state, + {"README.md": "Downloaded README for ref 61cbef.\n"}, + ) self.assertDictEqual(metadata, {"team_name": "my-team"}) self.assertEqual(debugging, "Downloading.\n") @@ -724,11 +842,18 @@ def _build_test_resource_docker_image() -> str: for setup_file in ("setup.py", "setup.cfg", "pyproject.toml"): try: - shutil.copyfile(concoursetools_path.parent / setup_file, temp_repo_path / setup_file) + shutil.copyfile( + concoursetools_path.parent / setup_file, temp_repo_path / setup_file + ) except FileNotFoundError: pass - cli_commands.dockerfile(str(temp_dir), resource_file="concourse.py", class_name=TestResource.__name__, dev=True) + cli_commands.dockerfile( + str(temp_dir), + resource_file="concourse.py", + class_name=TestResource.__name__, + dev=True, + ) stdout, _ = run_command("docker", ["build", ".", "-q"], cwd=temp_dir) sha1_hash = stdout.strip() @@ -755,13 +880,17 @@ def setUp(self) -> None: def test_check_step_with_version_no_debugging(self) -> None: version_config = {"version": "1", "privileged": "true"} new_version_configs = self.wrapper.fetch_new_versions(version_config) - self.assertListEqual(new_version_configs, [{"version": "1", "privileged": "true"}]) + self.assertListEqual( + new_version_configs, [{"version": "1", "privileged": "true"}] + ) def test_check_step_with_version(self) -> None: version_config = {"version": "1", "privileged": "true"} with self.wrapper.capture_debugging() as debugging: new_version_configs = self.wrapper.fetch_new_versions(version_config) - self.assertListEqual(new_version_configs, [{"version": "1", "privileged": "true"}]) + self.assertListEqual( + new_version_configs, [{"version": "1", "privileged": "true"}] + ) self.assertEqual(debugging, self._format_debugging_message("Debug message")) def test_check_step_with_version_twice(self) -> None: @@ -780,7 +909,9 @@ def test_check_step_with_directory_state_capture(self) -> None: def test_check_step_without_version(self) -> None: with self.wrapper.capture_debugging() as debugging: new_version_configs = self.wrapper.fetch_new_versions() - self.assertListEqual(new_version_configs, [{"version": "0", "privileged": "true"}]) + self.assertListEqual( + new_version_configs, [{"version": "0", "privileged": "true"}] + ) self.assertEqual(debugging, self._format_debugging_message("Debug message")) def test_in_step_no_metadata(self) -> None: @@ -789,11 +920,13 @@ def test_in_step_no_metadata(self) -> None: with self.wrapper.capture_debugging() as debugging: _, metadata_pairs = self.wrapper.download_version(version_config) self.assertListEqual(metadata_pairs, []) - expected_debugging = "".join([ - self._format_debugging_message("Debug message"), - self._format_debugging_message("fetching in a privileged container"), - self._format_debugging_message("fetching version: 1"), - ]) + expected_debugging = "".join( + [ + self._format_debugging_message("Debug message"), + self._format_debugging_message("fetching in a privileged container"), + self._format_debugging_message("fetching version: 1"), + ] + ) self.assertEqual(debugging, expected_debugging) def test_in_step_no_directory_state(self) -> None: @@ -801,11 +934,13 @@ def test_in_step_no_directory_state(self) -> None: with self.wrapper.capture_debugging() as debugging: _, metadata_pairs = self.wrapper.download_version(version_config) self.assertListEqual(metadata_pairs, [{"name": "key", "value": "value"}]) - expected_debugging = "".join([ - self._format_debugging_message("Debug message"), - self._format_debugging_message("fetching in a privileged container"), - self._format_debugging_message("fetching version: 1"), - ]) + expected_debugging = "".join( + [ + self._format_debugging_message("Debug message"), + self._format_debugging_message("fetching in a privileged container"), + self._format_debugging_message("fetching version: 1"), + ] + ) self.assertEqual(debugging, expected_debugging) def test_in_step_no_params(self) -> None: @@ -813,13 +948,17 @@ def test_in_step_no_params(self) -> None: with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state() as directory_state: _, metadata_pairs = self.wrapper.download_version(version_config) - self.assertDictEqual(directory_state.final_state, {"privileged": "true\n", "version": "1\n"}) + self.assertDictEqual( + directory_state.final_state, {"privileged": "true\n", "version": "1\n"} + ) self.assertListEqual(metadata_pairs, [{"name": "key", "value": "value"}]) - expected_debugging = "".join([ - self._format_debugging_message("Debug message"), - self._format_debugging_message("fetching in a privileged container"), - self._format_debugging_message("fetching version: 1"), - ]) + expected_debugging = "".join( + [ + self._format_debugging_message("Debug message"), + self._format_debugging_message("fetching in a privileged container"), + self._format_debugging_message("fetching version: 1"), + ] + ) self.assertEqual(debugging, expected_debugging) def test_in_step_with_params(self) -> None: @@ -827,15 +966,21 @@ def test_in_step_with_params(self) -> None: params = {"create_files_via_params": {"file.txt": "contents"}} with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state() as directory_state: - _, metadata_pairs = self.wrapper.download_version(version_config, params=params) - self.assertDictEqual(directory_state.final_state, {"privileged": "true\n", "version": "1\n", - "file.txt": "contents"}) + _, metadata_pairs = self.wrapper.download_version( + version_config, params=params + ) + self.assertDictEqual( + directory_state.final_state, + {"privileged": "true\n", "version": "1\n", "file.txt": "contents"}, + ) self.assertListEqual(metadata_pairs, [{"name": "key", "value": "value"}]) - expected_debugging = "".join([ - self._format_debugging_message("Debug message"), - self._format_debugging_message("fetching in a privileged container"), - self._format_debugging_message("fetching version: 1"), - ]) + expected_debugging = "".join( + [ + self._format_debugging_message("Debug message"), + self._format_debugging_message("fetching in a privileged container"), + self._format_debugging_message("fetching version: 1"), + ] + ) self.assertEqual(debugging, expected_debugging) def test_in_step_with_incorrect_params(self) -> None: @@ -849,15 +994,19 @@ def test_out_step_without_params(self) -> None: with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state(): - version_config, metadata_pairs = self.wrapper.publish_new_version(params=params) + version_config, metadata_pairs = self.wrapper.publish_new_version( + params=params + ) self.assertDictEqual(version_config, {"version": "2", "privileged": "true"}) self.assertListEqual(metadata_pairs, [{"name": "key", "value": "value"}]) - expected_debugging = "".join([ - self._format_debugging_message("Debug message"), - self._format_debugging_message("pushing in a privileged container"), - self._format_debugging_message("pushing version: 2"), - ]) + expected_debugging = "".join( + [ + self._format_debugging_message("Debug message"), + self._format_debugging_message("pushing in a privileged container"), + self._format_debugging_message("pushing version: 2"), + ] + ) self.assertEqual(debugging, expected_debugging) def test_out_step_with_params(self) -> None: @@ -871,15 +1020,19 @@ def test_out_step_with_params(self) -> None: with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state(directory): - version_config, metadata_pairs = self.wrapper.publish_new_version(params=params) + version_config, metadata_pairs = self.wrapper.publish_new_version( + params=params + ) self.assertDictEqual(version_config, {"version": "2", "privileged": "true"}) self.assertListEqual(metadata_pairs, [{"name": "key", "value": "value"}]) - expected_debugging = "".join([ - self._format_debugging_message("Debug message"), - self._format_debugging_message("pushing in a privileged container"), - self._format_debugging_message("pushing version: 2"), - ]) + expected_debugging = "".join( + [ + self._format_debugging_message("Debug message"), + self._format_debugging_message("pushing in a privileged container"), + self._format_debugging_message("pushing version: 2"), + ] + ) self.assertEqual(debugging, expected_debugging) def test_out_step_missing_params(self) -> None: @@ -891,7 +1044,9 @@ def test_out_step_print_env_vars(self) -> None: with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state(): - version_config, metadata_pairs = self.wrapper.publish_new_version(params=params) + version_config, metadata_pairs = self.wrapper.publish_new_version( + params=params + ) self.assertDictEqual(version_config, {"version": "2", "privileged": "true"}) self.assertListEqual(metadata_pairs, [{"name": "key", "value": "value"}]) @@ -899,20 +1054,27 @@ def test_out_step_print_env_vars(self) -> None: debugging_lines = debugging.value.splitlines(keepends=True) original_lines, env_lines = debugging_lines[:3], debugging_lines[3:] - self.assertListEqual(original_lines, [ - self._format_debugging_message("Debug message"), - self._format_debugging_message("pushing in a privileged container"), - self._format_debugging_message("pushing version: 2"), - ]) + self.assertListEqual( + original_lines, + [ + self._format_debugging_message("Debug message"), + self._format_debugging_message("pushing in a privileged container"), + self._format_debugging_message("pushing version: 2"), + ], + ) expected_env_lines = { - self._format_debugging_message("env: PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin "), + self._format_debugging_message( + "env: PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin " + ), self._format_debugging_message("env: HOSTNAME=resource"), self._format_debugging_message("env: HOME=/root"), } for env_var, value in self.wrapper.mocked_environ.items(): - expected_env_lines.add(self._format_debugging_message(f"env: {env_var}={value} ")) + expected_env_lines.add( + self._format_debugging_message(f"env: {env_var}={value} ") + ) self.assertSetEqual(set(env_lines), expected_env_lines) @@ -937,18 +1099,24 @@ def setUp(self) -> None: "log": "Debug message", "metadata": [{"name": "key", "value": "value"}], } - self.wrapper = DockerConversionTestResourceWrapper(ConcourseMockResource, config, self.image) + self.wrapper = DockerConversionTestResourceWrapper( + ConcourseMockResource, config, self.image + ) def test_check_step_with_version_no_debugging(self) -> None: version = ConcourseMockVersion(version=1, privileged=True) new_versions = self.wrapper.fetch_new_versions(version) - self.assertListEqual(new_versions, [ConcourseMockVersion(version=1, privileged=True)]) + self.assertListEqual( + new_versions, [ConcourseMockVersion(version=1, privileged=True)] + ) def test_check_step_with_version(self) -> None: version = ConcourseMockVersion(version=1, privileged=True) with self.wrapper.capture_debugging() as debugging: new_versions = self.wrapper.fetch_new_versions(version) - self.assertListEqual(new_versions, [ConcourseMockVersion(version=1, privileged=True)]) + self.assertListEqual( + new_versions, [ConcourseMockVersion(version=1, privileged=True)] + ) self.assertEqual(debugging, self._format_debugging_message("Debug message")) def test_check_step_with_version_twice(self) -> None: @@ -967,7 +1135,9 @@ def test_check_step_with_directory_state_capture(self) -> None: def test_check_step_without_version(self) -> None: with self.wrapper.capture_debugging() as debugging: new_versions = self.wrapper.fetch_new_versions() - self.assertListEqual(new_versions, [ConcourseMockVersion(version=0, privileged=True)]) + self.assertListEqual( + new_versions, [ConcourseMockVersion(version=0, privileged=True)] + ) self.assertEqual(debugging, self._format_debugging_message("Debug message")) def test_in_step_no_metadata(self) -> None: @@ -976,11 +1146,13 @@ def test_in_step_no_metadata(self) -> None: with self.wrapper.capture_debugging() as debugging: _, metadata = self.wrapper.download_version(version) self.assertDictEqual(metadata, {}) - expected_debugging = "".join([ - self._format_debugging_message("Debug message"), - self._format_debugging_message("fetching in a privileged container"), - self._format_debugging_message("fetching version: 1"), - ]) + expected_debugging = "".join( + [ + self._format_debugging_message("Debug message"), + self._format_debugging_message("fetching in a privileged container"), + self._format_debugging_message("fetching version: 1"), + ] + ) self.assertEqual(debugging, expected_debugging) def test_in_step_no_directory_state(self) -> None: @@ -988,11 +1160,13 @@ def test_in_step_no_directory_state(self) -> None: with self.wrapper.capture_debugging() as debugging: _, metadata = self.wrapper.download_version(version) self.assertDictEqual(metadata, {"key": "value"}) - expected_debugging = "".join([ - self._format_debugging_message("Debug message"), - self._format_debugging_message("fetching in a privileged container"), - self._format_debugging_message("fetching version: 1"), - ]) + expected_debugging = "".join( + [ + self._format_debugging_message("Debug message"), + self._format_debugging_message("fetching in a privileged container"), + self._format_debugging_message("fetching version: 1"), + ] + ) self.assertEqual(debugging, expected_debugging) def test_in_step_no_params(self) -> None: @@ -1000,28 +1174,38 @@ def test_in_step_no_params(self) -> None: with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state() as directory_state: _, metadata = self.wrapper.download_version(version) - self.assertDictEqual(directory_state.final_state, {"privileged": "true\n", "version": "1\n"}) + self.assertDictEqual( + directory_state.final_state, {"privileged": "true\n", "version": "1\n"} + ) self.assertDictEqual(metadata, {"key": "value"}) - expected_debugging = "".join([ - self._format_debugging_message("Debug message"), - self._format_debugging_message("fetching in a privileged container"), - self._format_debugging_message("fetching version: 1"), - ]) + expected_debugging = "".join( + [ + self._format_debugging_message("Debug message"), + self._format_debugging_message("fetching in a privileged container"), + self._format_debugging_message("fetching version: 1"), + ] + ) self.assertEqual(debugging, expected_debugging) def test_in_step_with_params(self) -> None: version = ConcourseMockVersion(version=1, privileged=True) with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state() as directory_state: - _, metadata = self.wrapper.download_version(version, create_files_via_params={"file.txt": "contents"}) - self.assertDictEqual(directory_state.final_state, {"privileged": "true\n", "version": "1\n", - "file.txt": "contents"}) + _, metadata = self.wrapper.download_version( + version, create_files_via_params={"file.txt": "contents"} + ) + self.assertDictEqual( + directory_state.final_state, + {"privileged": "true\n", "version": "1\n", "file.txt": "contents"}, + ) self.assertDictEqual(metadata, {"key": "value"}) - expected_debugging = "".join([ - self._format_debugging_message("Debug message"), - self._format_debugging_message("fetching in a privileged container"), - self._format_debugging_message("fetching version: 1"), - ]) + expected_debugging = "".join( + [ + self._format_debugging_message("Debug message"), + self._format_debugging_message("fetching in a privileged container"), + self._format_debugging_message("fetching version: 1"), + ] + ) self.assertEqual(debugging, expected_debugging) def test_in_step_with_incorrect_params(self) -> None: @@ -1036,11 +1220,13 @@ def test_out_step_without_params(self) -> None: self.assertEqual(version, ConcourseMockVersion(version=2, privileged=True)) self.assertDictEqual(metadata, {"key": "value"}) - expected_debugging = "".join([ - self._format_debugging_message("Debug message"), - self._format_debugging_message("pushing in a privileged container"), - self._format_debugging_message("pushing version: 2"), - ]) + expected_debugging = "".join( + [ + self._format_debugging_message("Debug message"), + self._format_debugging_message("pushing in a privileged container"), + self._format_debugging_message("pushing version: 2"), + ] + ) self.assertEqual(debugging, expected_debugging) def test_out_step_with_params(self) -> None: @@ -1052,15 +1238,19 @@ def test_out_step_with_params(self) -> None: with self.wrapper.capture_debugging() as debugging: with self.wrapper.capture_directory_state(directory): - version, metadata = self.wrapper.publish_new_version(file="folder/version.txt") + version, metadata = self.wrapper.publish_new_version( + file="folder/version.txt" + ) self.assertEqual(version, ConcourseMockVersion(version=2, privileged=True)) self.assertDictEqual(metadata, {"key": "value"}) - expected_debugging = "".join([ - self._format_debugging_message("Debug message"), - self._format_debugging_message("pushing in a privileged container"), - self._format_debugging_message("pushing version: 2"), - ]) + expected_debugging = "".join( + [ + self._format_debugging_message("Debug message"), + self._format_debugging_message("pushing in a privileged container"), + self._format_debugging_message("pushing version: 2"), + ] + ) self.assertEqual(debugging, expected_debugging) def test_out_step_missing_params(self) -> None: diff --git a/tests/test_testing.py b/tests/test_testing.py index 8047005..f004df0 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -37,7 +37,9 @@ def tearDownClass(cls) -> None: cls.temp_dir.cleanup() def test_folder_dict_depth_1(self) -> None: - folder_dict = TemporaryDirectoryState()._get_folder_as_dict(self.root, max_depth=1) + folder_dict = TemporaryDirectoryState()._get_folder_as_dict( + self.root, max_depth=1 + ) expected = { "folder_1": ..., "folder_2": ..., @@ -46,7 +48,9 @@ def test_folder_dict_depth_1(self) -> None: self.assertDictEqual(folder_dict, expected) def test_folder_dict_depth_2(self) -> None: - folder_dict = TemporaryDirectoryState()._get_folder_as_dict(self.root, max_depth=2) + folder_dict = TemporaryDirectoryState()._get_folder_as_dict( + self.root, max_depth=2 + ) expected = { "folder_1": {}, "folder_2": { @@ -58,7 +62,9 @@ def test_folder_dict_depth_2(self) -> None: self.assertDictEqual(folder_dict, expected) def test_folder_dict_depth_3(self) -> None: - folder_dict = TemporaryDirectoryState()._get_folder_as_dict(self.root, max_depth=3) + folder_dict = TemporaryDirectoryState()._get_folder_as_dict( + self.root, max_depth=3 + ) expected = { "folder_1": {}, "folder_2": { @@ -97,5 +103,7 @@ def test_folder_dict_depth_3(self) -> None: "file_1": "Testing 1\n", } TemporaryDirectoryState()._set_folder_from_dict(self.root, original) - final_dict = TemporaryDirectoryState()._get_folder_as_dict(self.root, max_depth=3) + final_dict = TemporaryDirectoryState()._get_folder_as_dict( + self.root, max_depth=3 + ) self.assertDictEqual(final_dict, original) diff --git a/tests/test_version.py b/tests/test_version.py index b0f8d9f..0c079c7 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -11,7 +11,6 @@ class BasicVersion(Version): - def __init__(self, file_path: str) -> None: self.file_path = file_path @@ -20,13 +19,13 @@ class CreationTests(TestCase): """ Tests for the creation of an instance. """ + def test_base_version(self) -> None: with self.assertRaises(TypeError): Version() # type: ignore[abstract] class ComplexVersion(BasicVersion, SortableVersionMixin): - def __eq__(self, other: object) -> bool: if not isinstance(other, type(self)): return NotImplemented @@ -43,14 +42,12 @@ def file_name(self) -> str: class ComparisonTests(TestCase): - def test_repr(self) -> None: version_1 = BasicVersion("file.txt") self.assertEqual(repr(version_1), "BasicVersion(file_path='file.txt')") def test_sortable_mixin_with_version_with_abstract(self) -> None: class MyVersion(Version, SortableVersionMixin): - def __init__(self, file_path: str) -> None: self.file_path = file_path @@ -63,7 +60,6 @@ def __lt__(self, other: object) -> bool: def test_sortable_mixin_with_version_without_abstract(self) -> None: class MyVersion(Version, SortableVersionMixin): - def __init__(self, file_path: str) -> None: self.file_path = file_path @@ -125,7 +121,9 @@ def test_complex_sorting(self) -> None: version_2 = ComplexVersion("folder/file.txt") version_3 = ComplexVersion("image.png") - self.assertListEqual(sorted([version_3, version_1, version_2]), [version_1, version_2, version_3]) + self.assertListEqual( + sorted([version_3, version_1, version_2]), [version_1, version_2, version_3] + ) def test_complex_comparisons(self) -> None: version_1 = ComplexVersion("file.txt") @@ -139,7 +137,6 @@ def test_complex_comparisons(self) -> None: class CommitVersion(Version): - def __init__(self, commit_hash: str, is_merge: bool) -> None: self.commit_hash = commit_hash self.is_merge = is_merge @@ -153,10 +150,9 @@ class TypedCommitVersion(TypedVersion): class CommitVersionImproved(CommitVersion): - @classmethod def from_flat_dict(cls, version_dict: VersionConfig) -> "CommitVersionImproved": - is_merge = (version_dict["is_merge"] == "True") + is_merge = version_dict["is_merge"] == "True" return cls(version_dict["commit_hash"], is_merge) @@ -164,13 +160,11 @@ class DictTests(TestCase): """ Tests for the conversion between version and dict. """ + def test_non_strings(self) -> None: version = CommitVersion("abcdef", True) flat_dict = version.to_flat_dict() - self.assertDictEqual(flat_dict, { - "commit_hash": "abcdef", - "is_merge": "True" - }) + self.assertDictEqual(flat_dict, {"commit_hash": "abcdef", "is_merge": "True"}) new_version = CommitVersion.from_flat_dict(flat_dict) self.assertEqual(new_version.commit_hash, "abcdef") self.assertEqual(new_version.is_merge, "True") @@ -180,19 +174,14 @@ def test_non_strings(self) -> None: self.assertEqual(better_new_version.is_merge, True) def test_private_attribute(self) -> None: - class CommitVersionPrivate(CommitVersion): - def __init__(self, commit_hash: str, is_merge: bool): super().__init__(commit_hash, is_merge) self._force_push = True version = CommitVersionPrivate("abcdef", True) flat_dict = version.to_flat_dict() - self.assertDictEqual(flat_dict, { - "commit_hash": "abcdef", - "is_merge": "True" - }) + self.assertDictEqual(flat_dict, {"commit_hash": "abcdef", "is_merge": "True"}) class MyEnum(Enum): @@ -201,7 +190,6 @@ class MyEnum(Enum): class TypedTests(TestCase): - def test_flattened_and_unflattened_types(self) -> None: expected = { "string": "string", @@ -215,27 +203,34 @@ def test_flattened_and_unflattened_types(self) -> None: for flattened_obj, obj in expected.items(): with self.subTest(obj=obj): self.assertEqual(TypedVersion._flatten_object(obj), flattened_obj) - un_flattened_obj = TypedVersion._un_flatten_object(type(obj), flattened_obj) + un_flattened_obj = TypedVersion._un_flatten_object( + type(obj), flattened_obj + ) self.assertEqual(type(un_flattened_obj), type(obj)) self.assertEqual(un_flattened_obj, obj) def test_flattened_and_unflattened_version(self) -> None: version = TypedCommitVersion("abcdef", datetime(2020, 1, 1, 12, 30), False) flattened = version.to_flat_dict() - self.assertDictEqual(flattened, { - "commit_hash": "abcdef", - "date": "1577881800", - "is_merge": "False", - }) + self.assertDictEqual( + flattened, + { + "commit_hash": "abcdef", + "date": "1577881800", + "is_merge": "False", + }, + ) self.assertEqual(TypedCommitVersion.from_flat_dict(flattened), version) def test_implementing_empty_version(self) -> None: with self.assertRaises(TypeError): + @dataclass class _(TypedVersion): pass def test_missing_dataclass(self) -> None: with self.assertRaises(TypeError): + class _(TypedVersion): pass