From 72796e9b8377b8df91b1dc4b33dd08171e8108d6 Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Thu, 21 Nov 2024 18:57:11 +0100 Subject: [PATCH 1/3] Switch from click to tyro --- .pre-commit-config.yaml | 2 +- README.md | 259 ++++++++------ manageprojects/__init__.py | 2 +- manageprojects/cli_app/__init__.py | 36 +- manageprojects/cli_app/manage.py | 329 +++++++----------- .../cli_app/update_readme_history.py | 27 -- manageprojects/cli_dev/__init__.py | 32 +- manageprojects/cli_dev/code_style.py | 10 +- manageprojects/cli_dev/git_hooks.py | 10 +- manageprojects/cli_dev/packaging.py | 74 +--- manageprojects/cli_dev/testing.py | 22 +- .../cli_dev/update_readme_history.py | 11 +- manageprojects/test_utils/click_cli_utils.py | 13 +- manageprojects/tests/test_cli.py | 70 ++-- manageprojects/tests/test_click_cli_utils.py | 21 -- .../tests/test_cookiecutter_templates.py | 21 +- manageprojects/tests/test_readme.py | 56 +-- manageprojects/utilities/log_utils.py | 26 +- pyproject.toml | 9 +- uv.lock | 73 ++-- 20 files changed, 506 insertions(+), 597 deletions(-) delete mode 100644 manageprojects/cli_app/update_readme_history.py delete mode 100644 manageprojects/tests/test_click_cli_utils.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index faa6e55..310b352 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,6 +8,6 @@ default_install_hook_types: repos: - repo: https://github.com/jedie/cli-base-utilities - rev: v0.22.0 + rev: v0.23.0 hooks: - id: update-readme-history diff --git a/README.md b/README.md index d5542d8..8143f0c 100644 --- a/README.md +++ b/README.md @@ -37,25 +37,49 @@ The output of `./cli.py --help` looks like: [comment]: <> (✂✂✂ auto generated main help start ✂✂✂) ``` -Usage: ./cli.py [OPTIONS] COMMAND [ARGS]... - -╭─ Options ────────────────────────────────────────────────────────────────────────────────────────╮ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────╮ -│ clone-project Clone existing project by replay the cookiecutter template in a new │ -│ directory. │ -│ format-file Format and check the given python source code file with │ -│ darker/autoflake/isort/pyupgrade/autopep8/mypy etc. │ -│ reverse Create a cookiecutter template from a managed project. │ -│ start-project Start a new "managed" project via a CookieCutter Template. Note: The │ -│ CookieCutter Template *must* be use git! │ -│ update-project Update a existing project. │ -│ update-readme-history Update project history base on git commits/tags in README.md │ -│ version Print version and exit │ -│ wiggle Run wiggle to merge *.rej in given directory. │ -│ https://github.com/neilbrown/wiggle │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ +usage: ./cli.py [-h] {clone-project,format-file,reverse,start-project,update-project,version,wiggle} + + + +╭─ options ──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ -h, --help show this help message and exit │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ subcommands ──────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ {clone-project,format-file,reverse,start-project,update-project,version,wiggle} │ +│ clone-project │ +│ Clone existing project by replay the cookiecutter template in a new directory. e.g.: │ +│ │ +│ │ +│ ./cli.py clone-project ~/foo/bar ~/cloned/ │ +│ format-file Format and check the given python source code file with ruff, codespell and mypy. The optional │ +│ fallback values will be only used, if we can't get them from the project meta files like │ +│ ".editorconfig" and "pyproject.toml" │ +│ reverse Create a cookiecutter template from a managed project. e.g.: │ +│ │ +│ │ +│ ./cli.py reverse ~/my_managed_project/ ~/my_new_cookiecutter_template/ │ +│ start-project │ +│ Start a new "managed" project via a CookieCutter Template. Note: The CookieCutter Template │ +│ *must* be use git! │ +│ │ +│ e.g.: │ +│ │ +│ │ +│ ./cli.py start-project https://github.com/jedie/cookiecutter_templates/ --directory │ +│ piptools-python ~/foobar/ │ +│ update-project │ +│ Update a existing project. e.g. update by overwrite (and merge changes manually via git): │ +│ │ +│ │ +│ ./cli.py update-project ~/foo/bar/ │ +│ version Print version and exit │ +│ wiggle Run wiggle to merge *.rej in given directory. https://github.com/neilbrown/wiggle │ +│ │ +│ e.g.: │ +│ │ +│ │ +│ ./cli.py wiggle ~/my_managed_project/ │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ``` [comment]: <> (✂✂✂ auto generated main help end ✂✂✂) @@ -67,31 +91,33 @@ Help from `./cli.py start-project --help` Looks like: [comment]: <> (✂✂✂ auto generated start-project help start ✂✂✂) ``` -Usage: ./cli.py start-project [OPTIONS] TEMPLATE OUTPUT_DIR - - Start a new "managed" project via a CookieCutter Template. Note: The CookieCutter Template *must* - be use git! - e.g.: - ./cli.py start-project https://github.com/jedie/cookiecutter_templates/ --directory - piptools-python ~/foobar/ - -╭─ Options ────────────────────────────────────────────────────────────────────────────────────────╮ -│ --directory TEXT Cookiecutter Option: Directory within repo that holds │ -│ cookiecutter.json file for advanced repositories with multi │ -│ templates in it │ -│ --checkout TEXT Cookiecutter Option: branch, tag or commit to checkout after git │ -│ clone │ -│ --input/--no-input Cookiecutter Option: Do not prompt for parameters and only use │ -│ cookiecutter.json file content │ -│ [default: input] │ -│ --replay/--no-replay Cookiecutter Option: Do not prompt for parameters and only use │ -│ information entered previously │ -│ [default: no-replay] │ -│ --password TEXT Cookiecutter Option: Password to use when extracting the │ -│ repository │ -│ --config-file FILE Cookiecutter Option: Optional path to "cookiecutter_config.yaml" │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ +usage: ./cli.py start-project [-h] [START-PROJECT OPTIONS] STR PATH + +Start a new "managed" project via a CookieCutter Template. Note: The CookieCutter Template *must* be use git! + +e.g.: + +./cli.py start-project https://github.com/jedie/cookiecutter_templates/ --directory piptools-python ~/foobar/ + +╭─ positional arguments ─────────────────────────────────────────────────────────────────────────────────────────────╮ +│ STR The name of the CookieCutter Template. (required) │ +│ PATH Target path for the new project. Must not exist yet! (required) │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ options ──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ -h, --help show this help message and exit │ +│ -v, --verbosity Verbosity level; e.g.: -v, -vv, -vvv, etc. (repeatable) │ +│ --directory {None}|STR Cookiecutter Option: Directory within repo that holds cookiecutter.json file for advanced │ +│ repositories with multi templates in it (default: None) │ +│ --replay, --no-replay Cookiecutter Option: Do not prompt for parameters and only use information entered │ +│ previously (default: False) │ +│ --input, --no-input Cookiecutter Option: Do not prompt for parameters and only use cookiecutter.json file │ +│ content (default: False) │ +│ --checkout {None}|STR Cookiecutter Option: Optional branch, tag or commit ID to checkout after clone (default: │ +│ None) │ +│ --password {None}|STR Cookiecutter Option: Password to use when extracting the repository (default: None) │ +│ --config-file {None}|PATH │ +│ Cookiecutter Option: Optional path to "cookiecutter_config.yaml" (default: None) │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ``` [comment]: <> (✂✂✂ auto generated start-project help end ✂✂✂) @@ -102,29 +128,30 @@ Help from `./cli.py update-project --help` Looks like: [comment]: <> (✂✂✂ auto generated update-project help start ✂✂✂) ``` -Usage: ./cli.py update-project [OPTIONS] PROJECT_PATH - - Update a existing project. - e.g. update by overwrite (and merge changes manually via git): - ./cli.py update-project ~/foo/bar/ - -╭─ Options ────────────────────────────────────────────────────────────────────────────────────────╮ -│ --overwrite/--no-overwrite Overwrite all Cookiecutter template files to the last │ -│ template state and do not apply the changes via git patches. │ -│ The developer is supposed to apply the differences manually │ -│ via git. Will be aborted if the project git repro is not in │ -│ a clean state. │ -│ [default: overwrite] │ -│ --password TEXT Cookiecutter Option: Password to use when extracting the │ -│ repository │ -│ --config-file FILE Cookiecutter Option: Optional path to │ -│ "cookiecutter_config.yaml" │ -│ --input/--no-input Cookiecutter Option: Do not prompt for parameters and only │ -│ use cookiecutter.json file content │ -│ [default: no-input] │ -│ --cleanup/--no-cleanup Cleanup created temporary files [default: cleanup] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ +usage: ./cli.py update-project [-h] [UPDATE-PROJECT OPTIONS] PATH + +Update a existing project. e.g. update by overwrite (and merge changes manually via git): + +./cli.py update-project ~/foo/bar/ + +╭─ positional arguments ─────────────────────────────────────────────────────────────────────────────────────────────╮ +│ PATH project-path (required) │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ options ──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ -h, --help show this help message and exit │ +│ -v, --verbosity Verbosity level; e.g.: -v, -vv, -vvv, etc. (repeatable) │ +│ --overwrite, --no-overwrite │ +│ Overwrite all Cookiecutter template files to the last template state and do not apply the │ +│ changes via git patches. The developer is supposed to apply the differences manually via │ +│ git. Will be aborted if the project git repro is not in a clean state. (default: True) │ +│ --cleanup, --no-cleanup │ +│ Cleanup created temporary files (default: True) │ +│ --input, --no-input Cookiecutter Option: Do not prompt for parameters and only use cookiecutter.json file │ +│ content (default: False) │ +│ --password {None}|STR Cookiecutter Option: Password to use when extracting the repository (default: None) │ +│ --config-file {None}|PATH │ +│ Cookiecutter Option: Optional path to "cookiecutter_config.yaml" (default: None) │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ``` [comment]: <> (✂✂✂ auto generated update-project help end ✂✂✂) @@ -259,22 +286,22 @@ You can use `format-file` as "Action on save" or manual action in your IDE to fi [comment]: <> (✂✂✂ auto generated format-file help start ✂✂✂) ``` -Usage: ./cli.py format-file [OPTIONS] FILE_PATH - - Format and check the given python source code file with - darker/autoflake/isort/pyupgrade/autopep8/mypy etc. - The optional fallback values will be only used, if we can't get them from the project meta files - like ".editorconfig" and "pyproject.toml" - -╭─ Options ────────────────────────────────────────────────────────────────────────────────────────╮ -│ --py-version TEXT Fallback Python version for darker/pyupgrade, if version is not │ -│ defined in pyproject.toml │ -│ [default: 3.10] │ -│ --max-line-length -l INTEGER Fallback max. line length for darker/isort etc., if not defined │ -│ in .editorconfig │ -│ [default: 119] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ +usage: ./cli.py format-file [-h] [FORMAT-FILE OPTIONS] PATH + +Format and check the given python source code file with ruff, codespell and mypy. The optional fallback values will be +only used, if we can't get them from the project meta files like ".editorconfig" and "pyproject.toml" + +╭─ positional arguments ─────────────────────────────────────────────────────────────────────────────────────────────╮ +│ PATH file-path (required) │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ options ──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ -h, --help show this help message and exit │ +│ -v, --verbosity Verbosity level; e.g.: -v, -vv, -vvv, etc. (repeatable) │ +│ --py-version STR Fallback Python version for darker/pyupgrade, if version is not defined in pyproject.toml │ +│ (default: 3.10) │ +│ --max-line-length INT Fallback max. line length for darker/isort etc., if not defined in .editorconfig (default: │ +│ 119) │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ``` [comment]: <> (✂✂✂ auto generated format-file help end ✂✂✂) @@ -301,30 +328,40 @@ The output of `./dev-cli.py --help` looks like: [comment]: <> (✂✂✂ auto generated dev help start ✂✂✂) ``` -Usage: ./dev-cli.py [OPTIONS] COMMAND [ARGS]... - -╭─ Options ────────────────────────────────────────────────────────────────────────────────────────╮ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -╭─ Commands ───────────────────────────────────────────────────────────────────────────────────────╮ -│ build Build the manageproject (More a test of │ -│ manageprojects.utilities.publish.build) │ -│ coverage Run tests and show coverage report. │ -│ git-hooks Setup our "pre-commit" git hooks │ -│ install Install requirements and 'manageprojects' via pip as editable. │ -│ lint Check/fix code style by run: "ruff check --fix" │ -│ mypy Run Mypy (configured in pyproject.toml) │ -│ nox Run nox │ -│ pip-audit Run pip-audit check against current requirements files │ -│ publish Build and upload this project to PyPi │ -│ run-git-hooks Run the installed "pre-commit" git hooks │ -│ test Run unittests │ -│ update Update dependencies (uv.lock) and git pre-commit hooks │ -│ update-readme-history Update project history base on git commits/tags in README.md │ -│ update-test-snapshot-files Update all test snapshot files (by remove and recreate all snapshot │ -│ files) │ -│ version Print version and exit │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ +usage: ./dev-cli.py [-h] + {coverage,git-hooks,install,lint,mypy,nox,pip-audit,publish,run-git-hooks,test,update,update-readm +e-history,update-test-snapshot-files,version} + + + +╭─ options ──────────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ -h, --help show this help message and exit │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ +╭─ subcommands ──────────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ {coverage,git-hooks,install,lint,mypy,nox,pip-audit,publish,run-git-hooks,test,update,update-readme-history,update │ +│ -test-snapshot-files,version} │ +│ coverage Run tests and show coverage report. │ +│ git-hooks Setup our "pre-commit" git hooks │ +│ install Install requirements and 'manageprojects' via pip as editable. │ +│ lint Check/fix code style by run: "ruff check --fix" │ +│ mypy Run Mypy (configured in pyproject.toml) │ +│ nox Run nox │ +│ pip-audit Run pip-audit check against current requirements files │ +│ publish Build and upload this project to PyPi │ +│ run-git-hooks │ +│ Run the installed "pre-commit" git hooks │ +│ test Run unittests │ +│ update Update dependencies (uv.lock) and git pre-commit hooks │ +│ update-readme-history │ +│ Update project history base on git commits/tags in README.md Will be exited with 1 if the │ +│ README.md was updated otherwise with 0. │ +│ │ +│ Also, callable via e.g.: │ +│ python -m cli_base update-readme-history -v │ +│ update-test-snapshot-files │ +│ Update all test snapshot files (by remove and recreate all snapshot files) │ +│ version Print version and exit │ +╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ``` [comment]: <> (✂✂✂ auto generated dev help end ✂✂✂) @@ -343,6 +380,8 @@ See also git tags: https://github.com/jedie/manageprojects/tags [comment]: <> (✂✂✂ auto generated history start ✂✂✂) +* [v0.23.0](https://github.com/jedie/manageprojects/compare/v0.22.1...v0.23.0) + * 2024-11-21 - Switch from click to tyro * [v0.22.1](https://github.com/jedie/manageprojects/compare/v0.22.0...v0.22.1) * 2025-09-03 - Make install/setup python script executeable after renew the code * 2025-09-03 - Update code style stuff to ruff @@ -353,12 +392,12 @@ See also git tags: https://github.com/jedie/manageprojects/tags * [v0.21.3](https://github.com/jedie/manageprojects/compare/v0.21.2...v0.21.3) * 2025-04-22 - Support getting the current version from hatchling * 2025-04-22 - replace setuptools with hatchling -* [v0.21.2](https://github.com/jedie/manageprojects/compare/v0.21.1...v0.21.2) - * 2025-03-11 - Fix nox CLI call - * 2025-03-11 - Update requirements and replace tox with nox
Expand older history entries ... +* [v0.21.2](https://github.com/jedie/manageprojects/compare/v0.21.1...v0.21.2) + * 2025-03-11 - Fix nox CLI call + * 2025-03-11 - Update requirements and replace tox with nox * [v0.21.1](https://github.com/jedie/manageprojects/compare/v0.21.0...v0.21.1) * 2025-01-31 - Use cli_tools.path_utils.which * [v0.21.0](https://github.com/jedie/manageprojects/compare/v0.20.0...v0.21.0) diff --git a/manageprojects/__init__.py b/manageprojects/__init__.py index 133bdf0..5df4739 100644 --- a/manageprojects/__init__.py +++ b/manageprojects/__init__.py @@ -4,5 +4,5 @@ """ # See https://packaging.python.org/en/latest/specifications/version-specifiers/ -__version__ = '0.22.1' +__version__ = '0.23.0' __author__ = 'Jens Diemer ' diff --git a/manageprojects/cli_app/__init__.py b/manageprojects/cli_app/__init__.py index 6ed6b32..d8701f0 100644 --- a/manageprojects/cli_app/__init__.py +++ b/manageprojects/cli_app/__init__.py @@ -2,14 +2,14 @@ CLI for usage """ +from collections.abc import Sequence import logging import sys from cli_base.autodiscover import import_all_files from cli_base.cli_tools.version_info import print_version from rich import print # noqa -import rich_click as click -from rich_click import RichGroup +from tyro.extras import SubcommandApp import manageprojects from manageprojects import constants @@ -17,35 +17,25 @@ logger = logging.getLogger(__name__) +app = SubcommandApp() -class ClickGroup(RichGroup): # FIXME: How to set the "info_name" easier? - def make_context(self, info_name, *args, **kwargs): - info_name = './cli.py' - return super().make_context(info_name, *args, **kwargs) - - -@click.group( - cls=ClickGroup, - epilog=constants.CLI_EPILOG, -) -def cli(): - pass - - -# Register all click commands, just by import all files in this package: +# Register all CLI commands, just by import all files in this package: import_all_files(package=__package__, init_file=__file__) -@cli.command() +@app.command def version(): """Print version and exit""" # Pseudo command, because the version always printed on every CLI call ;) sys.exit(0) -def main(): +def main(args: Sequence[str] | None = None): print_version(manageprojects) - - # Execute Click CLI: - cli.name = './cli.py' - cli() + app.cli( + prog='./cli.py', + description=constants.CLI_EPILOG, + use_underscores=False, # use hyphens instead of underscores + sort_subcommands=True, + args=args, + ) diff --git a/manageprojects/cli_app/manage.py b/manageprojects/cli_app/manage.py index 30ba265..b800d25 100644 --- a/manageprojects/cli_app/manage.py +++ b/manageprojects/cli_app/manage.py @@ -9,23 +9,15 @@ import shutil import subprocess import sys +from typing import Annotated from bx_py_utils.path import assert_is_dir from cli_base.cli_tools.subprocess_utils import verbose_check_call -from cli_base.cli_tools.verbosity import OPTION_KWARGS_VERBOSE -from cli_base.cli_tools.version_info import print_version -from cli_base.click_defaults import ( - ARGUMENT_EXISTING_DIR, - ARGUMENT_EXISTING_FILE, - ARGUMENT_NOT_EXISTING_DIR, - OPTION_ARGS_DEFAULT_FALSE, - OPTION_ARGS_DEFAULT_TRUE, -) +from cli_base.tyro_commands import TyroVerbosityArgType from rich import print # noqa -import rich_click as click +from tyro.conf import arg -import manageprojects -from manageprojects.cli_app import cli +from manageprojects.cli_app import app from manageprojects.constants import ( FORMAT_PY_FILE_DEFAULT_MAX_LINE_LENGTH, FORMAT_PY_FILE_DEFAULT_MIN_PYTHON_VERSION, @@ -44,52 +36,49 @@ logger = logging.getLogger(__name__) -@cli.command() -@click.argument('template') -@click.argument('output_dir', **ARGUMENT_NOT_EXISTING_DIR) -@click.option( - '--directory', - default=None, - help=( - 'Cookiecutter Option: Directory within repo that holds cookiecutter.json file' - ' for advanced repositories with multi templates in it' - ), -) -@click.option( - '--checkout', - default=None, - help='Cookiecutter Option: branch, tag or commit to checkout after git clone', -) -@click.option( - '--input/--no-input', - **OPTION_ARGS_DEFAULT_TRUE, - help=('Cookiecutter Option: Do not prompt for parameters' ' and only use cookiecutter.json file content'), -) -@click.option( - '--replay/--no-replay', - **OPTION_ARGS_DEFAULT_FALSE, - help=('Cookiecutter Option: Do not prompt for parameters' ' and only use information entered previously'), -) -@click.option( - '--password', - default=None, - help='Cookiecutter Option: Password to use when extracting the repository', -) -@click.option( - '--config-file', - default=None, - type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True), - help='Cookiecutter Option: Optional path to "cookiecutter_config.yaml"', -) +@app.command def start_project( - template: str, - output_dir: Path, - directory: str | None, - checkout: str | None, - input: bool, - replay: bool, - password: str | None, - config_file: Path | None, + template: Annotated[ + str, + arg(help='The name of the CookieCutter Template.'), + ], + output_dir: Annotated[ + Path, + arg(help='Target path for the new project. Must not exist yet!'), + ], + /, + verbosity: TyroVerbosityArgType, + # + # Cookiecutter options: + directory: Annotated[ + str | None, + arg( + help=( + 'Cookiecutter Option: Directory within repo that holds cookiecutter.json file' + ' for advanced repositories with multi templates in it' + ) + ), + ] = None, + replay: Annotated[ + bool, + arg(help='Cookiecutter Option: Do not prompt for parameters and only use information entered previously'), + ] = False, + input: Annotated[ + bool, + arg(help='Cookiecutter Option: Do not prompt for parameters and only use cookiecutter.json file content'), + ] = False, + checkout: Annotated[ + str | None, + arg(help='Cookiecutter Option: Optional branch, tag or commit ID to checkout after clone'), + ] = None, + password: Annotated[ + str | None, + arg(help='Cookiecutter Option: Password to use when extracting the repository'), + ] = None, + config_file: Annotated[ + Path | None, + arg(help='Cookiecutter Option: Optional path to "cookiecutter_config.yaml"'), + ] = None, ): """ Start a new "managed" project via a CookieCutter Template. @@ -99,7 +88,7 @@ def start_project( ./cli.py start-project https://github.com/jedie/cookiecutter_templates/ --directory piptools-python ~/foobar/ """ - log_config() + log_config(verbosity, log_in_file=True) print(f'Start project with template: {template!r}') print(f'Destination: {output_dir}') if output_dir.exists(): @@ -124,49 +113,40 @@ def start_project( return result -cli.add_command(start_project) - - -@cli.command() -@click.argument('project_path', **ARGUMENT_EXISTING_DIR) -@click.option( - '--overwrite/--no-overwrite', - **OPTION_ARGS_DEFAULT_TRUE, - help=( - 'Overwrite all Cookiecutter template files to the last template state and' - ' do not apply the changes via git patches.' - ' The developer is supposed to apply the differences manually via git.' - ' Will be aborted if the project git repro is not in a clean state.' - ), -) -@click.option( - '--password', - default=None, - help='Cookiecutter Option: Password to use when extracting the repository', -) -@click.option( - '--config-file', - default=None, - type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True), - help='Cookiecutter Option: Optional path to "cookiecutter_config.yaml"', -) -@click.option( - '--input/--no-input', - **OPTION_ARGS_DEFAULT_FALSE, - help='Cookiecutter Option: Do not prompt for parameters and only use cookiecutter.json file content', -) -@click.option( - '--cleanup/--no-cleanup', - **OPTION_ARGS_DEFAULT_TRUE, - help='Cleanup created temporary files', -) +@app.command def update_project( project_path: Path, - overwrite: bool, - password: str | None, - config_file: Path | None, - input: bool, - cleanup: bool, + /, + verbosity: TyroVerbosityArgType, + overwrite: Annotated[ + bool, + arg( + help=( + 'Overwrite all Cookiecutter template files to the last template state and' + ' do not apply the changes via git patches.' + ' The developer is supposed to apply the differences manually via git.' + ' Will be aborted if the project git repro is not in a clean state.' + ) + ), + ] = True, + cleanup: Annotated[ + bool, + arg(help='Cleanup created temporary files'), + ] = True, + # + # Cookiecutter options: + input: Annotated[ + bool, + arg(help='Cookiecutter Option: Do not prompt for parameters and only use cookiecutter.json file content'), + ] = False, + password: Annotated[ + str | None, + arg(help='Cookiecutter Option: Password to use when extracting the repository'), + ] = None, + config_file: Annotated[ + Path | None, + arg(help='Cookiecutter Option: Optional path to "cookiecutter_config.yaml"'), + ] = None, ): """ Update a existing project. @@ -175,7 +155,7 @@ def update_project( ./cli.py update-project ~/foo/bar/ """ - log_config() + log_config(verbosity, log_in_file=True) print(f'Update project: "{project_path}"...') update_managed_project( project_path=project_path, @@ -188,35 +168,36 @@ def update_project( print(f'Managed project "{project_path}" updated, ok.') -cli.add_command(update_project) - - -@cli.command() -@click.argument('project_path', **ARGUMENT_EXISTING_DIR) -@click.argument('output_dir', **ARGUMENT_NOT_EXISTING_DIR) -@click.option( - '--password', - default=None, - help='Cookiecutter Option: Password to use when extracting the repository', -) -@click.option( - '--config-file', - default=None, - type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True), - help='Cookiecutter Option: Optional path to "cookiecutter_config.yaml"', -) -@click.option( - '--input/--no-input', - **OPTION_ARGS_DEFAULT_FALSE, - help=('Cookiecutter Option: Do not prompt for parameters' ' and only use cookiecutter.json file content'), -) +@app.command def clone_project( - project_path: Path, - output_dir: Path, - input: bool, - checkout: str | None = None, - password: str | None = None, - config_file: Path | None = None, + project_path: Annotated[ + Path, + arg(help='Source project that should be cloned. Must be a managed project!'), + ], + output_dir: Annotated[ + Path, + arg(help='Destination of the cloned project. Must not exist yet!'), + ], + /, + verbosity: TyroVerbosityArgType, + # + # Cookiecutter options: + input: Annotated[ + bool, + arg(help='Cookiecutter Option: Do not prompt for parameters and only use cookiecutter.json file content'), + ] = False, + checkout: Annotated[ + str | None, + arg(help='Cookiecutter Option: Optional branch, tag or commit ID to checkout after clone'), + ] = None, + password: Annotated[ + str | None, + arg(help='Cookiecutter Option: Password to use when extracting the repository'), + ] = None, + config_file: Annotated[ + Path | None, + arg(help='Cookiecutter Option: Optional path to "cookiecutter_config.yaml"'), + ] = None, ): """ Clone existing project by replay the cookiecutter template in a new directory. @@ -225,8 +206,7 @@ def clone_project( ./cli.py clone-project ~/foo/bar ~/cloned/ """ - print(locals()) - log_config() + log_config(verbosity=verbosity) return clone_managed_project( project_path=project_path, destination=output_dir, @@ -237,23 +217,13 @@ def clone_project( ) -cli.add_command(clone_project) - - -@cli.command() -@click.argument('project_path', **ARGUMENT_EXISTING_DIR) -@click.argument('destination', **ARGUMENT_NOT_EXISTING_DIR) -@click.option( - '--overwrite/--no-overwrite', - **OPTION_ARGS_DEFAULT_FALSE, - help='Overwrite existing files.', -) -@click.option('-v', '--verbosity', **OPTION_KWARGS_VERBOSE) +@app.command def reverse( project_path: Path, destination: Path, - overwrite: bool, - verbosity: int, + /, + verbosity: TyroVerbosityArgType, + overwrite: Annotated[bool, arg(help='Overwrite existing files.')] = False, ): """ Create a cookiecutter template from a managed project. @@ -262,7 +232,7 @@ def reverse( ./cli.py reverse ~/my_managed_project/ ~/my_new_cookiecutter_template/ """ - log_config() + log_config(verbosity) return reverse_managed_project( project_path=project_path, destination=destination, @@ -271,17 +241,12 @@ def reverse( ) -cli.add_command(reverse) - - -@cli.command() -@click.argument('project_path', **ARGUMENT_EXISTING_DIR) -@click.option( - '--words/--no-words', - **OPTION_ARGS_DEFAULT_FALSE, - help='wiggle Option: word-wise diff and merge.', -) -def wiggle(project_path: Path, words: bool): +@app.command +def wiggle( + project_path: Path, + /, + words: Annotated[bool, arg(help='wiggle Option: word-wise diff and merge.')] = False, +): """ Run wiggle to merge *.rej in given directory. https://github.com/neilbrown/wiggle @@ -320,37 +285,27 @@ def wiggle(project_path: Path, words: bool): continue -cli.add_command(wiggle) - - -@cli.command() -@click.option( - '--py-version', - default=FORMAT_PY_FILE_DEFAULT_MIN_PYTHON_VERSION, - show_default=True, - help='Fallback Python version for darker/pyupgrade, if version is not defined in pyproject.toml', -) -@click.option( - '-l', - '--max-line-length', - default=FORMAT_PY_FILE_DEFAULT_MAX_LINE_LENGTH, - type=int, - show_default=True, - help='Fallback max. line length for darker/isort etc., if not defined in .editorconfig', -) -@click.argument('file_path', **ARGUMENT_EXISTING_FILE) +@app.command def format_file( - *, - py_version: str, - max_line_length: int, file_path: Path, + /, + verbosity: TyroVerbosityArgType, + py_version: Annotated[ + str, + arg(help='Fallback Python version for darker/pyupgrade, if version is not defined in pyproject.toml'), + ] = FORMAT_PY_FILE_DEFAULT_MIN_PYTHON_VERSION, + max_line_length: Annotated[ + int, + arg(help='Fallback max. line length for darker/isort etc., if not defined in .editorconfig'), + ] = FORMAT_PY_FILE_DEFAULT_MAX_LINE_LENGTH, ): """ - Format and check the given python source code file with darker/autoflake/isort/pyupgrade/autopep8/mypy etc. + Format and check the given python source code file with ruff, codespell and mypy. The optional fallback values will be only used, if we can't get them from the project meta files like ".editorconfig" and "pyproject.toml" """ + log_config(verbosity=verbosity, log_in_file=False) format_one_file( default_min_py_version=py_version, default_max_line_length=max_line_length, @@ -358,22 +313,8 @@ def format_file( ) -cli.add_command(format_file) - - -@cli.command() +@app.command def version(): """Print version and exit""" # Pseudo command, because the version always printed on every CLI call ;) sys.exit(0) - - -cli.add_command(version) - - -def main(): - print_version(manageprojects) - - # Execute Click CLI: - cli.name = './cli.py' - cli() diff --git a/manageprojects/cli_app/update_readme_history.py b/manageprojects/cli_app/update_readme_history.py deleted file mode 100644 index cfedcc1..0000000 --- a/manageprojects/cli_app/update_readme_history.py +++ /dev/null @@ -1,27 +0,0 @@ -import sys - -from cli_base.cli_tools import git_history -from cli_base.cli_tools.verbosity import OPTION_KWARGS_VERBOSE, setup_logging -from rich import print # noqa -import rich_click as click - -from manageprojects.cli_app import cli - - -@cli.command() -@click.option('-v', '--verbosity', **OPTION_KWARGS_VERBOSE) -def update_readme_history(verbosity: int): - """ - Update project history base on git commits/tags in README.md - - Will be exited with 1 if the README.md was updated otherwise with 0. - - Also, callable via e.g.: - python -m cli_base update-readme-history -v - """ - setup_logging(verbosity=verbosity) - updated = git_history.update_readme_history(verbosity=verbosity) - exit_code = 1 if updated else 0 - if verbosity: - print(f'{exit_code=}') - sys.exit(exit_code) diff --git a/manageprojects/cli_dev/__init__.py b/manageprojects/cli_dev/__init__.py index e75d1f4..1250310 100644 --- a/manageprojects/cli_dev/__init__.py +++ b/manageprojects/cli_dev/__init__.py @@ -2,6 +2,7 @@ CLI for development """ +from collections.abc import Sequence import importlib import logging import sys @@ -10,9 +11,8 @@ from cli_base.autodiscover import import_all_files from cli_base.cli_tools.dev_tools import run_coverage, run_nox, run_unittest_cli from cli_base.cli_tools.version_info import print_version -import rich_click as click -from rich_click import RichGroup from typeguard import install_import_hook +from tyro.extras import SubcommandApp import manageprojects from manageprojects import constants @@ -33,32 +33,21 @@ assert_is_file(PACKAGE_ROOT / 'pyproject.toml') # Exists only in cloned git repo -class ClickGroup(RichGroup): # FIXME: How to set the "info_name" easier? - def make_context(self, info_name, *args, **kwargs): - info_name = './dev-cli.py' - return super().make_context(info_name, *args, **kwargs) +app = SubcommandApp() -@click.group( - cls=ClickGroup, - epilog=constants.CLI_EPILOG, -) -def cli(): - pass - - -# Register all click commands, just by import all files in this package: +# Register all CLI commands, just by import all files in this package: import_all_files(package=__package__, init_file=__file__) -@cli.command() +@app.command def version(): """Print version and exit""" # Pseudo command, because the version always printed on every CLI call ;) sys.exit(0) -def main(): +def main(args: Sequence[str] | None = None): print_version(manageprojects) if len(sys.argv) >= 2: @@ -72,5 +61,10 @@ def main(): if real_func := command_map.get(command): real_func(argv=sys.argv, exit_after_run=True) - # Execute Click CLI: - cli() + app.cli( + prog='./dev-cli.py', + description=constants.CLI_EPILOG, + use_underscores=False, # use hyphens instead of underscores + sort_subcommands=True, + args=args, + ) diff --git a/manageprojects/cli_dev/code_style.py b/manageprojects/cli_dev/code_style.py index b1759a5..763eb64 100644 --- a/manageprojects/cli_dev/code_style.py +++ b/manageprojects/cli_dev/code_style.py @@ -1,13 +1,11 @@ from cli_base.cli_tools.code_style import assert_code_style -from cli_base.cli_tools.verbosity import OPTION_KWARGS_VERBOSE -import rich_click as click +from cli_base.tyro_commands import TyroVerbosityArgType -from manageprojects.cli_dev import PACKAGE_ROOT, cli +from manageprojects.cli_dev import PACKAGE_ROOT, app -@cli.command() -@click.option('-v', '--verbosity', **OPTION_KWARGS_VERBOSE) -def lint(verbosity: int): +@app.command +def lint(verbosity: TyroVerbosityArgType = 1): """ Check/fix code style by run: "ruff check --fix" """ diff --git a/manageprojects/cli_dev/git_hooks.py b/manageprojects/cli_dev/git_hooks.py index 01be66a..0c43212 100644 --- a/manageprojects/cli_dev/git_hooks.py +++ b/manageprojects/cli_dev/git_hooks.py @@ -1,8 +1,9 @@ -from manageprojects.cli_dev import PACKAGE_ROOT, cli + +from manageprojects.cli_dev import PACKAGE_ROOT, app from manageprojects.format_file import ToolsExecutor -@cli.command() +@app.command def git_hooks(): """ Setup our "pre-commit" git hooks @@ -11,10 +12,13 @@ def git_hooks(): executor.verbose_check_call('pre-commit', 'install') -@cli.command() +@app.command def run_git_hooks(): """ Run the installed "pre-commit" git hooks """ executor = ToolsExecutor(cwd=PACKAGE_ROOT) executor.verbose_check_call('pre-commit', 'run', '--verbose', exit_on_error=True) + + + diff --git a/manageprojects/cli_dev/packaging.py b/manageprojects/cli_dev/packaging.py index efe4164..cc85544 100644 --- a/manageprojects/cli_dev/packaging.py +++ b/manageprojects/cli_dev/packaging.py @@ -1,22 +1,20 @@ import logging -import tempfile -from bx_py_utils.pyproject_toml import get_pyproject_config from cli_base.cli_tools.dev_tools import run_unittest_cli from cli_base.cli_tools.subprocess_utils import ToolsExecutor -from cli_base.cli_tools.verbosity import OPTION_KWARGS_VERBOSE, setup_logging -import click +from cli_base.cli_tools.verbosity import setup_logging +from cli_base.run_pip_audit import run_pip_audit +from cli_base.tyro_commands import TyroVerbosityArgType import manageprojects -from manageprojects.cli_dev import PACKAGE_ROOT, cli -from manageprojects.utilities.publish import build as publish_build +from manageprojects.cli_dev import PACKAGE_ROOT, app from manageprojects.utilities.publish import publish_package logger = logging.getLogger(__name__) -@cli.command() +@app.command def install(): """ Install requirements and 'manageprojects' via pip as editable. @@ -26,65 +24,17 @@ def install(): tools_executor.verbose_check_call('pip', 'install', '--no-deps', '-e', '.') -@cli.command() -@click.option('-v', '--verbosity', **OPTION_KWARGS_VERBOSE) -def build(verbosity: int): - """ - Build the manageproject (More a test of manageprojects.utilities.publish.build) - """ - setup_logging(verbosity=verbosity) - publish_build(PACKAGE_ROOT) - - -def run_pip_audit(verbosity: int): - tools_executor = ToolsExecutor(cwd=PACKAGE_ROOT) - - with tempfile.NamedTemporaryFile(prefix='requirements', suffix='.txt') as temp_file: - tools_executor.verbose_check_call( - 'uv', - 'export', - '--no-header', - '--frozen', - '--no-editable', - '--no-emit-project', - '-o', - temp_file.name, - ) - - config: dict = get_pyproject_config( - section=('tool', 'cli_base', 'pip_audit'), - base_path=PACKAGE_ROOT, - ) - logger.debug('pip_audit config: %r', config) - assert isinstance(config, dict), f'Expected a dict: {config=}' - - popenargs = ['pip-audit', '--strict', '--require-hashes'] - - if verbosity: - popenargs.append(f'-{"v" * verbosity}') - - for vulnerability_id in config.get('ignore-vuln', []): - popenargs.extend(['--ignore-vuln', vulnerability_id]) - - popenargs.extend(['-r', temp_file.name]) - - logger.debug('pip_audit args: %s', popenargs) - tools_executor.verbose_check_call(*popenargs) - - -@cli.command() -@click.option('-v', '--verbosity', **OPTION_KWARGS_VERBOSE) -def pip_audit(verbosity: int): +@app.command +def pip_audit(verbosity: TyroVerbosityArgType): """ Run pip-audit check against current requirements files """ setup_logging(verbosity=verbosity) - run_pip_audit(verbosity=verbosity) + run_pip_audit(base_path=PACKAGE_ROOT, verbosity=verbosity) -@cli.command() -@click.option('-v', '--verbosity', **OPTION_KWARGS_VERBOSE) -def update(verbosity: int): +@app.command +def update(verbosity: TyroVerbosityArgType): """ Update dependencies (uv.lock) and git pre-commit hooks """ @@ -96,7 +46,7 @@ def update(verbosity: int): tools_executor.verbose_check_call('pip', 'install', '-U', 'uv') tools_executor.verbose_check_call('uv', 'lock', '--upgrade') - run_pip_audit(verbosity=verbosity) + run_pip_audit(base_path=PACKAGE_ROOT, verbosity=verbosity) # Install new dependencies in current .venv: tools_executor.verbose_check_call('uv', 'sync') @@ -105,7 +55,7 @@ def update(verbosity: int): tools_executor.verbose_check_call('pre-commit', 'autoupdate') -@cli.command() +@app.command def publish(): """ Build and upload this project to PyPi diff --git a/manageprojects/cli_dev/testing.py b/manageprojects/cli_dev/testing.py index 7bd1a6d..dc95b44 100644 --- a/manageprojects/cli_dev/testing.py +++ b/manageprojects/cli_dev/testing.py @@ -1,26 +1,22 @@ from cli_base.cli_tools.dev_tools import run_coverage, run_nox, run_unittest_cli from cli_base.cli_tools.subprocess_utils import verbose_check_call from cli_base.cli_tools.test_utils.snapshot import UpdateTestSnapshotFiles -from cli_base.cli_tools.verbosity import OPTION_KWARGS_VERBOSE -import rich_click as click +from cli_base.tyro_commands import TyroVerbosityArgType -from manageprojects.cli_dev import PACKAGE_ROOT, cli +from manageprojects.cli_dev import PACKAGE_ROOT, app -@cli.command() -@click.option('-v', '--verbosity', **OPTION_KWARGS_VERBOSE) -def mypy(verbosity: int): +@app.command +def mypy(verbosity: TyroVerbosityArgType): """Run Mypy (configured in pyproject.toml)""" verbose_check_call('mypy', '.', cwd=PACKAGE_ROOT, verbose=verbosity > 0, exit_on_error=True) -@cli.command() -@click.option('-v', '--verbosity', **OPTION_KWARGS_VERBOSE) -def update_test_snapshot_files(verbosity: int): +@app.command +def update_test_snapshot_files(verbosity: TyroVerbosityArgType): """ Update all test snapshot files (by remove and recreate all snapshot files) """ - with UpdateTestSnapshotFiles(root_path=PACKAGE_ROOT, verbose=verbosity > 0): # Just recreate them by running tests: run_unittest_cli( @@ -32,7 +28,7 @@ def update_test_snapshot_files(verbosity: int): ) -@cli.command() # Dummy command +@app.command # Dummy command def test(): """ Run unittests @@ -40,7 +36,7 @@ def test(): run_unittest_cli() -@cli.command() # Dummy command +@app.command # Dummy command def coverage(): """ Run tests and show coverage report. @@ -48,7 +44,7 @@ def coverage(): run_coverage() -@cli.command() # Dummy "nox" command +@app.command # Dummy "nox" command def nox(): """ Run nox diff --git a/manageprojects/cli_dev/update_readme_history.py b/manageprojects/cli_dev/update_readme_history.py index c636221..3aed099 100644 --- a/manageprojects/cli_dev/update_readme_history.py +++ b/manageprojects/cli_dev/update_readme_history.py @@ -3,19 +3,18 @@ import sys from cli_base.cli_tools import git_history -from cli_base.cli_tools.verbosity import OPTION_KWARGS_VERBOSE, setup_logging +from cli_base.cli_tools.verbosity import setup_logging +from cli_base.tyro_commands import TyroVerbosityArgType from rich import print # noqa -import rich_click as click -from manageprojects.cli_dev import cli +from manageprojects.cli_dev import app logger = logging.getLogger(__name__) -@cli.command() -@click.option('-v', '--verbosity', **OPTION_KWARGS_VERBOSE) -def update_readme_history(verbosity: int): +@app.command +def update_readme_history(verbosity: TyroVerbosityArgType): """ Update project history base on git commits/tags in README.md diff --git a/manageprojects/test_utils/click_cli_utils.py b/manageprojects/test_utils/click_cli_utils.py index 25c755d..7a0ca0f 100644 --- a/manageprojects/test_utils/click_cli_utils.py +++ b/manageprojects/test_utils/click_cli_utils.py @@ -1,6 +1,6 @@ import warnings -from cli_base.cli_tools.test_utils.rich_test_utils import NoColorEnvRichClick, NoColorRichClickCli +from cli_base.cli_tools.test_utils.rich_test_utils import NoColorEnvRich import click from click.testing import CliRunner, Result @@ -10,10 +10,12 @@ def subprocess_cli(*, cli_bin, args, exit_on_error=True): warnings.warn( - 'Migrate to: cli_base.cli_tools.test_utils.rich_test_utils.NoColorRichClickCli context manager !', + 'Migrate to: NoColorRichClickCli context manager !', DeprecationWarning, stacklevel=2, ) + from cli_base.cli_tools.test_utils.rich_click_test_utils import NoColorRichClickCli + with NoColorRichClickCli() as cm: stdout = cm.invoke(cli_bin=cli_bin, args=args, exit_on_error=exit_on_error) return stdout @@ -25,11 +27,16 @@ def __init__(self, result: Result): def invoke_click(cli, *args, expected_stderr='', expected_exit_code=0, strip=True, **kwargs): + warnings.warn( + 'Deprecated: Will be removed in future !', + DeprecationWarning, + stacklevel=2, + ) assert isinstance(cli, click.Command) args = tuple([str(arg) for arg in args]) # e.g.: Path() -> str - with NoColorEnvRichClick(width=TERMINAL_WIDTH): + with NoColorEnvRich(width=TERMINAL_WIDTH): runner = CliRunner() result: Result = runner.invoke(cli=cli, args=args, **kwargs, color=False) diff --git a/manageprojects/tests/test_cli.py b/manageprojects/tests/test_cli.py index ed97566..060e24c 100644 --- a/manageprojects/tests/test_cli.py +++ b/manageprojects/tests/test_cli.py @@ -1,15 +1,16 @@ -from pathlib import Path +from pathlib import Path, PosixPath import tempfile from unittest import mock -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch +from bx_py_utils.test_utils.redirect import RedirectOut +from cli_base.cli_tools.test_utils.rich_test_utils import NoColorEnvRich + +from manageprojects import cli_app, cli_dev from manageprojects.cli_app import manage -from manageprojects.cli_app.manage import start_project, update_project -from manageprojects.cli_dev import cli as dev_cli from manageprojects.constants import PY_BIN_PATH from manageprojects.data_classes import CookiecutterResult -from manageprojects.test_utils.click_cli_utils import invoke_click -from manageprojects.test_utils.subprocess import SubprocessCallMock +from manageprojects.test_utils.subprocess import SimpleRunReturnCallback, SubprocessCallMock from manageprojects.tests.base import BaseTestCase @@ -23,26 +24,33 @@ def test_start_project_cli(self): commit_date=None, cookiecutter_context=dict(foo=1, bar=2), ) - with mock.patch.object(manage, 'start_managed_project', MagicMock(return_value=result)) as m: - stdout = invoke_click( - start_project, - 'https://github.com/jedie/cookiecutter_templates/', - '--directory', - 'piptools-python', - 'foobar/', + with ( + NoColorEnvRich(), + mock.patch.object(manage, 'start_managed_project', MagicMock(return_value=result)) as m, + RedirectOut() as buffer, + ): + cli_app.main( + args=( + 'start-project', + 'https://github.com/jedie/cookiecutter_templates/', + '--directory', + 'uv-python', + 'foobar/', + ) ) m.assert_called_once_with( template='https://github.com/jedie/cookiecutter_templates/', - directory='piptools-python', - output_dir=Path('foobar'), checkout=None, - input=True, + output_dir=PosixPath('foobar'), + input=False, replay=False, password=None, + directory='uv-python', config_file=None, ) + self.assertEqual(buffer.stderr, '') self.assert_in_content( - got=stdout, + got=buffer.stdout, parts=( 'https://github.com/jedie/cookiecutter_templates/', 'foobar', @@ -53,8 +61,12 @@ def test_start_project_cli(self): def test_update_project_cli(self): tempdir = tempfile.gettempdir() - with mock.patch.object(manage, 'update_managed_project') as m: - stdout = invoke_click(update_project, tempdir) + with ( + NoColorEnvRich(), + mock.patch.object(manage, 'update_managed_project') as m, + RedirectOut() as buffer, + ): + cli_app.main(args=('update-project', tempdir)) m.assert_called_once_with( project_path=Path(tempdir), @@ -64,8 +76,9 @@ def test_update_project_cli(self): cleanup=True, input=False, ) + self.assertEqual(buffer.stderr, '') self.assert_in_content( - got=stdout, + got=buffer.stdout, parts=( f'Update project: "{tempdir}"...', f'Managed project "{tempdir}" updated, ok.', @@ -73,8 +86,13 @@ def test_update_project_cli(self): ) def test_install(self): - with SubprocessCallMock() as call_mock: - invoke_click(dev_cli, 'install') + with ( + NoColorEnvRich(), + patch('manageprojects.cli_dev.print_version'), + SubprocessCallMock(return_callback=SimpleRunReturnCallback(stdout='')) as call_mock, + RedirectOut() as buffer, + ): + cli_dev.main(args=('install',)) self.assertEqual( call_mock.get_popenargs(rstrip_paths=(PY_BIN_PATH,)), @@ -83,3 +101,11 @@ def test_install(self): ['.../pip', 'install', '--no-deps', '-e', '.'], ], ) + self.assertEqual(buffer.stderr, '') + self.assert_in_content( + got=buffer.stdout, + parts=( + '/manageprojects$ .venv/bin/uv sync', + '/manageprojects$ .venv/bin/pip install --no-deps -e .', + ), + ) diff --git a/manageprojects/tests/test_click_cli_utils.py b/manageprojects/tests/test_click_cli_utils.py deleted file mode 100644 index a259256..0000000 --- a/manageprojects/tests/test_click_cli_utils.py +++ /dev/null @@ -1,21 +0,0 @@ -from unittest.mock import patch - -from click.testing import Result - -from manageprojects.cli_app import cli, manage -from manageprojects.test_utils.click_cli_utils import ClickInvokeCliException, invoke_click -from manageprojects.tests.base import BaseTestCase - - -class CliTestCase(BaseTestCase): - def test_invoke_click(self): - with patch.object(manage, 'format_one_file', side_effect=RuntimeError('Bam!')): - with self.assertRaises(ClickInvokeCliException) as cm: - invoke_click(cli, 'format-file', __file__) - - self.assertIsInstance(cm.exception, ClickInvokeCliException) - self.assertIsInstance(cm.exception.result, Result) - self.assertEqual(cm.exception.result.stdout, '') - self.assertEqual(cm.exception.result.stderr, '') - self.assertIsInstance(cm.exception.result.exception, RuntimeError) - self.assertEqual(cm.exception.result.exception.args, ('Bam!',)) diff --git a/manageprojects/tests/test_cookiecutter_templates.py b/manageprojects/tests/test_cookiecutter_templates.py index 93d35a2..fdbc194 100644 --- a/manageprojects/tests/test_cookiecutter_templates.py +++ b/manageprojects/tests/test_cookiecutter_templates.py @@ -4,14 +4,14 @@ from bx_py_utils.path import assert_is_dir, assert_is_file from bx_py_utils.test_utils.datetime import parse_dt +from bx_py_utils.test_utils.redirect import RedirectOut from cli_base.cli_tools.test_utils.git_utils import init_git from cli_base.cli_tools.test_utils.logs import AssertLogs import yaml -from manageprojects.cli_app.manage import clone_project +from manageprojects import cli_app from manageprojects.cookiecutter_templates import start_managed_project, update_managed_project from manageprojects.data_classes import CookiecutterResult, GenerateTemplatePatchResult, ManageProjectsMeta -from manageprojects.test_utils.click_cli_utils import invoke_click from manageprojects.tests.base import BaseTestCase from manageprojects.utilities.pyproject_toml import PyProjectToml from manageprojects.utilities.temp_path import TemporaryDirectory @@ -111,15 +111,16 @@ def test_start_managed_project(self): # Test clone a existing project cloned_path = main_temp_path / 'cloned_project' - with AssertLogs(self) as logs: - output = invoke_click(clone_project, project_path, cloned_path) - logs.assert_in( - 'Read existing pyproject.toml', - "Call 'cookiecutter'", - 'Create new pyproject.toml', - ) + with AssertLogs(self) as logs, RedirectOut() as buffer: + cli_app.main(args=('clone-project', str(project_path), str(cloned_path))) + logs.assert_in( + 'Read existing pyproject.toml', + "Call 'cookiecutter'", + 'Create new pyproject.toml', + ) + self.assertEqual(buffer.stderr, '') end_path = cloned_path / 'a_dir_name' - self.assert_in_content(got=output, parts=(f'{project_path} successfully cloned to {end_path}',)) + self.assert_in_content(got=buffer.stdout, parts=(f'{project_path} successfully cloned to {end_path}',)) # pyproject.toml created? with AssertLogs(self, loggers=('manageprojects',)) as logs: diff --git a/manageprojects/tests/test_readme.py b/manageprojects/tests/test_readme.py index fb10137..09b842b 100644 --- a/manageprojects/tests/test_readme.py +++ b/manageprojects/tests/test_readme.py @@ -1,20 +1,28 @@ from bx_py_utils.auto_doc import assert_readme_block from bx_py_utils.path import assert_is_file +from cli_base.cli_tools.test_utils.rich_test_utils import NoColorEnvRich, invoke from manageprojects import constants -from manageprojects.cli_app import cli from manageprojects.cli_dev import PACKAGE_ROOT -from manageprojects.cli_dev import cli as dev_cli -from manageprojects.test_utils.click_cli_utils import invoke_click from manageprojects.tests.base import BaseTestCase +def remove_until_usage(text: str) -> str: + lines = text.splitlines() + for idx, line in enumerate(lines): + if line.startswith('usage: '): + return '\n'.join(lines[idx:]) + raise ValueError('No usage line found') + + def assert_cli_help_in_readme(text_block: str, marker: str): README_PATH = PACKAGE_ROOT / 'README.md' assert_is_file(README_PATH) text_block = text_block.replace(constants.CLI_EPILOG, '') + text_block = remove_until_usage(text_block) text_block = f'```\n{text_block.strip()}\n```' + assert_readme_block( readme_path=README_PATH, text_block=text_block, @@ -25,26 +33,29 @@ def assert_cli_help_in_readme(text_block: str, marker: str): class ReadmeTestCase(BaseTestCase): def test_main_help(self): - stdout = invoke_click(cli, '--help') + with NoColorEnvRich(): + stdout = invoke(cli_bin=PACKAGE_ROOT / 'cli.py', args=['--help'], strip_line_prefix='usage: ') self.assert_in_content( got=stdout, parts=( - 'Usage: ./cli.py [OPTIONS] COMMAND [ARGS]...', - ' start-project ', - ' update-project ', + 'usage: ./cli.py [-h]', + ' version ', + 'Print version and exit', constants.CLI_EPILOG, ), ) assert_cli_help_in_readme(text_block=stdout, marker='main help') def test_dev_help(self): - stdout = invoke_click(dev_cli, '--help') + with NoColorEnvRich(): + stdout = invoke(cli_bin=PACKAGE_ROOT / 'dev-cli.py', args=['--help'], strip_line_prefix='usage: ') self.assert_in_content( got=stdout, parts=( - 'Usage: ./dev-cli.py [OPTIONS] COMMAND [ARGS]...', + 'usage: ./dev-cli.py [-h]', ' lint ', ' coverage ', + ' update-readme-history ', ' publish ', constants.CLI_EPILOG, ), @@ -52,37 +63,40 @@ def test_dev_help(self): assert_cli_help_in_readme(text_block=stdout, marker='dev help') def test_start_project_help(self): - stdout = invoke_click(cli, 'start-project', '--help') + with NoColorEnvRich(): + stdout = invoke(cli_bin=PACKAGE_ROOT / 'cli.py', args=('start-project', '--help')) self.assert_in_content( got=stdout, parts=( - 'Usage: ./cli.py start-project [OPTIONS] TEMPLATE OUTPUT_DIR', - '--directory', - '--input/--no-input', + 'usage: ./cli.py start-project [-h] ', + ' --directory ', + ' --input, --no-input ', ), ) assert_cli_help_in_readme(text_block=stdout, marker='start-project help') def test_update_project_help(self): - stdout = invoke_click(cli, 'update-project', '--help') + with NoColorEnvRich(): + stdout = invoke(cli_bin=PACKAGE_ROOT / 'cli.py', args=('update-project', '--help')) self.assert_in_content( got=stdout, parts=( - 'Usage: ./cli.py update-project [OPTIONS] PROJECT_PATH', - '--input/--no-input', - '--cleanup/--no-cleanup', + 'usage: ./cli.py update-project [-h] ', + ' --input, --no-input ', + ' --cleanup, --no-cleanup ', ), ) assert_cli_help_in_readme(text_block=stdout, marker='update-project help') def test_format_file_help(self): - stdout = invoke_click(cli, 'format-file', '--help') + with NoColorEnvRich(): + stdout = invoke(cli_bin=PACKAGE_ROOT / 'cli.py', args=('format-file', '--help')) self.assert_in_content( got=stdout, parts=( - 'Usage: ./cli.py format-file [OPTIONS] FILE_PATH', - '--py-version', - '--max-line-length', + 'usage: ./cli.py format-file [-h] ', + ' --py-version ', + ' --max-line-length ', ), ) assert_cli_help_in_readme(text_block=stdout, marker='format-file help') diff --git a/manageprojects/utilities/log_utils.py b/manageprojects/utilities/log_utils.py index fa1efe8..48430ab 100644 --- a/manageprojects/utilities/log_utils.py +++ b/manageprojects/utilities/log_utils.py @@ -3,6 +3,8 @@ import tempfile from bx_py_utils.test_utils.log_utils import RaiseLogUsage +from cli_base.tyro_commands import TyroVerbosityArgType +from rich import get_console def logger_setup(*, logger_name, level, format, log_filename, raise_log_output): @@ -28,11 +30,22 @@ def print_log_info(filename): def log_config( - level=logging.DEBUG, - format='%(asctime)s %(levelname)s %(name)s.%(funcName)s %(lineno)d | %(message)s', - log_in_file=True, + verbosity: TyroVerbosityArgType, + log_in_file=False, raise_log_output=False, ): + log_format = '%(message)s' + if verbosity == 0: + level = logging.ERROR + elif verbosity == 1: + level = logging.WARNING + elif verbosity == 2: + level = logging.INFO + log_format = '(%(name)s) %(message)s' + else: + level = logging.DEBUG + log_format = '%(asctime)s %(levelname)s %(name)s.%(funcName)s %(lineno)d | %(message)s' + if log_in_file: log_file = tempfile.NamedTemporaryFile( prefix='manageprojects_', suffix='.log', delete=False @@ -41,17 +54,20 @@ def log_config( else: log_filename = None + console = get_console() + console.print(f'(Set log level {verbosity}: {logging.getLevelName(level)})', justify='right') + logger_setup( logger_name='manageprojects', level=level, - format=format, + format=log_format, log_filename=log_filename, raise_log_output=raise_log_output, ) logger_setup( logger_name='cookiecutter', level=level, - format=format, + format=log_format, log_filename=log_filename, raise_log_output=raise_log_output, ) diff --git a/pyproject.toml b/pyproject.toml index f2d47d5..e272791 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,9 +20,8 @@ dependencies = [ "codespell", # https://github.com/codespell-project/codespell "mypy", # https://github.com/python/mypy - "cli-base-utilities>=0.22.0", # https://github.com/jedie/cli-base-utilities - "click", # https://github.com/pallets/click/ - "rich-click", # https://github.com/ewels/rich-click + "cli-base-utilities>=0.23.0", # https://github.com/jedie/cli-base-utilities + "tyro", # https://github.com/brentyi/tyro "rich", # https://github.com/Textualize/rich ] @@ -90,7 +89,6 @@ exclude = [ ".*/", ] - [tool.ruff.lint] preview = true # Needed for some of the rules extend-select = [ @@ -103,7 +101,7 @@ ignore = [ ] [tool.ruff.lint.isort] -# https://docs.astral.sh/ruff/rules/#isort-i +# https://docs.astral.sh/ruff/settings/#lintisort lines-after-imports = 2 force-sort-within-sections = true @@ -167,6 +165,7 @@ applied_migrations = [ "56c3caa", # 2024-09-22T16:52:30+02:00 "ff48b81", # 2024-11-09T19:08:01+01:00 "38129ec", # 2025-02-12T17:41:38+01:00 + "9c976dd", # 2025-09-04T16:33:51+02:00 ] [manageprojects.cookiecutter_context.cookiecutter] diff --git a/uv.lock b/uv.lock index 7519dd2..c4c48f7 100644 --- a/uv.lock +++ b/uv.lock @@ -205,20 +205,19 @@ wheels = [ [[package]] name = "cli-base-utilities" -version = "0.22.0" +version = "0.23.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bx-py-utils" }, - { name = "click" }, { name = "packaging" }, { name = "python-dateutil" }, { name = "rich" }, { name = "tomlkit" }, { name = "tyro" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/cc/9d2d5953cf6f79a87d59c7ac8503bfb3cf330e7acf3c7a6efe901638b5fa/cli_base_utilities-0.22.0.tar.gz", hash = "sha256:5b4ef9c36587185a00b4a09c4f46bfc6f12ce57386dff1e7b9d62b0755c7ab0b", size = 123735, upload-time = "2025-09-03T07:20:42.021Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/e1/df2185db43a81a5dfe4aec21fba1adf14bc150d81d4fcc8801c673f2e5c6/cli_base_utilities-0.23.0.tar.gz", hash = "sha256:631838b4b1689b9bb9cfc3728addfb2b0645e578a347ff76db17e1e0cd54f9b0", size = 124171, upload-time = "2025-09-04T15:19:00.453Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/b6/f7f9e8683d2f7537b3338a59ee4c5733a0337e39ad6011495be512020fef/cli_base_utilities-0.22.0-py3-none-any.whl", hash = "sha256:7243bf7c1590102f0de8b30b61f533d84608951504e1bccf80321511a849390b", size = 85727, upload-time = "2025-09-03T07:20:40.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/c7/0b1e41defe8ecf60ebe85d786e51d6f8d9e7d4d32f2e643e683f3bddc0a3/cli_base_utilities-0.23.0-py3-none-any.whl", hash = "sha256:b464f08374f598cd44b0ffc9e0a059585b155540960f26e0e1894e906d6b59f8", size = 87035, upload-time = "2025-09-04T15:18:59.074Z" }, ] [[package]] @@ -620,15 +619,14 @@ name = "manageprojects" source = { editable = "." } dependencies = [ { name = "cli-base-utilities" }, - { name = "click" }, { name = "codespell" }, { name = "cookiecutter" }, { name = "editorconfig" }, { name = "mypy" }, { name = "rich" }, - { name = "rich-click" }, { name = "ruff" }, { name = "tomlkit" }, + { name = "tyro" }, ] [package.dev-dependencies] @@ -650,16 +648,15 @@ dev = [ [package.metadata] requires-dist = [ - { name = "cli-base-utilities", specifier = ">=0.22.0" }, - { name = "click" }, + { name = "cli-base-utilities", specifier = ">=0.23.0" }, { name = "codespell" }, { name = "cookiecutter", specifier = ">=2.4.0" }, { name = "editorconfig" }, { name = "mypy" }, { name = "rich" }, - { name = "rich-click" }, { name = "ruff" }, { name = "tomlkit" }, + { name = "tyro" }, ] [package.metadata.requires-dev] @@ -1195,20 +1192,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, ] -[[package]] -name = "rich-click" -version = "1.8.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "rich" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/a8/dcc0a8ec9e91d76ecad9413a84b6d3a3310c6111cfe012d75ed385c78d96/rich_click-1.8.9.tar.gz", hash = "sha256:fd98c0ab9ddc1cf9c0b7463f68daf28b4d0033a74214ceb02f761b3ff2af3136", size = 39378, upload-time = "2025-05-19T21:33:05.569Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/c2/9fce4c8a9587c4e90500114d742fe8ef0fd92d7bad29d136bb9941add271/rich_click-1.8.9-py3-none-any.whl", hash = "sha256:c3fa81ed8a671a10de65a9e20abf642cfdac6fdb882db1ef465ee33919fbcfe2", size = 36082, upload-time = "2025-05-19T21:33:04.195Z" }, -] - [[package]] name = "ruff" version = "0.12.11" @@ -1389,28 +1372,28 @@ wheels = [ [[package]] name = "uv" -version = "0.8.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e2/b0/c3bc06ba5f6b72ba3ad278e854292d81b7aaaea2b6988e40fdb892f813f8/uv-0.8.14.tar.gz", hash = "sha256:7c68e0cde3d048500c073696881c07c2bd97503fc77d7091e1454d3fd58febb4", size = 3543853, upload-time = "2025-08-28T21:55:59.769Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/a3/bf0a80a7770f5c11a735345073fdf085a031ecd0525ae229ceb3ed7496f5/uv-0.8.14-py3-none-linux_armv6l.whl", hash = "sha256:bae6621a72e6643f140c4e62f10d3a52d210ccdec48bf4f733e6a25d5739e533", size = 18810682, upload-time = "2025-08-28T21:55:07.027Z" }, - { url = "https://files.pythonhosted.org/packages/61/de/e8d3c1669edb70ae165ad6c06598ff237ddbc1dc743cc590a2c30c245b93/uv-0.8.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2334945ef3dba395067164c7e25b0c1420d8fdab9637d33cb753b5dbe0499b2c", size = 18939300, upload-time = "2025-08-28T21:55:11.244Z" }, - { url = "https://files.pythonhosted.org/packages/dc/61/9e4c3382f79cef69229f4f301ce1b391121f5a9d1015dd82487e08f0d718/uv-0.8.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9a65096847d3341713be92e98cb35d5315d172690032405e8ae4e1b0c366a19a", size = 17555624, upload-time = "2025-08-28T21:55:14.107Z" }, - { url = "https://files.pythonhosted.org/packages/03/6d/5200cba528844e33586fadae78c06c054774e7702063356795f6cc124331/uv-0.8.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:f7a5d72e4fefae57f675cf0ac0adb9e68fb638f3f95be142b7f072fc6fddfe3e", size = 18151749, upload-time = "2025-08-28T21:55:16.904Z" }, - { url = "https://files.pythonhosted.org/packages/5a/b6/6f9407a792f0ca566b61276cadbffa032cff4039847ac77c47959151f753/uv-0.8.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:935b602d40f0c6a41337de81a02850d6892b0c8c6b5d98543fa229d5bb247364", size = 18472626, upload-time = "2025-08-28T21:55:19.994Z" }, - { url = "https://files.pythonhosted.org/packages/14/a2/2eadfccb1d6aa3672c947071b18c50cee41bdb9c9dba6d8af011a5c44e50/uv-0.8.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34286de8d1244f06124c5bd7b4bfb5ef5791c147e0aa4473c7856c02fedc58ff", size = 19292728, upload-time = "2025-08-28T21:55:22.441Z" }, - { url = "https://files.pythonhosted.org/packages/b6/db/96071cddd37e4bfc9bd10c4daab0942c3d610da92f32c74de07621990455/uv-0.8.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d26ea49a595992bc58d31bb6a10660a8015d902b6845c8ceed1e011866013593", size = 20577332, upload-time = "2025-08-28T21:55:25.774Z" }, - { url = "https://files.pythonhosted.org/packages/c9/4c/8e0da19b4bd5612bd782a82a1869c71e8ea059b59c547230146d36583a39/uv-0.8.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2aa721841812e9a74cad883dbd0f6cf908309cc40a86ab33d3576a8b369595a9", size = 20317704, upload-time = "2025-08-28T21:55:28.537Z" }, - { url = "https://files.pythonhosted.org/packages/1c/f2/4ad6abe850e31663d3971eb4af4a3b6ef216870f4f2115ae65e72917ea02/uv-0.8.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5088fa0ceff698a3fb2464f5cd7ebb4af59aa85db4ba83150d4c3af027251228", size = 19615504, upload-time = "2025-08-28T21:55:31.695Z" }, - { url = "https://files.pythonhosted.org/packages/ed/6c/b86f5f2f5aeebb0028034ea180399af23c8cbc42748bba0672c9cabdde38/uv-0.8.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3853202f4eb0bedbe31b0b62b1323521e97306f44f8f4b6ed4bb13b636797873", size = 19605107, upload-time = "2025-08-28T21:55:34.33Z" }, - { url = "https://files.pythonhosted.org/packages/b4/04/7b019c63d26d296bf6dfd8ad9b86e51f84b2ec7f37d68f8b93138a3fa404/uv-0.8.14-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e45047a89592a5b38c88caa6da5d1b70a05c9762ff1c5100f9700f85f533dc99", size = 18412515, upload-time = "2025-08-28T21:55:37.185Z" }, - { url = "https://files.pythonhosted.org/packages/59/b8/c277b6ff1e4fc6d2c4f000ebccef9c2879603875ab092390f7073b911bdf/uv-0.8.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72971573f21e617267b3737750cdb8a9ae99862b06d23df7fde60fc9f8ef78d6", size = 19290057, upload-time = "2025-08-28T21:55:39.769Z" }, - { url = "https://files.pythonhosted.org/packages/ed/09/59f84ea996bc3bf52c88bc7ba2d988bc5edfd7d0a9aee7cc0500f77d83ce/uv-0.8.14-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:ab22d9712f6b06b04359cfaf625722a81fcd0f2335868738dbee26a79a93bd99", size = 18433918, upload-time = "2025-08-28T21:55:42.262Z" }, - { url = "https://files.pythonhosted.org/packages/fa/2c/8a76455ea1f578fab8a88457c4d50c28928860335d3420956b75661f5e7b/uv-0.8.14-py3-none-musllinux_1_1_i686.whl", hash = "sha256:b5003c30c44065b70e03f083d73af45c094f1f96d9c394acafd8f547c2aee4d0", size = 18800856, upload-time = "2025-08-28T21:55:44.697Z" }, - { url = "https://files.pythonhosted.org/packages/f7/87/16699c592d816325554702d771024fbe5ec39127bfbc06d5cb54843673bb/uv-0.8.14-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:dacfad1193c7facd3a414cc2f3468b4a79a07c565c776a3136f97527a628b960", size = 19704752, upload-time = "2025-08-28T21:55:47.375Z" }, - { url = "https://files.pythonhosted.org/packages/ce/e9/0cdeed22e6c540db493ea364040b17af09fabaa7a56c8ff02b9152819442/uv-0.8.14-py3-none-win32.whl", hash = "sha256:0a4abb2a327e3709ef02765dc392ee10e204275bdb107b492977f88633a1e6b0", size = 18630132, upload-time = "2025-08-28T21:55:51.988Z" }, - { url = "https://files.pythonhosted.org/packages/45/5e/9bf7004bd53e9279265d73a131fe2a6c7d74c1125c53e805b5e9f4047f37/uv-0.8.14-py3-none-win_amd64.whl", hash = "sha256:5091d588753bbbd1f120f13311ede2ae113d7ec2760e149fc502a237f2516075", size = 20672637, upload-time = "2025-08-28T21:55:55.341Z" }, - { url = "https://files.pythonhosted.org/packages/d8/7f/41074c81faa36a34d44524997c345a857bd82d7f73ea60e24dca606306ec/uv-0.8.14-py3-none-win_arm64.whl", hash = "sha256:7c424fd4561f4528d8b52fc8c16991d0ad0000d3ad12c82e01e722f314b2669d", size = 19171656, upload-time = "2025-08-28T21:55:57.799Z" }, +version = "0.8.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/7c/ab905b0425f88842f3d8e5da50491524f45a231b7a3dc9c988608162adb2/uv-0.8.15.tar.gz", hash = "sha256:8ea57b78be9f0911a2a50b6814d15aec7d1f8aa6517059dc8250b1414156f93a", size = 3602914, upload-time = "2025-09-03T14:32:15.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/1d/794352a01b40f2b0a0abe02f4f020219b3f59ee6ed900561be3b2b47a82b/uv-0.8.15-py3-none-linux_armv6l.whl", hash = "sha256:f02e6b8be08b840f86b8d5997b658b657acdda95bc216ecf62fce6c71414bdc7", size = 20136396, upload-time = "2025-09-03T14:31:30.404Z" }, + { url = "https://files.pythonhosted.org/packages/8f/89/528f01cff01eb8d10dd396f437656266443e399dda2fe4787b2cf6983698/uv-0.8.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b0461bb1ad616c8bcb59c9b39ae9363245ca33815ebb1d11130385236eca21b9", size = 19297422, upload-time = "2025-09-03T14:31:34.412Z" }, + { url = "https://files.pythonhosted.org/packages/94/03/532af32a64d162894a1daebb7bc5028ba00225ea720cf0f287e934dc2bd5/uv-0.8.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:069eed78b79d1e88bced23e3d4303348edb0a0209e7cae0f20024c42430bf50f", size = 17882409, upload-time = "2025-09-03T14:31:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/25/21/57df6d53fbadfa947d9d65a0926e5d8540199f49aa958d23be2707262a80/uv-0.8.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:333a93bb6af64f3b95ee99e82b4ea227e2af6362c45f91c89a24e2bfefb628f9", size = 19557216, upload-time = "2025-09-03T14:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/68/22/c3784749e1c78119e5375ec34c6ea29e944192a601f17c746339611db237/uv-0.8.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7d5b19ac2bdda3d1456b5d6013af50b443ffb0e40c66d42874f71190a5364711", size = 19781097, upload-time = "2025-09-03T14:31:42.314Z" }, + { url = "https://files.pythonhosted.org/packages/00/28/0597599fb35408dd73e0a7d25108dca1fa6ce8f8d570c8f24151b0016eef/uv-0.8.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3330bb4f206a6180679a75a8b2e77ff0f933fcb06c028b6f4da877b10a5e4f95", size = 20741549, upload-time = "2025-09-03T14:31:44.574Z" }, + { url = "https://files.pythonhosted.org/packages/4f/61/98fa07981722660f5a3c28b987df99c2486f63d01b1256e6cca05a43bdce/uv-0.8.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:de9896ad4fa724ab317a8048f4891b9b23df1403b3724e96606f3be2dbbbf009", size = 22193727, upload-time = "2025-09-03T14:31:46.915Z" }, + { url = "https://files.pythonhosted.org/packages/fa/65/523188e11a759144b00f0fe48943f6d00706fcd9b5f561a54a07b9fd4541/uv-0.8.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:226360003e71084e0a73cbec72170e88634b045e95529654d067ea3741bba242", size = 21817550, upload-time = "2025-09-03T14:31:49.548Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/7898acf3d9ed2d3a2986cccc8209c14d3e9ac72dfaa616e49d329423b1d3/uv-0.8.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9488260536b35b94a79962fea76837f279c0cd0ae5021c761e66b311f47ffa70", size = 21024011, upload-time = "2025-09-03T14:31:51.789Z" }, + { url = "https://files.pythonhosted.org/packages/13/fc/e0da45ee179367dcc1e1040ad00ed8a99b78355d43024b0b5fc2edf5c389/uv-0.8.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07765f99fd5fd3b257d7e210e8d0844c0a8fd111612e31fcca66a85656cc728e", size = 21009338, upload-time = "2025-09-03T14:31:54.104Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5d/180904fa7ed49081b27f00e86f7220ca62cc098d7ef6459f0c69a8ae8f74/uv-0.8.15-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:c4868e6a4e1a8c777a5ba3cff452c405837318fb0b272ff203bfda0e1b8fc54d", size = 19799578, upload-time = "2025-09-03T14:31:56.47Z" }, + { url = "https://files.pythonhosted.org/packages/b6/09/fed823212e695b6765bdb8462850abffbe685cd965c4de905efed5e2e5c9/uv-0.8.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3ec78a54a8eb0bbb9a9c653982390af84673657c8a48a0be6cdcb81d7d3e95c3", size = 20845428, upload-time = "2025-09-03T14:31:59.475Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f3/9c4211897c00f79b7973a10800166e0580eaad20fe27f7c06adb7b248ac7/uv-0.8.15-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:a022a752d20da80d2a49fc0721522a81e3a32efe539152d756d84ebdba29dbc3", size = 19728113, upload-time = "2025-09-03T14:32:01.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/43/4ec6047150e2fba494d80d36b881a1a973835afa497ae9ccdf51828cae4f/uv-0.8.15-py3-none-musllinux_1_1_i686.whl", hash = "sha256:3780d2f3951d83e55812fdeb7eee233787b70c774497dbfc55b0fdf6063aa345", size = 20169115, upload-time = "2025-09-03T14:32:03.995Z" }, + { url = "https://files.pythonhosted.org/packages/ea/98/b4220bf462fb225c4a2d74ef4f105020238472b4b0da94ebc17a310d7b4e/uv-0.8.15-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:56f2451c9193ee1754ce1d8390ded68e9cb8dee0aaf7e2f38a9bd04d99be1be7", size = 21129804, upload-time = "2025-09-03T14:32:06.204Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b8/40ce3d385254ac87a664a5d9a4664fac697e2734352f404382b81d03235b/uv-0.8.15-py3-none-win32.whl", hash = "sha256:89c7c10089e07d944c72d388fd88666c650dec2f8c79ca541e365f32843882c6", size = 19077103, upload-time = "2025-09-03T14:32:08.628Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9d/081a0af395c0e307c0c930e80161a2aa551c25064cfb636d060574566fa4/uv-0.8.15-py3-none-win_amd64.whl", hash = "sha256:6aa824ab933dfafe11efe32e6541c6bcd65ecaa927e8e834ea6b14d3821020f6", size = 21179816, upload-time = "2025-09-03T14:32:11.42Z" }, + { url = "https://files.pythonhosted.org/packages/30/47/d8f50264a8c8ebbb9a44a8fed08b6e873d943adf299d944fe3a776ff5fbf/uv-0.8.15-py3-none-win_arm64.whl", hash = "sha256:a395fa1fc8948eacdd18e4592ed489fad13558b13fea6b3544cb16e5006c5b02", size = 19448833, upload-time = "2025-09-03T14:32:13.639Z" }, ] [[package]] From 80473e8f627513210877b6efb37a4eadab89fc61 Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Thu, 4 Sep 2025 18:29:02 +0200 Subject: [PATCH 2/3] ruff: remove "force-sort-within-sections = true" (Just use the default) --- README.md | 1 + cli.py | 2 +- dev-cli.py | 2 +- manageprojects/cli_app/__init__.py | 2 +- manageprojects/cli_app/manage.py | 2 +- manageprojects/cli_dev/__init__.py | 2 +- manageprojects/cli_dev/update_readme_history.py | 2 +- manageprojects/constants.py | 2 +- manageprojects/cookiecutter_generator.py | 2 +- manageprojects/cookiecutter_templates.py | 2 +- manageprojects/format_file.py | 2 +- manageprojects/install_python.py | 2 +- manageprojects/overwrite.py | 2 +- manageprojects/patching.py | 2 +- manageprojects/setup_python.py | 2 +- manageprojects/test_utils/click_cli_utils.py | 2 +- manageprojects/test_utils/project_setup.py | 2 +- manageprojects/test_utils/subprocess.py | 2 +- manageprojects/test_utils/temp_utils.py | 2 +- manageprojects/tests/__init__.py | 2 +- manageprojects/tests/base.py | 4 ++-- manageprojects/tests/docwrite_macros_install_python.py | 4 ++-- manageprojects/tests/test_cli.py | 2 +- manageprojects/tests/test_cookiecutter_templates.py | 2 +- manageprojects/tests/test_doc_write.py | 2 +- manageprojects/tests/test_install_python.py | 2 +- manageprojects/tests/test_setup_python.py | 2 +- manageprojects/tests/test_utilities_publish.py | 2 +- manageprojects/utilities/code_style.py | 2 +- manageprojects/utilities/gitignore.py | 2 +- manageprojects/utilities/publish.py | 6 +++--- manageprojects/utilities/pyproject_toml.py | 2 +- manageprojects/utilities/temp_path.py | 2 +- pyproject.toml | 1 - 34 files changed, 37 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 8143f0c..6cc2240 100644 --- a/README.md +++ b/README.md @@ -381,6 +381,7 @@ See also git tags: https://github.com/jedie/manageprojects/tags [comment]: <> (✂✂✂ auto generated history start ✂✂✂) * [v0.23.0](https://github.com/jedie/manageprojects/compare/v0.22.1...v0.23.0) + * 2025-09-04 - ruff: remove "force-sort-within-sections = true" (Just use the default) * 2024-11-21 - Switch from click to tyro * [v0.22.1](https://github.com/jedie/manageprojects/compare/v0.22.0...v0.22.1) * 2025-09-03 - Make install/setup python script executeable after renew the code diff --git a/cli.py b/cli.py index 2ee24af..1bb9894 100755 --- a/cli.py +++ b/cli.py @@ -9,11 +9,11 @@ import hashlib import os -from pathlib import Path import shlex import subprocess import sys import venv +from pathlib import Path def print_no_pip_error(): diff --git a/dev-cli.py b/dev-cli.py index d54b929..a48716f 100755 --- a/dev-cli.py +++ b/dev-cli.py @@ -8,11 +8,11 @@ """ import hashlib -from pathlib import Path import shlex import subprocess import sys import venv +from pathlib import Path def print_no_pip_error(): diff --git a/manageprojects/cli_app/__init__.py b/manageprojects/cli_app/__init__.py index d8701f0..a3f689d 100644 --- a/manageprojects/cli_app/__init__.py +++ b/manageprojects/cli_app/__init__.py @@ -2,9 +2,9 @@ CLI for usage """ -from collections.abc import Sequence import logging import sys +from collections.abc import Sequence from cli_base.autodiscover import import_all_files from cli_base.cli_tools.version_info import print_version diff --git a/manageprojects/cli_app/manage.py b/manageprojects/cli_app/manage.py index b800d25..1bc59fb 100644 --- a/manageprojects/cli_app/manage.py +++ b/manageprojects/cli_app/manage.py @@ -5,10 +5,10 @@ from __future__ import annotations import logging -from pathlib import Path import shutil import subprocess import sys +from pathlib import Path from typing import Annotated from bx_py_utils.path import assert_is_dir diff --git a/manageprojects/cli_dev/__init__.py b/manageprojects/cli_dev/__init__.py index 1250310..37c1127 100644 --- a/manageprojects/cli_dev/__init__.py +++ b/manageprojects/cli_dev/__init__.py @@ -2,10 +2,10 @@ CLI for development """ -from collections.abc import Sequence import importlib import logging import sys +from collections.abc import Sequence from bx_py_utils.path import assert_is_file from cli_base.autodiscover import import_all_files diff --git a/manageprojects/cli_dev/update_readme_history.py b/manageprojects/cli_dev/update_readme_history.py index 3aed099..5248d56 100644 --- a/manageprojects/cli_dev/update_readme_history.py +++ b/manageprojects/cli_dev/update_readme_history.py @@ -1,6 +1,6 @@ import logging -from pathlib import Path import sys +from pathlib import Path from cli_base.cli_tools import git_history from cli_base.cli_tools.verbosity import setup_logging diff --git a/manageprojects/constants.py b/manageprojects/constants.py index a0924f2..ad6d29d 100644 --- a/manageprojects/constants.py +++ b/manageprojects/constants.py @@ -1,5 +1,5 @@ -from pathlib import Path import sys +from pathlib import Path import manageprojects diff --git a/manageprojects/cookiecutter_generator.py b/manageprojects/cookiecutter_generator.py index 5731556..6693202 100644 --- a/manageprojects/cookiecutter_generator.py +++ b/manageprojects/cookiecutter_generator.py @@ -1,7 +1,7 @@ from __future__ import annotations -from pathlib import Path import shutil +from pathlib import Path from bx_py_utils.path import assert_is_dir from cli_base.cli_tools.git import Git diff --git a/manageprojects/cookiecutter_templates.py b/manageprojects/cookiecutter_templates.py index 8842ce9..36ff739 100644 --- a/manageprojects/cookiecutter_templates.py +++ b/manageprojects/cookiecutter_templates.py @@ -1,9 +1,9 @@ from __future__ import annotations import logging -from pathlib import Path import subprocess import sys +from pathlib import Path from cli_base.cli_tools.git import Git from rich import print as rprint diff --git a/manageprojects/format_file.py b/manageprojects/format_file.py index 6428e92..039806d 100644 --- a/manageprojects/format_file.py +++ b/manageprojects/format_file.py @@ -1,7 +1,7 @@ import dataclasses import logging -from pathlib import Path import subprocess +from pathlib import Path from bx_py_utils.dict_utils import dict_get from cli_base.cli_tools.git import Git, GitError, NoGitRepoError diff --git a/manageprojects/install_python.py b/manageprojects/install_python.py index ea79f4c..8a7f1a6 100644 --- a/manageprojects/install_python.py +++ b/manageprojects/install_python.py @@ -19,7 +19,6 @@ import hashlib import logging import os -from pathlib import Path import re import shlex import shutil @@ -28,6 +27,7 @@ import sys import tempfile import urllib.request +from pathlib import Path """DocWrite: install_python.md # Install Python Interpreter diff --git a/manageprojects/overwrite.py b/manageprojects/overwrite.py index 226cf28..e2e3b9e 100644 --- a/manageprojects/overwrite.py +++ b/manageprojects/overwrite.py @@ -1,8 +1,8 @@ import filecmp import logging -from pathlib import Path import shutil import sys +from pathlib import Path from bx_py_utils.path import assert_is_dir from cli_base.cli_tools.git import Git diff --git a/manageprojects/patching.py b/manageprojects/patching.py index ae324db..b582526 100644 --- a/manageprojects/patching.py +++ b/manageprojects/patching.py @@ -1,6 +1,6 @@ import logging -from pathlib import Path import shutil +from pathlib import Path from bx_py_utils.path import assert_is_dir from cli_base.cli_tools.git import Git diff --git a/manageprojects/setup_python.py b/manageprojects/setup_python.py index a225e3f..f597b17 100644 --- a/manageprojects/setup_python.py +++ b/manageprojects/setup_python.py @@ -16,7 +16,6 @@ import hashlib import json import logging -from pathlib import Path import platform import re import shlex @@ -26,6 +25,7 @@ import sys import tempfile import time +from pathlib import Path from urllib import request diff --git a/manageprojects/test_utils/click_cli_utils.py b/manageprojects/test_utils/click_cli_utils.py index 7a0ca0f..f6ae671 100644 --- a/manageprojects/test_utils/click_cli_utils.py +++ b/manageprojects/test_utils/click_cli_utils.py @@ -1,7 +1,7 @@ import warnings -from cli_base.cli_tools.test_utils.rich_test_utils import NoColorEnvRich import click +from cli_base.cli_tools.test_utils.rich_test_utils import NoColorEnvRich from click.testing import CliRunner, Result diff --git a/manageprojects/test_utils/project_setup.py b/manageprojects/test_utils/project_setup.py index 61e49a7..2e200b5 100644 --- a/manageprojects/test_utils/project_setup.py +++ b/manageprojects/test_utils/project_setup.py @@ -1,6 +1,6 @@ import configparser -from pathlib import Path import warnings +from pathlib import Path from bx_py_utils.path import assert_is_dir, assert_is_file diff --git a/manageprojects/test_utils/subprocess.py b/manageprojects/test_utils/subprocess.py index 3189c9a..aa7073f 100644 --- a/manageprojects/test_utils/subprocess.py +++ b/manageprojects/test_utils/subprocess.py @@ -1,8 +1,8 @@ from __future__ import annotations -from collections.abc import Callable, Sequence import dataclasses import subprocess +from collections.abc import Callable, Sequence from unittest.mock import patch from bx_py_utils.test_utils.context_managers import MassContextManager diff --git a/manageprojects/test_utils/temp_utils.py b/manageprojects/test_utils/temp_utils.py index 8acc5bc..786d0f3 100644 --- a/manageprojects/test_utils/temp_utils.py +++ b/manageprojects/test_utils/temp_utils.py @@ -1,5 +1,5 @@ -from pathlib import Path import tempfile +from pathlib import Path class TempContentFile: diff --git a/manageprojects/tests/__init__.py b/manageprojects/tests/__init__.py index ff079d2..b367ca8 100644 --- a/manageprojects/tests/__init__.py +++ b/manageprojects/tests/__init__.py @@ -1,6 +1,6 @@ import os -from pathlib import Path import unittest.util +from pathlib import Path from bx_py_utils.test_utils.deny_requests import deny_any_real_request from cli_base.cli_tools.verbosity import MAX_LOG_LEVEL, setup_logging diff --git a/manageprojects/tests/base.py b/manageprojects/tests/base.py index 45e7bfc..030d9d4 100644 --- a/manageprojects/tests/base.py +++ b/manageprojects/tests/base.py @@ -1,8 +1,8 @@ -from collections.abc import Iterable import datetime +import shutil +from collections.abc import Iterable from pathlib import Path from pprint import pprint -import shutil from unittest import TestCase from bx_py_utils.path import assert_is_file diff --git a/manageprojects/tests/docwrite_macros_install_python.py b/manageprojects/tests/docwrite_macros_install_python.py index 8f3b4ab..462b5c3 100644 --- a/manageprojects/tests/docwrite_macros_install_python.py +++ b/manageprojects/tests/docwrite_macros_install_python.py @@ -1,8 +1,8 @@ import argparse -from argparse import ArgumentParser import datetime -from pathlib import Path import tempfile +from argparse import ArgumentParser +from pathlib import Path from unittest.mock import patch from bx_py_utils.doc_write.data_structures import MacroContext diff --git a/manageprojects/tests/test_cli.py b/manageprojects/tests/test_cli.py index 060e24c..c91be57 100644 --- a/manageprojects/tests/test_cli.py +++ b/manageprojects/tests/test_cli.py @@ -1,5 +1,5 @@ -from pathlib import Path, PosixPath import tempfile +from pathlib import Path, PosixPath from unittest import mock from unittest.mock import MagicMock, patch diff --git a/manageprojects/tests/test_cookiecutter_templates.py b/manageprojects/tests/test_cookiecutter_templates.py index fdbc194..0152a21 100644 --- a/manageprojects/tests/test_cookiecutter_templates.py +++ b/manageprojects/tests/test_cookiecutter_templates.py @@ -2,12 +2,12 @@ import json from pathlib import Path +import yaml from bx_py_utils.path import assert_is_dir, assert_is_file from bx_py_utils.test_utils.datetime import parse_dt from bx_py_utils.test_utils.redirect import RedirectOut from cli_base.cli_tools.test_utils.git_utils import init_git from cli_base.cli_tools.test_utils.logs import AssertLogs -import yaml from manageprojects import cli_app from manageprojects.cookiecutter_templates import start_managed_project, update_managed_project diff --git a/manageprojects/tests/test_doc_write.py b/manageprojects/tests/test_doc_write.py index 2cd817f..c7f62b4 100644 --- a/manageprojects/tests/test_doc_write.py +++ b/manageprojects/tests/test_doc_write.py @@ -1,5 +1,5 @@ -from pathlib import Path import subprocess +from pathlib import Path from unittest import TestCase from bx_py_utils.doc_write.api import GeneratedInfo, generate diff --git a/manageprojects/tests/test_install_python.py b/manageprojects/tests/test_install_python.py index 8e7a6f2..ab762e3 100644 --- a/manageprojects/tests/test_install_python.py +++ b/manageprojects/tests/test_install_python.py @@ -1,9 +1,9 @@ import filecmp import inspect -from pathlib import Path import subprocess import sys import tempfile +from pathlib import Path from unittest import TestCase from bx_py_utils.path import assert_is_file diff --git a/manageprojects/tests/test_setup_python.py b/manageprojects/tests/test_setup_python.py index c3d40a6..9f8a440 100644 --- a/manageprojects/tests/test_setup_python.py +++ b/manageprojects/tests/test_setup_python.py @@ -1,8 +1,8 @@ import filecmp -from pathlib import Path import subprocess import sys import tempfile +from pathlib import Path from unittest import TestCase from bx_py_utils.path import assert_is_file diff --git a/manageprojects/tests/test_utilities_publish.py b/manageprojects/tests/test_utilities_publish.py index b0361f2..bbbada5 100644 --- a/manageprojects/tests/test_utilities_publish.py +++ b/manageprojects/tests/test_utilities_publish.py @@ -1,7 +1,7 @@ import inspect -from pathlib import Path import sys import tomllib +from pathlib import Path from unittest import TestCase from unittest.mock import patch diff --git a/manageprojects/utilities/code_style.py b/manageprojects/utilities/code_style.py index e735b96..0231f03 100644 --- a/manageprojects/utilities/code_style.py +++ b/manageprojects/utilities/code_style.py @@ -1,6 +1,6 @@ -from pathlib import Path import sys import warnings +from pathlib import Path from cli_base.cli_tools.subprocess_utils import ToolsExecutor, verbose_check_call diff --git a/manageprojects/utilities/gitignore.py b/manageprojects/utilities/gitignore.py index 2d227aa..804618f 100644 --- a/manageprojects/utilities/gitignore.py +++ b/manageprojects/utilities/gitignore.py @@ -1,5 +1,5 @@ -from functools import lru_cache import logging +from functools import lru_cache from pathlib import Path from pathspec import GitIgnoreSpec, PathSpec diff --git a/manageprojects/utilities/publish.py b/manageprojects/utilities/publish.py index ac84963..fef73c8 100644 --- a/manageprojects/utilities/publish.py +++ b/manageprojects/utilities/publish.py @@ -1,10 +1,10 @@ -from collections.abc import Iterable -from importlib.metadata import version import logging -from pathlib import Path import shutil import sys import tempfile +from collections.abc import Iterable +from importlib.metadata import version +from pathlib import Path from packaging.version import Version from rich import print # noqa diff --git a/manageprojects/utilities/pyproject_toml.py b/manageprojects/utilities/pyproject_toml.py index 59c611a..93d5990 100644 --- a/manageprojects/utilities/pyproject_toml.py +++ b/manageprojects/utilities/pyproject_toml.py @@ -3,9 +3,9 @@ import logging from pathlib import Path +import tomlkit from bx_py_utils.path import assert_is_dir, assert_is_file from cli_base.cli_tools.rich_utils import human_error -import tomlkit from tomlkit import TOMLDocument from tomlkit.items import Table diff --git a/manageprojects/utilities/temp_path.py b/manageprojects/utilities/temp_path.py index f18fc3c..3462f97 100644 --- a/manageprojects/utilities/temp_path.py +++ b/manageprojects/utilities/temp_path.py @@ -1,7 +1,7 @@ import logging -from pathlib import Path import shutil import tempfile +from pathlib import Path logger = logging.getLogger(__name__) diff --git a/pyproject.toml b/pyproject.toml index e272791..4fa8899 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -103,7 +103,6 @@ ignore = [ [tool.ruff.lint.isort] # https://docs.astral.sh/ruff/settings/#lintisort lines-after-imports = 2 -force-sort-within-sections = true [tool.ruff.format] quote-style = "single" From 5c7c1786e607dcabd2be63c9604a7bbe8a54e23c Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Thu, 4 Sep 2025 18:45:36 +0200 Subject: [PATCH 3/3] Bugfix type error in publish version check --- README.md | 1 + .../tests/test_utilities_publish.py | 38 +++++++++++++++++-- manageprojects/utilities/publish.py | 8 +++- 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6cc2240..d66afd2 100644 --- a/README.md +++ b/README.md @@ -381,6 +381,7 @@ See also git tags: https://github.com/jedie/manageprojects/tags [comment]: <> (✂✂✂ auto generated history start ✂✂✂) * [v0.23.0](https://github.com/jedie/manageprojects/compare/v0.22.1...v0.23.0) + * 2025-09-04 - Bugfix type error in publish version check * 2025-09-04 - ruff: remove "force-sort-within-sections = true" (Just use the default) * 2024-11-21 - Switch from click to tyro * [v0.22.1](https://github.com/jedie/manageprojects/compare/v0.22.0...v0.22.1) diff --git a/manageprojects/tests/test_utilities_publish.py b/manageprojects/tests/test_utilities_publish.py index bbbada5..1a8f917 100644 --- a/manageprojects/tests/test_utilities_publish.py +++ b/manageprojects/tests/test_utilities_publish.py @@ -2,21 +2,23 @@ import sys import tomllib from pathlib import Path -from unittest import TestCase from unittest.mock import patch +from bx_py_utils.test_utils.redirect import RedirectOut from cli_base.cli_tools import subprocess_utils from cli_base.cli_tools.test_utils.git_utils import init_git from cli_base.cli_tools.test_utils.logs import AssertLogs +from cli_base.cli_tools.test_utils.rich_test_utils import NoColorEnvRich from packaging.version import Version import manageprojects from manageprojects.cli_dev import PACKAGE_ROOT from manageprojects.test_utils.subprocess import FakeStdout, SubprocessCallMock -from manageprojects.tests.base import GIT_BIN_PARENT +from manageprojects.tests.base import GIT_BIN_PARENT, BaseTestCase from manageprojects.utilities.publish import ( PublisherGit, build, + check_version, clean_version, get_pyproject_toml_version, hatchling_dynamic_version, @@ -25,7 +27,7 @@ from manageprojects.utilities.temp_path import TemporaryDirectory -class PublishTestCase(TestCase): +class PublishTestCase(BaseTestCase): def test_build(self): def return_callback(popenargs, args, kwargs): return FakeStdout(stdout='Mocked run output') @@ -216,3 +218,33 @@ def test_hatchling_dynamic_version(self): ) self.assertIsInstance(version, Version) self.assertEqual(version, Version(manageprojects.__version__)) + + def test_check_version(self): + # Enforce matching versions: + with patch('manageprojects.utilities.publish.version', return_value=manageprojects.__version__): + module_version = check_version( + module=manageprojects, + package_path=PACKAGE_ROOT, + ) + self.assertEqual(module_version, Version(manageprojects.__version__)) + + # Error case: + with ( + NoColorEnvRich(), + RedirectOut() as buffer, + self.assertRaises(SystemExit), + patch('manageprojects.utilities.publish.version', return_value='0.1.2'), + ): + module_version = check_version( + module=manageprojects, + package_path=PACKAGE_ROOT, + ) + self.assertEqual(module_version, Version(manageprojects.__version__)) + self.assertEqual(buffer.stderr, '') + self.assert_in_content( + got=buffer.stdout, + parts=( + f'Version mismatch: current version {manageprojects.__version__} is not the installed one: 0.1.2', + '(Hint: Install package and run publish again)', + ), + ) diff --git a/manageprojects/utilities/publish.py b/manageprojects/utilities/publish.py index fef73c8..d6145c5 100644 --- a/manageprojects/utilities/publish.py +++ b/manageprojects/utilities/publish.py @@ -204,8 +204,12 @@ def check_version(*, module, package_path: Path, distribution_name: str | None = if not distribution_name: distribution_name = module.__name__ - module_version = clean_version(module.__version__) - installed_version = clean_version(version(distribution_name)) + if module_version := module.__version__: + module_version = clean_version(module_version) + + if installed_version := version(distribution_name): + installed_version = clean_version(installed_version) + if module_version != installed_version: exit_with_error( f'Version mismatch: current version {module_version} is not the installed one: {installed_version}',