From e37f40722b52c44d00655afe6722b52e7ebf48d2 Mon Sep 17 00:00:00 2001 From: foefl Date: Fri, 4 Jul 2025 12:30:42 +0200 Subject: [PATCH 1/3] add option to overwrite existing installations, related to #9 --- pyproject.toml | 4 ++-- src/pycage/clean.py | 18 +----------------- src/pycage/compile.py | 4 ++-- src/pycage/get.py | 42 ++++++++++++++++++++++++++++++++++++++---- src/pycage/helpers.py | 16 ++++++++++++++++ 5 files changed, 59 insertions(+), 25 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f84c885..776c03f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pycage" -version = "0.2.6" +version = "0.2.7dev1" description = "tool to handle standalone Python installations from the CLI" authors = [ {name = "Florian Förster", email = "f.foerster@d-opt.com"}, @@ -78,7 +78,7 @@ directory = "reports/coverage" [tool.bumpversion] -current_version = "0.2.6" +current_version = "0.2.7dev1" parse = """(?x) (?P0|[1-9]\\d*)\\. (?P0|[1-9]\\d*)\\. diff --git a/src/pycage/clean.py b/src/pycage/clean.py index cb1f8e5..30582df 100644 --- a/src/pycage/clean.py +++ b/src/pycage/clean.py @@ -1,6 +1,6 @@ import click -from pycage.helpers import get_interpreter, print_error +from pycage.helpers import delete_files, print_error @click.group(help="commands to remove specified files") @@ -8,22 +8,6 @@ def clean() -> None: pass -def delete_files( - glob_pattern: str, -) -> None: - try: - pth_intp = get_interpreter() - except RuntimeError: - click.echo("Base interpreter path could not be found", err=True) - return - - base_folder = pth_intp.parent - assert base_folder.is_dir(), "base folder not a directory" - files_to_delete = base_folder.glob(glob_pattern) - for file in files_to_delete: - file.unlink(missing_ok=True) - - @click.command(help="removes all pre-compiled byte code files in the distribution") def precompiled() -> None: try: diff --git a/src/pycage/compile.py b/src/pycage/compile.py index 13904be..8c3dd0d 100644 --- a/src/pycage/compile.py +++ b/src/pycage/compile.py @@ -4,7 +4,7 @@ import subprocess import click -from pycage import clean +from pycage import helpers from pycage.helpers import get_interpreter, print_error @@ -61,7 +61,7 @@ def compile( if delete_existing: try: - clean.delete_files(r"**/*.pyc") + helpers.delete_files(r"**/*.pyc") except Exception as err: print_error(err) return diff --git a/src/pycage/get.py b/src/pycage/get.py index 0cca9bf..62d5405 100644 --- a/src/pycage/get.py +++ b/src/pycage/get.py @@ -11,7 +11,12 @@ from dopt_basics.io import combine_route from requests.exceptions import HTTPError from pycage import config -from pycage.helpers import delete_folder_recursively, path_verify_existence, print_error +from pycage.helpers import ( + delete_files, + delete_folder_recursively, + path_verify_existence, + print_error, +) from pycage.types import JsonMetadata, RetrievalInfo # cfg_path = Path.cwd() / "../config/config.toml" @@ -113,6 +118,14 @@ def extract_archive( default=None, help="specifiy a different target location, default: current working directory", ) +@click.option( + "-ow", + "--overwrite", + is_flag=True, + show_default=True, + default=False, + help="overwrite installation folder instead of deleting", +) @click.option( "-k", "--keep", @@ -141,6 +154,7 @@ def get( release_tag: str | None, force_reextract: bool, dl_folder: Path | None, + overwrite: bool, keep: bool, ) -> None: url_metadata = cast(str, config.CFG["metadata"]["URL"]) @@ -164,21 +178,30 @@ def get( # destination folder dl_folder = dl_folder if dl_folder is not None else Path.cwd() path_verify_existence(dl_folder) - # folder_extract = prepare_path(dl_folder, None, None, None, create_folder=True) target_folder = dl_folder / "python" folder_extract = dl_folder src_file = dl_folder / filename + _overwrite: bool = False + if target_folder.exists() and overwrite: + if click.confirm("Do you really want to overwrite the existing installation?"): + _overwrite = True + else: + click.echo( + "Overwrite option ignored. Delete existing installation before extraction." + ) try: if not src_file.exists(): click.echo("File not yet available. Download...") load_file_from_url(url=retrieval_info.url, file_save_path=src_file) - delete_folder_recursively(target_folder) + if not _overwrite: + delete_folder_recursively(target_folder) extract_archive(src_file, folder_extract) click.echo("Download and extraction successfully.") elif force_reextract: click.echo("File already downloaded. Re-extract file...") - delete_folder_recursively(target_folder) + if not _overwrite: + delete_folder_recursively(target_folder) extract_archive(src_file, folder_extract) click.echo("Re-extraction successfully.") else: @@ -189,6 +212,17 @@ def get( except Exception as err: print_error(err) + if _overwrite: + click.echo( + "Overwrite option was chosen. Delete any existing pre-compiled " + "files to force recompilation..." + ) + try: + delete_files(r"**/*.pyc") + except Exception as err: + print_error(err) + click.echo("Pre-compiled files were removed successfully.") + if not keep: try: src_file.unlink() diff --git a/src/pycage/helpers.py b/src/pycage/helpers.py index c648c3c..b2b90dd 100644 --- a/src/pycage/helpers.py +++ b/src/pycage/helpers.py @@ -57,3 +57,19 @@ def get_interpreter() -> Path: raise RuntimeError("Base interpreter not found") from err return pth_int + + +def delete_files( + glob_pattern: str, +) -> None: + try: + pth_intp = get_interpreter() + except RuntimeError: + click.echo("Base interpreter path could not be found", err=True) + return + + base_folder = pth_intp.parent + assert base_folder.is_dir(), "base folder not a directory" + files_to_delete = base_folder.glob(glob_pattern) + for file in files_to_delete: + file.unlink(missing_ok=True) -- 2.34.1 From 54bd6026c9100c5d25f1c222906d9bf1913e47c9 Mon Sep 17 00:00:00 2001 From: foefl Date: Fri, 4 Jul 2025 12:38:43 +0200 Subject: [PATCH 2/3] more concise description of overwrite option for get cmd --- src/pycage/get.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pycage/get.py b/src/pycage/get.py index 62d5405..1ed1bff 100644 --- a/src/pycage/get.py +++ b/src/pycage/get.py @@ -124,7 +124,13 @@ def extract_archive( is_flag=True, show_default=True, default=False, - help="overwrite installation folder instead of deleting", + help=( + "overwrite installation with specified Python bundle instead of deleting it, any " + "pre-compiled files are deleted to force the re-comilation of the code by the new " + "Python interpreter. CAUTION: No compatibility checks for the installed packages is " + "done. Therefore, it is not guaranteed that the new installation will work. This " + "option is primarily intented for patch versions of Python." + ), ) @click.option( "-k", -- 2.34.1 From 6061294b28fbf78771b27e01480483a62f3f1c54 Mon Sep 17 00:00:00 2001 From: foefl Date: Fri, 4 Jul 2025 13:01:11 +0200 Subject: [PATCH 3/3] add command to show version and corresponding release tags as pairs which can be used for the get command, related to #10 --- src/pycage/constants.py | 8 +++++++- src/pycage/get.py | 15 +++++++++++++++ src/pycage/main.py | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/pycage/constants.py b/src/pycage/constants.py index 84710d2..09ebc3b 100644 --- a/src/pycage/constants.py +++ b/src/pycage/constants.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Final +from typing import Final, TypeAlias LIB_ROOT_FOLDER: Final[Path] = Path(__file__).parent if not LIB_ROOT_FOLDER.exists(): @@ -8,3 +8,9 @@ if not LIB_ROOT_FOLDER.exists(): MSVC_FOLDER: Final[Path] = LIB_ROOT_FOLDER / "_files/msvc-redist" if not MSVC_FOLDER.exists(): raise FileNotFoundError(f"Folder for MSVC Redist files not found under: >{MSVC_FOLDER}<") + +PyVersionToReleaseTag: TypeAlias = tuple[str, str] +PY_VERSION_TO_RELEASE_TAG_PAIRS: Final[tuple[PyVersionToReleaseTag, ...]] = ( + ("3.11.12", "20250409"), + ("3.11.13", "20250702"), +) diff --git a/src/pycage/get.py b/src/pycage/get.py index 1ed1bff..14a09ac 100644 --- a/src/pycage/get.py +++ b/src/pycage/get.py @@ -11,6 +11,7 @@ from dopt_basics.io import combine_route from requests.exceptions import HTTPError from pycage import config +from pycage.constants import PY_VERSION_TO_RELEASE_TAG_PAIRS from pycage.helpers import ( delete_files, delete_folder_recursively, @@ -237,3 +238,17 @@ def get( "The archive file could not be deleted because of following exception..." ) print_error(err) + + +@click.command( + help=( + "show supported Python version and corresponding release tags from the" + "standalone repository to have a quick lookup which version can be downloaded " + "with which release tag, information can be used with the 'get' command" + ) +) +def show_version_release_tag() -> None: + click.echo("Known version and release tag pairs are...") + click.echo("Python Version --> Release Tag") + for pair in PY_VERSION_TO_RELEASE_TAG_PAIRS: + click.echo(f"{pair[0]:<8} --> {pair[1]:>9}") diff --git a/src/pycage/main.py b/src/pycage/main.py index d792575..cdbe0d0 100644 --- a/src/pycage/main.py +++ b/src/pycage/main.py @@ -15,6 +15,7 @@ def cli(): cli.add_command(pycage.get.get) +cli.add_command(pycage.get.show_version_release_tag) cli.add_command(pycage.venv.venv) cli.add_command(pycage.files.copy) cli.add_command(pycage.compile.compile) -- 2.34.1