diff --git a/dcontainer/__main__.py b/dcontainer/__main__.py index 37029854..48712f56 100644 --- a/dcontainer/__main__.py +++ b/dcontainer/__main__.py @@ -1,7 +1,10 @@ import typer from dcontainer.cli.feature import app as feature_app -from dcontainer.utils.version import resolve_own_package_version, resolve_own_release_version +from dcontainer.utils.version import ( + resolve_own_package_version, + resolve_own_release_version, +) app = typer.Typer(pretty_exceptions_show_locals=False, pretty_exceptions_short=False) app.add_typer(feature_app, name="feature") @@ -18,13 +21,17 @@ def release_version_callback(value: bool) -> None: typer.echo(f"dcontainer release version: {resolve_own_release_version()}") raise typer.Exit() + @app.callback() def version( - version: bool = typer.Option(None, "--version", callback=version_callback, is_eager=True), - release_version: bool = typer.Option(None, "--release-version", callback=release_version_callback, is_eager=True), - + version: bool = typer.Option( + None, "--version", callback=version_callback, is_eager=True + ), + release_version: bool = typer.Option( + None, "--release-version", callback=release_version_callback, is_eager=True + ), ): - return + return def main() -> None: diff --git a/dcontainer/cli/feature.py b/dcontainer/cli/feature.py index ac3496a0..9f240891 100644 --- a/dcontainer/cli/feature.py +++ b/dcontainer/cli/feature.py @@ -66,27 +66,27 @@ def install_command( verbose: bool = False, ) -> None: def _strip_if_wrapped_around(value: str, char: str) -> str: - if len(char) > 1: - raise ValueError("For clarity sake, will only strip one character at a time") - + raise ValueError( + "For clarity sake, will only strip one character at a time" + ) + if len(value) >= 2 and value[0] == char and value[-1] == char: return value.strip(char) return value - if option is None: options = [] else: options = option - + options_dict = {} for single_option in options: single_option = _strip_if_wrapped_around(single_option, '"') option_name = single_option.split("=")[0] - option_value = single_option[len(option_name)+1:] + option_value = single_option[len(option_name) + 1 :] option_value = _strip_if_wrapped_around(option_value, '"') options_dict[option_name] = option_value diff --git a/dcontainer/cli/generate/dir_models/src_dir.py b/dcontainer/cli/generate/dir_models/src_dir.py index 966dcb02..aa81f8cc 100644 --- a/dcontainer/cli/generate/dir_models/src_dir.py +++ b/dcontainer/cli/generate/dir_models/src_dir.py @@ -4,13 +4,9 @@ from dcontainer.cli.generate.file_models.devcontainer_feature_json import ( DevcontainerFeatureJson, ) -from dcontainer.cli.generate.file_models.install_command_sh import ( - InstallCommandSH, -) +from dcontainer.cli.generate.file_models.install_command_sh import InstallCommandSH from dcontainer.cli.generate.file_models.install_sh import InstallSH -from dcontainer.models.devcontainer_feature_definition import ( - FeatureDefinition, -) +from dcontainer.models.devcontainer_feature_definition import FeatureDefinition class SrcDir(Directory): diff --git a/dcontainer/cli/generate/file_models/dependencies_sh.py b/dcontainer/cli/generate/file_models/dependencies_sh.py index 7a327446..88cbc52a 100644 --- a/dcontainer/cli/generate/file_models/dependencies_sh.py +++ b/dcontainer/cli/generate/file_models/dependencies_sh.py @@ -7,8 +7,8 @@ FeatureDependencies, FeatureDependency, ) -from dcontainer.utils.version import resolve_own_release_version from dcontainer.settings import ENV_CLI_LOCATION, ENV_FORCE_CLI_INSTALLATION +from dcontainer.utils.version import resolve_own_release_version RELEASE_VERSION = resolve_own_release_version() @@ -64,7 +64,9 @@ """ -SINGLE_DEPENDENCY = """$dcontainer_location feature install "{feature_oci}" {stringified_envs_args} """ +SINGLE_DEPENDENCY = ( + """$dcontainer_location feature install "{feature_oci}" {stringified_envs_args} """ +) class DependenciesSH(File): @@ -139,11 +141,9 @@ def to_str(self) -> str: ) return HEADER.format( - dependency_installation_lines="\n\n".join( - installation_lines - ), + dependency_installation_lines="\n\n".join(installation_lines), dcontainer_link=DCONTAINER_LINK, checksums_link=CHECKSUM_LINK, force_cli_installation_env=ENV_FORCE_CLI_INSTALLATION, - cli_location_env=ENV_CLI_LOCATION + cli_location_env=ENV_CLI_LOCATION, ) diff --git a/dcontainer/cli/generate/file_models/devcontainer_feature_json.py b/dcontainer/cli/generate/file_models/devcontainer_feature_json.py index 8e567222..f7a6ed25 100644 --- a/dcontainer/cli/generate/file_models/devcontainer_feature_json.py +++ b/dcontainer/cli/generate/file_models/devcontainer_feature_json.py @@ -1,8 +1,6 @@ from easyfs import File -from dcontainer.models.devcontainer_feature_definition import ( - FeatureDefinition, -) +from dcontainer.models.devcontainer_feature_definition import FeatureDefinition class DevcontainerFeatureJson(File): diff --git a/dcontainer/cli/generate/generate_feature.py b/dcontainer/cli/generate/generate_feature.py index 60b5561d..e863c02b 100644 --- a/dcontainer/cli/generate/generate_feature.py +++ b/dcontainer/cli/generate/generate_feature.py @@ -5,9 +5,7 @@ from dcontainer.cli.generate.dir_models.src_dir import SrcDir from dcontainer.cli.generate.dir_models.test_dir import TestDir -from dcontainer.models.devcontainer_feature_definition import ( - FeatureDefinition, -) +from dcontainer.models.devcontainer_feature_definition import FeatureDefinition def generate( diff --git a/dcontainer/models/devcontainer_feature.py b/dcontainer/models/devcontainer_feature.py index 315b3343..8501da86 100644 --- a/dcontainer/models/devcontainer_feature.py +++ b/dcontainer/models/devcontainer_feature.py @@ -34,7 +34,7 @@ class Config: default: Optional[str] = Field( "", description="Default value if the user omits this option from their configuration.", - ) # todo: remove Optional state after SDKMAN feature is fixed + ) # todo: remove Optional state after SDKMAN feature is fixed description: Optional[str] = Field( None, description="A description of the option displayed to the user by a supporting tool.", diff --git a/dcontainer/oci/oci_feature_installer.py b/dcontainer/oci/oci_feature_installer.py index 74887894..52f54d09 100644 --- a/dcontainer/oci/oci_feature_installer.py +++ b/dcontainer/oci/oci_feature_installer.py @@ -1,16 +1,22 @@ import logging import os import pwd -import tempfile -from typing import Dict, Optional, Union import sys -import os +import tempfile from pathlib import Path +from typing import Dict, Optional, Union + import invoke from dcontainer.models.devcontainer_feature import Feature from dcontainer.oci.oci_feature import OCIFeature -from dcontainer.settings import DContainerSettings, ENV_CLI_LOCATION, ENV_PROPAGATE_CLI_LOCATION, ENV_FORCE_CLI_INSTALLATION, ENV_VERBOSE +from dcontainer.settings import ( + ENV_CLI_LOCATION, + ENV_FORCE_CLI_INSTALLATION, + ENV_PROPAGATE_CLI_LOCATION, + ENV_VERBOSE, + DContainerSettings, +) logger = logging.getLogger(__name__) @@ -43,14 +49,12 @@ def install( ) -> None: if options is None: options = {} - + if envs is None: envs = {} - feature_obj=feature_oci.get_devcontainer_feature_obj() + feature_obj = feature_oci.get_devcontainer_feature_obj() - options = cls._resolve_options( - feature_obj=feature_obj, options=options - ) + options = cls._resolve_options(feature_obj=feature_obj, options=options) logger.info("resolved options: %s", str(options)) remote_user = cls._resolve_remote_user(remote_user_name) @@ -67,7 +71,7 @@ def install( settings = DContainerSettings() if settings.verbose == "1": - verbose = True + verbose = True envs[ENV_VERBOSE] = settings.verbose envs[ENV_FORCE_CLI_INSTALLATION] = settings.force_cli_installation @@ -76,26 +80,32 @@ def install( if settings.propagate_cli_location == "1": if settings.cli_location != "": envs[ENV_CLI_LOCATION] = settings.cli_location - elif getattr(sys, 'frozen', False): + elif getattr(sys, "frozen", False): envs[ENV_CLI_LOCATION] = sys.executable else: - # override it with empty string in case it already exists + # override it with empty string in case it already exists envs[ENV_CLI_LOCATION] = "" - + except Exception as e: logger.warning(f"could not create settings: {str(e)}") - + env_variables_cmd = " ".join( - [f'{env_name}="{cls._escape_quotes(env_value)}"' for env_name, env_value in envs.items()] + [ + f'{env_name}="{cls._escape_quotes(env_value)}"' + for env_name, env_value in envs.items() + ] ) - - + with tempfile.TemporaryDirectory() as tempdir: feature_oci.download_and_extract(tempdir) - sys.stdout.reconfigure(encoding='utf-8') # some processes will print in utf-8 while original stdout accept only ascii, causing a "UnicodeEncodeError: 'ascii' codec can't encode characters" error - sys.stderr.reconfigure(encoding='utf-8') # some processes will print in utf-8 while original stdout accept only ascii, causing a "UnicodeEncodeError: 'ascii' codec can't encode characters" error - + sys.stdout.reconfigure( + encoding="utf-8" + ) # some processes will print in utf-8 while original stdout accept only ascii, causing a "UnicodeEncodeError: 'ascii' codec can't encode characters" error + sys.stderr.reconfigure( + encoding="utf-8" + ) # some processes will print in utf-8 while original stdout accept only ascii, causing a "UnicodeEncodeError: 'ascii' codec can't encode characters" error + response = invoke.run( f"cd {tempdir} && \ chmod +x -R . && \ @@ -108,9 +118,9 @@ def install( raise OCIFeatureInstaller.FeatureInstallationException( f"feature {feature_oci.path} failed to install. return_code: {response.return_code}. see logs for error reason." ) - + cls._set_permanent_envs(feature_obj) - + @classmethod def _set_permanent_envs(cls, feature: Feature) -> None: if feature.containerEnv is None: @@ -118,7 +128,9 @@ def _set_permanent_envs(cls, feature: Feature) -> None: feature_profile_dir = Path(cls._PROFILE_DIR) feature_profile_dir.mkdir(exist_ok=True, parents=True) - feature_profile_file = feature_profile_dir.joinpath(f"dcontainer-{feature.id}.sh") + feature_profile_file = feature_profile_dir.joinpath( + f"dcontainer-{feature.id}.sh" + ) if not feature_profile_file.exists(): feature_profile_file.touch() @@ -130,19 +142,17 @@ def _set_permanent_envs(cls, feature: Feature) -> None: for env_name, env_value in feature.containerEnv.items(): statement = f"export {env_name}={env_value}" if statement not in current_content: - current_content += f"\n{statement}" - + current_content += f"\n{statement}" + modified = True - + if modified: with open(feature_profile_file, "w") as f: f.write(current_content) - @classmethod def _escape_quotes(cls, value: str) -> str: return value.replace('"', '\\"') - @classmethod def _resolve_options( diff --git a/dcontainer/oci/oci_registry.py b/dcontainer/oci/oci_registry.py index 876c5c73..215cc605 100644 --- a/dcontainer/oci/oci_registry.py +++ b/dcontainer/oci/oci_registry.py @@ -105,9 +105,9 @@ def _generate_token(raw_response_header: str) -> str: www_authenticate = OCIRegistry._parse_www_authenticate(raw_response_header) token_request_link = f"{www_authenticate.realm}?service={www_authenticate.service}&scope={www_authenticate.scope}" - if not token_request_link.startswith('http'): + if not token_request_link.startswith("http"): raise ValueError("only http/https links are permited") - + response = urllib.request.urlopen(token_request_link) # nosec token = json.loads(response.read())["token"] return token @@ -116,10 +116,8 @@ def _generate_token(raw_response_header: str) -> str: def _attempt_request( url: str, headers: Optional[Dict[str, str]] = None ) -> http.client.HTTPResponse: - - if not url.startswith('http'): + if not url.startswith("http"): raise ValueError("only http/https links are permited") - if headers is None: headers = {} @@ -127,7 +125,6 @@ def _attempt_request( if "User-Agent" not in headers: headers["User-Agent"] = "dcontainer" - request = urllib.request.Request(url=url, headers=headers) try: diff --git a/dcontainer/settings.py b/dcontainer/settings.py index 6480692d..ab9b67e3 100644 --- a/dcontainer/settings.py +++ b/dcontainer/settings.py @@ -3,6 +3,7 @@ from pydantic import BaseSettings + class DContainerSettings(BaseSettings): class Config: env_prefix = "DCONTAINER_" @@ -12,7 +13,12 @@ class Config: force_cli_installation: str = "" verbose: str = "" + ENV_CLI_LOCATION = f"{DContainerSettings.Config.env_prefix}CLI_LOCATION" -ENV_PROPAGATE_CLI_LOCATION = f"{DContainerSettings.Config.env_prefix}PROPAGATE_CLI_LOCATION" -ENV_FORCE_CLI_INSTALLATION = f"{DContainerSettings.Config.env_prefix}FORCE_CLI_INSTALLATION" +ENV_PROPAGATE_CLI_LOCATION = ( + f"{DContainerSettings.Config.env_prefix}PROPAGATE_CLI_LOCATION" +) +ENV_FORCE_CLI_INSTALLATION = ( + f"{DContainerSettings.Config.env_prefix}FORCE_CLI_INSTALLATION" +) ENV_VERBOSE = f"{DContainerSettings.Config.env_prefix}VERBOSE" diff --git a/dcontainer/utils/version.py b/dcontainer/utils/version.py index 75f18d02..80906425 100644 --- a/dcontainer/utils/version.py +++ b/dcontainer/utils/version.py @@ -1,8 +1,8 @@ +import json import urllib import urllib.request -import json from importlib.metadata import version -from typing import Optional, List +from typing import List, Optional OWN_REPO = "devcontainers-contrib/cli" OWN_PACKAGE = "dcontainer" @@ -13,25 +13,30 @@ def _resolve_package_version(package: str) -> str: def _get_latest_release(repo: str) -> str: - response = urllib.request.urlopen(f"https://api.github.com/repos/{repo}/releases/latest") # nosec + response = urllib.request.urlopen( + f"https://api.github.com/repos/{repo}/releases/latest" + ) # nosec response_json = json.loads(response.read()) - resolved_version = response_json['name'] + resolved_version = response_json["name"] return resolved_version def _get_github_tags(repo: str) -> List[str]: - response = urllib.request.urlopen(f"https://api.github.com/repos/{repo}/tags") # nosec - return [tag['name'] for tag in json.loads(response.read())] + response = urllib.request.urlopen( + f"https://api.github.com/repos/{repo}/tags" + ) # nosec + return [tag["name"] for tag in json.loads(response.read())] def resolve_own_package_version() -> str: return _resolve_package_version(OWN_PACKAGE) + def resolve_own_release_version() -> str: package_version = _resolve_package_version(OWN_PACKAGE) tags = _get_github_tags(OWN_REPO) if package_version is not None: if f"v{package_version}" in tags: return f"v{package_version}" - - return _get_latest_release(OWN_REPO) \ No newline at end of file + + return _get_latest_release(OWN_REPO) diff --git a/test/cli/test_features.py b/test/cli/test_features.py index b2970cd8..d8c26207 100644 --- a/test/cli/test_features.py +++ b/test/cli/test_features.py @@ -1,18 +1,23 @@ import os -from dcontainer.cli.generate.generate_feature import generate -from helpers import RESOURCE_DIR import pathlib + import pytest +from helpers import RESOURCE_DIR -FEATURE_DEFINITION_DIR = os.path.join( - RESOURCE_DIR, "test_feature_definitions") +from dcontainer.cli.generate.generate_feature import generate + +FEATURE_DEFINITION_DIR = os.path.join(RESOURCE_DIR, "test_feature_definitions") TEST_IMAGE = "mcr.microsoft.com/devcontainers/base:debian" @pytest.mark.parametrize( - "feature_id,feature_definition_dir", [(v, os.path.join(FEATURE_DEFINITION_DIR, v)) for v in os.listdir(FEATURE_DEFINITION_DIR)] + "feature_id,feature_definition_dir", + [ + (v, os.path.join(FEATURE_DEFINITION_DIR, v)) + for v in os.listdir(FEATURE_DEFINITION_DIR) + ], ) def test_feature_dir_generation( shell, tmp_path: pathlib.Path, feature_id: str, feature_definition_dir: str @@ -25,7 +30,9 @@ def test_feature_dir_generation( output_dir=tmp_path, ) - assert os.path.isfile(os.path.join(tmp_path_str, "test", feature_id, "scenarios.json")) + assert os.path.isfile( + os.path.join(tmp_path_str, "test", feature_id, "scenarios.json") + ) assert os.path.isfile( os.path.join(tmp_path_str, "src", feature_id, "dependencies.sh") ) @@ -36,11 +43,14 @@ def test_feature_dir_generation( assert os.path.isfile( os.path.join(tmp_path_str, "src", feature_id, "install_command.sh") ) - @pytest.mark.parametrize( - "feature_id,feature_definition_dir", [(v, os.path.join(FEATURE_DEFINITION_DIR, v)) for v in os.listdir(FEATURE_DEFINITION_DIR)] + "feature_id,feature_definition_dir", + [ + (v, os.path.join(FEATURE_DEFINITION_DIR, v)) + for v in os.listdir(FEATURE_DEFINITION_DIR) + ], ) def test_feature_dir_generation_and_run_devcontainer_tests( shell, tmp_path: pathlib.Path, feature_id: str, feature_definition_dir: str diff --git a/test/conftest.py b/test/conftest.py index 6f607800..6e4ed9d1 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,6 +1,6 @@ import os -import pytest +import pytest print(f"_PYTEST_RAISE: {os.getenv('_PYTEST_RAISE', '0')}", flush=True) if os.getenv("_PYTEST_RAISE", "0") != "0":