From deafa5b3415e4951d0f9c35b17f45714a6c8ee1c Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Mon, 10 Nov 2025 08:55:22 -0600 Subject: [PATCH 01/16] ui admin scripts --- admin/__init__.py | 1 + admin/ui.py | 124 ++++++++++++++++++++++++++++++++++++++++++++++ admin/utils.py | 26 ++++++++++ pyproject.toml | 1 + 4 files changed, 152 insertions(+) create mode 100644 admin/ui.py diff --git a/admin/__init__.py b/admin/__init__.py index 057669c..84e6741 100644 --- a/admin/__init__.py +++ b/admin/__init__.py @@ -2,3 +2,4 @@ PROJECT_ROOT = Path(__file__).parents[1] SOURCE_DIR = PROJECT_ROOT / 'src' +ASSETS_DIR = PROJECT_ROOT / 'assets' diff --git a/admin/ui.py b/admin/ui.py new file mode 100644 index 0000000..5963571 --- /dev/null +++ b/admin/ui.py @@ -0,0 +1,124 @@ +#!python +""" +UI operations for Qt. +""" +from typing import Annotated + +import typer + +from admin import ASSETS_DIR +from admin.utils import DryAnnotation, run, run_async, logger + +UI_FILES = tuple((ASSETS_DIR / 'ui').glob('**/*.ui')) +""" +Qt ``.ui`` files. +""" + +QRC_FILES = tuple(ASSETS_DIR.glob('**/*.qrc')) +""" +Qt ``.qrc`` resource files. +""" + +app = typer.Typer( + help=__doc__, + no_args_is_help=True, + add_completion=False, + rich_markup_mode='markdown', +) + + + +@task( + help={ + 'file': '`.ui` file to be converted to `.py`. `.ui` extension not required. ' + 'Can be a comma separated list. If not supplied, all files will be converted. ' + f'Available files: {", ".join(p.stem for p in UI_FILES)}.' + } +) +@app.command(name='py') +def ui_py(file=None, dry: DryAnnotation = False): + """ + Convert Qt `.ui` files into `.py`. + """ + if file: + file_stems = [ + (_f2[:-3] if _f2.lower().endswith('.ui') else _f2) + for _f2 in [_f1.strip() for _f1 in file.split(',')] + ] + else: + file_stems = [p.stem for p in UI_FILES] + + for file_stem in file_stems: + try: + file_path_in = next(p for p in UI_FILES if p.stem == file_stem) + except StopIteration: + logger.error( + f'File "{file}" not found. Available files: {", ".join(p.stem for p in UI_FILES)}' + ) + raise typer.Exit(1) + + file_path_out = SOURCE_DIR / 'ui/forms' / f'ui_{file_stem}.py' + + run(dry, 'pyside6-uic', str(file_path_in), '-o', str(file_path_out), '--from-imports') + + +@task( + help={ + 'file': '`.qrc` file to be converted to `.py`. `.qrc` extension not required. ' + 'Can be a coma separated list of filenames. If not supplied, all files will be converted. ' + f'Available files: {", ".join(p.stem for p in QRC_FILES)}.' + } +) +def ui_rc(file=None, dry: DryAnnotation = False): + """ + Convert Qt `.qrc` files into `.py`. + """ + if file: + file_stems = [ + (_f2[:-4] if _f2.lower().endswith('.qrc') else _f2) + for _f2 in [_f1.strip() for _f1 in file.split(',')] + ] + else: + file_stems = [p.stem for p in QRC_FILES] + + for file_stem in file_stems: + try: + file_path_in = next(p for p in QRC_FILES if p.stem == file_stem) + except StopIteration: + logger.error( + f'File "{file}" not found. Available files: {", ".join(p.stem for p in QRC_FILES)}' + ) + raise typer.Exit(1) + + file_path_out = SOURCE_DIR / 'ui/forms' / f'{file_stem}_rc.py' + + run(dry, 'pyside6-rcc', str(file_path_in), '-o', str(file_path_out)) + + +@app.command(name='edit') +def ui_edit( + file: Annotated[ + str, + typer.Argument( + help=f'`.ui` file to be edited. Available files: {", ".join(p.stem for p in UI_FILES)}.' + ), + ], + dry: DryAnnotation = False, +): + """ + Edit a file in QT Designer. + """ + file_stem = file[:-3] if file.lower().endswith('.ui') else file + try: + ui_file_path = next(p for p in UI_FILES if p.stem == file_stem) + except StopIteration: + logger.error( + f'File "{file}" not found. Available files: {", ".join(p.stem for p in UI_FILES)}' + ) + raise typer.Exit(1) + + run_async(dry, 'pyside6-designer', str(ui_file_path)) + + +if __name__ == '__main__': + app() diff --git a/admin/utils.py b/admin/utils.py index a236d4f..c9b23db 100644 --- a/admin/utils.py +++ b/admin/utils.py @@ -53,6 +53,32 @@ def run(dry: bool, *args) -> subprocess.CompletedProcess | None: raise typer.Exit(1) +def run_async(dry: bool, *args) -> subprocess.Popen | None: + """ + Starts the process and continues code execution. + + Use the following checks:: + + process.poll() # Returns None if still running, else return code + process.wait() # Wait for completion (blocking) + process.terminate() # Send SIGTERM (graceful) + process.kill() # Send SIGKILL (force) + process.returncode # Access return code after completion + + See ``subprocess.Popen(...)`` for more details. + """ + logger.info(' '.join(map(str, args))) + + if dry: + return None + + try: + return subprocess.Popen(args, cwd=PROJECT_ROOT) + except subprocess.CalledProcessError as e: + logger.error(e) + raise typer.Exit(1) + + def is_package_installed(package_name: str) -> bool: """Check if a Python package is installed.""" import importlib.util diff --git a/pyproject.toml b/pyproject.toml index b923dbd..dd9bfbf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ modules = [ 'admin.pip', 'admin.precommit', 'admin.test', + 'admin.ui', ] no_args_is_help = true add_completion = false From 60d07cdf9de2ffdcec98788525e667a72e872176 Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Mon, 10 Nov 2025 09:03:16 -0600 Subject: [PATCH 02/16] ui admin scripts --- admin/__init__.py | 6 +++++- admin/ui.py | 50 +++++++++++++++++++++++++++-------------------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/admin/__init__.py b/admin/__init__.py index 84e6741..af99127 100644 --- a/admin/__init__.py +++ b/admin/__init__.py @@ -1,5 +1,9 @@ from pathlib import Path PROJECT_ROOT = Path(__file__).parents[1] -SOURCE_DIR = PROJECT_ROOT / 'src' +PROJECT_NAME = PROJECT_ROOT.name ASSETS_DIR = PROJECT_ROOT / 'assets' +PROJECT_SOURCE_DIR = PROJECT_ROOT / 'src' +"""Source code for the whole project.""" +SOURCE_DIR = PROJECT_SOURCE_DIR / PROJECT_NAME +"""Source code for the this project's package.""" diff --git a/admin/ui.py b/admin/ui.py index 5963571..d771cf9 100644 --- a/admin/ui.py +++ b/admin/ui.py @@ -6,8 +6,8 @@ import typer -from admin import ASSETS_DIR -from admin.utils import DryAnnotation, run, run_async, logger +from admin import ASSETS_DIR, SOURCE_DIR +from admin.utils import DryAnnotation, logger, run, run_async UI_FILES = tuple((ASSETS_DIR / 'ui').glob('**/*.ui')) """ @@ -27,23 +27,26 @@ ) - -@task( - help={ - 'file': '`.ui` file to be converted to `.py`. `.ui` extension not required. ' - 'Can be a comma separated list. If not supplied, all files will be converted. ' - f'Available files: {", ".join(p.stem for p in UI_FILES)}.' - } -) @app.command(name='py') -def ui_py(file=None, dry: DryAnnotation = False): +def ui_py( + file: Annotated[ + list[str] | None, + typer.Argument( + help='`.ui` file to be converted to `.py`. `.ui` extension not required. ' + 'If not supplied, all files will be converted. ' + f'Available files: {", ".join(p.stem for p in UI_FILES)}.', + show_default=False, + ), + ] = None, + dry: DryAnnotation = False, +): """ Convert Qt `.ui` files into `.py`. """ if file: file_stems = [ (_f2[:-3] if _f2.lower().endswith('.ui') else _f2) - for _f2 in [_f1.strip() for _f1 in file.split(',')] + for _f2 in [_f1.strip() for _f1 in file] ] else: file_stems = [p.stem for p in UI_FILES] @@ -62,21 +65,26 @@ def ui_py(file=None, dry: DryAnnotation = False): run(dry, 'pyside6-uic', str(file_path_in), '-o', str(file_path_out), '--from-imports') -@task( - help={ - 'file': '`.qrc` file to be converted to `.py`. `.qrc` extension not required. ' - 'Can be a coma separated list of filenames. If not supplied, all files will be converted. ' - f'Available files: {", ".join(p.stem for p in QRC_FILES)}.' - } -) -def ui_rc(file=None, dry: DryAnnotation = False): +@app.command(name='rc') +def ui_rc( + file: Annotated[ + list[str] | None, + typer.Argument( + help='`.qrc` file(s) to be converted to `.py`. `.qrc` extension not required. ' + 'If not supplied, all files will be converted. ' + f'Available files: {", ".join(p.stem for p in QRC_FILES)}.', + show_default=False, + ), + ] = None, + dry: DryAnnotation = False, +): """ Convert Qt `.qrc` files into `.py`. """ if file: file_stems = [ (_f2[:-4] if _f2.lower().endswith('.qrc') else _f2) - for _f2 in [_f1.strip() for _f1 in file.split(',')] + for _f2 in [_f1.strip() for _f1 in file] ] else: file_stems = [p.stem for p in QRC_FILES] From 2ec582287141e1096366390dd4dd3d6650e5e368 Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Tue, 11 Nov 2025 00:00:23 -0600 Subject: [PATCH 03/16] admin scripts updates --- admin/pip.py | 8 ++++---- admin/utils.py | 12 +++++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/admin/pip.py b/admin/pip.py index 8162477..d7803dc 100644 --- a/admin/pip.py +++ b/admin/pip.py @@ -104,7 +104,7 @@ def pip_compile( """ Compile requirements file(s). """ - install_package('pip-tools', dry=dry) + install_package('piptools', 'pip-tools', dry=dry) if clean and not dry: for filename in _get_requirements_files(requirements, RequirementsType.OUT): @@ -138,7 +138,7 @@ def pip_sync(requirements: RequirementsAnnotation = None, dry: DryAnnotation = F """ Synchronize environment with requirements file. """ - install_package('pip-tools', dry=dry) + install_package('piptools', 'pip-tools', dry=dry) run(dry, 'pip-sync', *_get_requirements_files(requirements, RequirementsType.OUT)) @@ -153,7 +153,7 @@ def pip_package( """ Upgrade one or more packages. """ - install_package('pip-tools', dry=dry) + install_package('piptools', 'pip-tools', dry=dry) for filename in _get_requirements_files(requirements, RequirementsType.IN): run( @@ -171,7 +171,7 @@ def pip_upgrade(requirements, dry: DryAnnotation = False): Use ``package`` to only upgrade individual packages, Ex ``pip package dev mypy flake8``. """ - install_package('pip-tools', dry=dry) + install_package('piptools', 'pip-tools', dry=dry) for filename in _get_requirements_files(requirements, RequirementsType.IN): run(dry, ['pip-compile', '--upgrade', filename]) diff --git a/admin/utils.py b/admin/utils.py index c9b23db..356016b 100644 --- a/admin/utils.py +++ b/admin/utils.py @@ -86,13 +86,19 @@ def is_package_installed(package_name: str) -> bool: return importlib.util.find_spec(package_name) is not None -def install_package(package: str, dry: bool = False): - """Install a Python package if not already installed.""" +def install_package(package: str, package_install: str | None =None, dry: bool = False): + """ + Install a Python package if not already installed. + + :param package: Name of the package to check/install. + :param package_install: Name of the package to install, if different from the name to check. + :param dry: Show the command that would be run without running it. + """ if is_package_installed(package): logger.debug(f'Package `{package}` is already installed.') return - run(dry, sys.executable, '-m', 'pip', 'install', package) + run(dry, sys.executable, '-m', 'pip', 'install', package_install or package) def get_logger(name=None, level=logging.DEBUG) -> logging.Logger: From e7115f06044215de71af4d4a4bccb26eb0497634 Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sun, 16 Nov 2025 14:05:34 -0600 Subject: [PATCH 04/16] admin scripts updates --- admin/build.py | 34 +++++++++++++++++++++------------- admin/lint.py | 8 ++++---- admin/pip.py | 36 ++++++++++-------------------------- admin/test.py | 2 +- admin/ui.py | 6 +++--- admin/utils.py | 46 +++++++++++++++++++++++++++++++++++++--------- 6 files changed, 76 insertions(+), 56 deletions(-) diff --git a/admin/build.py b/admin/build.py index de6a7f4..c41f262 100644 --- a/admin/build.py +++ b/admin/build.py @@ -113,7 +113,7 @@ def _get_version_from_release_name(release_name: str) -> str: return release_name[1:] -def _get_latest_release(c) -> tuple[str, str, list[dict]]: +def _get_latest_release(dry: bool) -> tuple[str, str, list[dict]]: """ Retrieves the latest release from GitHub. @@ -121,14 +121,22 @@ def _get_latest_release(c) -> tuple[str, str, list[dict]]: """ import json - release_info_json = c.run('gh release view --json name,tagName,assets').stdout.strip() + release_info_json = ( + run('gh', 'release', 'view', '--json', 'name,tagName,assets', dry=dry, capture_output=True) + .stdout.decode() # type: ignore + .strip() + ) release_info = json.loads(release_info_json) return release_info['name'], release_info['tagName'], release_info['assets'] def _get_branch(): """Returns the current branch.""" - return run(False, 'git', 'branch', '--show-current') + return ( + run('git', 'branch', '--show-current', dry=False, capture_output=True) + .stdout.decode() # type: ignore + .strip() + ) def _get_default_branch(): @@ -140,12 +148,12 @@ def _get_default_branch(): def _commit(message: str, dry: bool): # Commit - run(dry, 'git', 'add', *VERSION_FILES) - run(dry, 'git', 'commit', '-m', message) + run('git', 'add', *VERSION_FILES, dry=dry) + run('git', 'commit', '-m', message, dry=dry) # Push current branch branch = _get_branch() - run(dry, 'git', 'push', 'origin', branch) + run('git', 'push', 'origin', branch, dry=dry) def _create_pr(title: str, description: str, dry: bool): @@ -158,7 +166,6 @@ def _create_pr(title: str, description: str, dry: bool): default_branch = _get_default_branch() branch = _get_branch() run( - dry, 'gh', 'pr', 'create', @@ -170,10 +177,11 @@ def _create_pr(title: str, description: str, dry: bool): branch, '--base', default_branch, + dry=dry, ) # Merge PR after checks pass - run(dry, 'gh', 'pr', 'merge', 'branch', '--squash', '--auto') + run('gh', 'pr', 'merge', 'branch', '--squash', '--auto', dry=dry) @app.command(name='clean') @@ -269,7 +277,7 @@ def build_version( if yes or input( f'Current branch `{branch}` is the default branch, create new branch? [Y/n] ' ).strip().lower() in ['', 'y', 'yes']: - run(dry, 'git', 'checkout', '-b', f'release-{v2}') + run('git', 'checkout', '-b', f'release-{v2}', dry=dry) branch_ok = True if not branch_ok: logger.error(f'Cannot make changes in the default branch `{branch}`.') @@ -277,20 +285,20 @@ def build_version( # Update files to new version _update_project_version(str(v2)) - print( + logger.info( f'New version is `{v2}`. Modified files :\n' + '\n'.join(f' {file.relative_to(PROJECT_ROOT)}' for file in VERSION_FILES) ) # Commit/push/pr if mode == 'nothing': - print('Files not committed, PR not created.') + logger.info('Files not committed, PR not created.') if mode in ['commit', 'pr']: - print('Commit and push changes.') + logger.info('Commit and push changes.') _commit(f'bump version to {v2}', dry) if mode == 'pr': pr_title = f'Release {v2}' - print(f'Create and merge PR `{pr_title}`.') + logger.info(f'Create and merge PR `{pr_title}`.') _create_pr(pr_title, f'Preparing for release {v2}', dry) diff --git a/admin/lint.py b/admin/lint.py index d16c13c..8648efb 100644 --- a/admin/lint.py +++ b/admin/lint.py @@ -17,22 +17,22 @@ @app.command(name='black') def lint_black(path='.', dry: DryAnnotation = False): - run(dry, 'black', path) + run('black', path, dry=dry) @app.command(name='flake8') def lint_flake8(path='.', dry: DryAnnotation = False): - run(dry, 'flake8', path) + run('flake8', path, dry=dry) @app.command(name='isort') def lint_isort(path='.', dry: DryAnnotation = False): - run(dry, 'isort', path) + run('isort', path, dry=dry) @app.command(name='mypy') def lint_mypy(path='.', dry: DryAnnotation = False): - run(dry, 'mypy', path) + run('mypy', path, dry=dry) @app.command(name='all') diff --git a/admin/pip.py b/admin/pip.py index d7803dc..884bfe7 100644 --- a/admin/pip.py +++ b/admin/pip.py @@ -9,7 +9,7 @@ import typer from admin import PROJECT_ROOT -from admin.utils import DryAnnotation, install_package, logger, run +from admin.utils import DryAnnotation, install_package, logger, multiple_parameters, run REQUIREMENTS_DIR = PROJECT_ROOT / 'admin' / 'requirements' @@ -32,7 +32,6 @@ class Requirements(StrEnum): MAIN = 'requirements' DEV = 'requirements-dev' - DOCS = 'requirements-docs' class RequirementsType(StrEnum): @@ -112,25 +111,7 @@ def pip_compile( dry_option = ['--dry-run'] if dry else [] for filename in _get_requirements_files(requirements, RequirementsType.IN): - run(False, 'pip-compile', *dry_option, str(filename)) - - -@app.command(name='install') -def pip_install(requirements: RequirementsAnnotation = None, dry: DryAnnotation = False): - """ - Install packages from the requirements file(s). - - Equivalent to ``pip install -r ``. Making it easier to point to the correct file. - """ - from itertools import chain - - files = _get_requirements_files(requirements, RequirementsType.OUT) - run( - dry, - 'pip', - 'install', - *list(chain.from_iterable(zip(['-r'] * len(files), map(str, files)))), - ) + run('pip-compile', *dry_option, str(filename), dry=False) @app.command(name='sync') @@ -139,14 +120,14 @@ def pip_sync(requirements: RequirementsAnnotation = None, dry: DryAnnotation = F Synchronize environment with requirements file. """ install_package('piptools', 'pip-tools', dry=dry) - run(dry, 'pip-sync', *_get_requirements_files(requirements, RequirementsType.OUT)) + run('pip-sync', *_get_requirements_files(requirements, RequirementsType.OUT), dry=dry) @app.command(name='package') def pip_package( requirements: RequirementsAnnotation, - packages: Annotated[ - list[str], typer.Option('--packages', '-p', help='One or more packages to upgrade.') + package: Annotated[ + list[str], typer.Option('--package', '-p', help='One or more packages to upgrade.') ], dry: DryAnnotation = False, ): @@ -157,7 +138,10 @@ def pip_package( for filename in _get_requirements_files(requirements, RequirementsType.IN): run( - dry, 'pip-compile', '--upgrade-package', *' --upgrade-package '.join(packages), filename + 'pip-compile', + *multiple_parameters('--upgrade-package', *package), + filename, + dry=dry, ) @@ -174,7 +158,7 @@ def pip_upgrade(requirements, dry: DryAnnotation = False): install_package('piptools', 'pip-tools', dry=dry) for filename in _get_requirements_files(requirements, RequirementsType.IN): - run(dry, ['pip-compile', '--upgrade', filename]) + run(['pip-compile', '--upgrade', filename], dry=dry) if __name__ == '__main__': diff --git a/admin/test.py b/admin/test.py index f4c6d3b..4380902 100644 --- a/admin/test.py +++ b/admin/test.py @@ -22,7 +22,7 @@ def test_unit(dry: DryAnnotation = False): Unit test configuration in ``pyproject.toml``. """ - run(dry, 'pytest', '.') + run('pytest', '.', dry=dry) if __name__ == '__main__': diff --git a/admin/ui.py b/admin/ui.py index d771cf9..4027afd 100644 --- a/admin/ui.py +++ b/admin/ui.py @@ -62,7 +62,7 @@ def ui_py( file_path_out = SOURCE_DIR / 'ui/forms' / f'ui_{file_stem}.py' - run(dry, 'pyside6-uic', str(file_path_in), '-o', str(file_path_out), '--from-imports') + run('pyside6-uic', str(file_path_in), '-o', str(file_path_out), '--from-imports', dry=dry) @app.command(name='rc') @@ -100,7 +100,7 @@ def ui_rc( file_path_out = SOURCE_DIR / 'ui/forms' / f'{file_stem}_rc.py' - run(dry, 'pyside6-rcc', str(file_path_in), '-o', str(file_path_out)) + run('pyside6-rcc', str(file_path_in), '-o', str(file_path_out), dry=dry) @app.command(name='edit') @@ -125,7 +125,7 @@ def ui_edit( ) raise typer.Exit(1) - run_async(dry, 'pyside6-designer', str(ui_file_path)) + run_async('pyside6-designer', str(ui_file_path), dry=dry) if __name__ == '__main__': diff --git a/admin/utils.py b/admin/utils.py index 356016b..f382065 100644 --- a/admin/utils.py +++ b/admin/utils.py @@ -2,6 +2,7 @@ import subprocess import sys from enum import Enum +from itertools import chain from typing import Annotated import typer @@ -40,20 +41,39 @@ def get_os() -> OS: return OS.Linux -def run(dry: bool, *args) -> subprocess.CompletedProcess | None: +def run(*args, dry: bool = False, **kwargs) -> subprocess.CompletedProcess | None: + """ + Run a CLI command synchronously (i.e., wait for the command to finish) and return the result. + + This function is a wrapper around ``subprocess.run(...)``. + + If you need access to the output, add the ``capture_output=True`` argument and do + ``.stdout.decode().strip()`` to get the output as a string. + """ logger.info(' '.join(map(str, args))) if dry: return None + defaults = dict( + cwd=PROJECT_ROOT, + capture_output=False, + check=True, + ) + try: - return subprocess.run(args, cwd=PROJECT_ROOT, check=True) + return subprocess.run(args, **(defaults | kwargs)) # type: ignore except subprocess.CalledProcessError as e: - logger.error(e) + msg = str(e) + if e.stdout: + msg += f'\nSTDOUT:\n{e.stdout.decode()}' + if e.stderr: + msg += f'\nSTDERR:\n{e.stderr.decode()}' + logger.error(msg) raise typer.Exit(1) -def run_async(dry: bool, *args) -> subprocess.Popen | None: +def run_async(*args, dry: bool = False, **kwargs) -> subprocess.Popen | None: """ Starts the process and continues code execution. @@ -72,8 +92,12 @@ def run_async(dry: bool, *args) -> subprocess.Popen | None: if dry: return None + defaults = dict( + cwd=PROJECT_ROOT, + ) + try: - return subprocess.Popen(args, cwd=PROJECT_ROOT) + return subprocess.Popen(args, **(defaults | kwargs)) except subprocess.CalledProcessError as e: logger.error(e) raise typer.Exit(1) @@ -86,7 +110,7 @@ def is_package_installed(package_name: str) -> bool: return importlib.util.find_spec(package_name) is not None -def install_package(package: str, package_install: str | None =None, dry: bool = False): +def install_package(package: str, package_install: str | None = None, dry: bool = False): """ Install a Python package if not already installed. @@ -98,14 +122,18 @@ def install_package(package: str, package_install: str | None =None, dry: bool = logger.debug(f'Package `{package}` is already installed.') return - run(dry, sys.executable, '-m', 'pip', 'install', package_install or package) + run(sys.executable, '-m', 'pip', 'install', package_install or package, dry=dry) + + +def multiple_parameters(parameter: str, *options) -> list[str]: + return list(chain.from_iterable(zip([parameter] * len(options), map(str, options)))) -def get_logger(name=None, level=logging.DEBUG) -> logging.Logger: +def get_logger(name: str | None = 'typer-invoke', level=logging.DEBUG) -> logging.Logger: """Set up logging configuration with Rich handler and custom formatting.""" # Create logger - _logger = logging.getLogger('typer-invoke') + _logger = logging.getLogger(name) _logger.setLevel(level) _logger.handlers.clear() handler = RichHandler( From 172283b7d7b37f176af2c8e9f55d462a256ba302 Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sat, 22 Nov 2025 10:27:13 -0600 Subject: [PATCH 05/16] small tweak in admin script --- admin/utils.py | 1 + tasks.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/admin/utils.py b/admin/utils.py index f382065..c7371e1 100644 --- a/admin/utils.py +++ b/admin/utils.py @@ -140,6 +140,7 @@ def get_logger(name: str | None = 'typer-invoke', level=logging.DEBUG) -> loggin level=level, show_time=False, show_level=True, + show_path=False, markup=True, rich_tracebacks=False, ) diff --git a/tasks.py b/tasks.py index d40bf93..9b2d07f 100644 --- a/tasks.py +++ b/tasks.py @@ -928,9 +928,9 @@ def docs_clean(c): ui_collection.add_task(ui_edit, 'edit') ns.add_collection(build_collection) +ns.add_collection(docs_collection) ns.add_collection(lint_collection) ns.add_collection(pip_collection) ns.add_collection(precommit_collection) ns.add_collection(test_collection) -ns.add_collection(docs_collection) ns.add_collection(ui_collection) From 9e18ce9f9f0ed7a7a65b942b18fdaab059ed98c0 Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sat, 22 Nov 2025 10:36:32 -0600 Subject: [PATCH 06/16] small tweak in admin script --- admin/pip.py | 6 ++++-- pyproject.toml | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/admin/pip.py b/admin/pip.py index 884bfe7..2714cdc 100644 --- a/admin/pip.py +++ b/admin/pip.py @@ -111,7 +111,9 @@ def pip_compile( dry_option = ['--dry-run'] if dry else [] for filename in _get_requirements_files(requirements, RequirementsType.IN): - run('pip-compile', *dry_option, str(filename), dry=False) + run( + 'pip-compile', '--no-header', '--no-strip-extras', *dry_option, str(filename), dry=False + ) @app.command(name='sync') @@ -158,7 +160,7 @@ def pip_upgrade(requirements, dry: DryAnnotation = False): install_package('piptools', 'pip-tools', dry=dry) for filename in _get_requirements_files(requirements, RequirementsType.IN): - run(['pip-compile', '--upgrade', filename], dry=dry) + run(['pip-compile', '--no-strip-extras', '--upgrade', filename], dry=dry) if __name__ == '__main__': diff --git a/pyproject.toml b/pyproject.toml index dd9bfbf..13767b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,8 +55,8 @@ modules = [ no_args_is_help = true add_completion = false rich_markup_mode = 'markdown' -logging_level = 'INFO' -logging_format = '%(message)s' +log_level = 'INFO' +log_format = '%(message)s' [build-system] requires = ['flit_core >=3.2,<4'] From dd3d8cb8df2d6aff488c710fa72f9ab81a7df6be Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sat, 22 Nov 2025 10:36:40 -0600 Subject: [PATCH 07/16] requirements --- admin/requirements/requirements-dev.txt | 36 +++++++++++-------------- admin/requirements/requirements.txt | 16 ++++------- 2 files changed, 20 insertions(+), 32 deletions(-) diff --git a/admin/requirements/requirements-dev.txt b/admin/requirements/requirements-dev.txt index 357b145..9946f9e 100644 --- a/admin/requirements/requirements-dev.txt +++ b/admin/requirements/requirements-dev.txt @@ -1,24 +1,18 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile 'requirements-dev.in' -# -altgraph==0.17.4 +altgraph==0.17.5 # via pyinstaller -bandit==1.8.6 +bandit==1.9.1 # via -r requirements-dev.in -black==25.9.0 +black==25.11.0 # via -r requirements-dev.in build==1.3.0 # via pip-tools -certifi==2025.10.5 +certifi==2025.11.12 # via requests -cfgv==3.4.0 +cfgv==3.5.0 # via pre-commit charset-normalizer==3.4.4 # via requests -click==8.3.0 +click==8.3.1 # via # black # pip-tools @@ -80,7 +74,7 @@ pathspec==0.12.1 # mypy pefile==2023.2.7 # via pyinstaller -pip-tools==7.5.1 +pip-tools==7.5.2 # via -r requirements-dev.in platformdirs==4.5.0 # via @@ -102,24 +96,24 @@ pygments==2.19.2 # rich pyinstaller==6.16.0 # via -r requirements-dev.in -pyinstaller-hooks-contrib==2025.9 +pyinstaller-hooks-contrib==2025.10 # via pyinstaller pyproject-hooks==1.2.0 # via # build # pip-tools -pyside6==6.10.0 +pyside6==6.10.1 # via -r requirements.txt -pyside6-addons==6.10.0 +pyside6-addons==6.10.1 # via # -r requirements.txt # pyside6 -pyside6-essentials==6.10.0 +pyside6-essentials==6.10.1 # via # -r requirements.txt # pyside6 # pyside6-addons -pytest==9.0.0 +pytest==9.0.1 # via # -r requirements-dev.in # pytest-params @@ -145,19 +139,19 @@ rich==14.2.0 # typer-invoke shellingham==1.5.4 # via typer -shiboken6==6.10.0 +shiboken6==6.10.1 # via # -r requirements.txt # pyside6 # pyside6-addons # pyside6-essentials -stevedore==5.5.0 +stevedore==5.6.0 # via bandit tomli-w==1.2.0 # via flit typer==0.20.0 # via typer-invoke -typer-invoke==0.3.0 +typer-invoke==0.4.0 # via -r requirements-dev.in typing-extensions==4.15.0 # via diff --git a/admin/requirements/requirements.txt b/admin/requirements/requirements.txt index 2b14004..30af837 100644 --- a/admin/requirements/requirements.txt +++ b/admin/requirements/requirements.txt @@ -1,18 +1,12 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile 'requirements.in' -# -pyside6==6.10.0 - # via -r requirements.in -pyside6-addons==6.10.0 +pyside6==6.10.1 + # via -r C:/Users/joaon/code/hd_active/admin/requirements/requirements.in +pyside6-addons==6.10.1 # via pyside6 -pyside6-essentials==6.10.0 +pyside6-essentials==6.10.1 # via # pyside6 # pyside6-addons -shiboken6==6.10.0 +shiboken6==6.10.1 # via # pyside6 # pyside6-addons From 2d4060747f5db3bae318cfb331445cdd1c5b4edc Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sat, 22 Nov 2025 10:40:16 -0600 Subject: [PATCH 08/16] fix --- admin/lint.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/admin/lint.py b/admin/lint.py index 8648efb..f971e9f 100644 --- a/admin/lint.py +++ b/admin/lint.py @@ -31,8 +31,13 @@ def lint_isort(path='.', dry: DryAnnotation = False): @app.command(name='mypy') -def lint_mypy(path='.', dry: DryAnnotation = False): - run('mypy', path, dry=dry) +def lint_mypy(path=None, dry: DryAnnotation = False): + if path: + run('mypy', path, dry=dry) + else: + run('mypy', 'src', dry=dry) + run('mypy', 'tests', dry=dry) + run('mypy', 'admin', dry=dry) @app.command(name='all') From eaac0cd0d2358be1519a520cf51bcc7d30e29eed Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sun, 8 Mar 2026 10:45:43 -0500 Subject: [PATCH 09/16] requirements --- admin/requirements/requirements-dev.txt | 79 ++++++++++++++----------- admin/requirements/requirements.txt | 10 ++-- 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/admin/requirements/requirements-dev.txt b/admin/requirements/requirements-dev.txt index 9946f9e..f32e81a 100644 --- a/admin/requirements/requirements-dev.txt +++ b/admin/requirements/requirements-dev.txt @@ -1,16 +1,18 @@ altgraph==0.17.5 # via pyinstaller -bandit==1.9.1 +annotated-doc==0.0.4 + # via typer +bandit==1.9.4 # via -r requirements-dev.in -black==25.11.0 +black==26.3.0 # via -r requirements-dev.in -build==1.3.0 +build==1.4.0 # via pip-tools -certifi==2025.11.12 +certifi==2026.2.25 # via requests cfgv==3.5.0 # via pre-commit -charset-normalizer==3.4.4 +charset-normalizer==3.4.5 # via requests click==8.3.1 # via @@ -25,66 +27,72 @@ colorama==0.4.6 # pytest distlib==0.4.0 # via virtualenv -docutils==0.22.3 +docutils==0.22.4 # via flit -filelock==3.20.0 - # via virtualenv +filelock==3.25.0 + # via + # python-discovery + # virtualenv flake8==7.3.0 # via # -r requirements-dev.in # flake8-pyproject -flake8-pyproject==1.2.3 +flake8-pyproject==1.2.4 # via -r requirements-dev.in flit==3.12.0 # via -r requirements-dev.in flit-core==3.12.0 # via flit -identify==2.6.15 +identify==2.6.17 # via pre-commit idna==3.11 # via requests iniconfig==2.3.0 # via pytest -isort==7.0.0 +isort==8.0.1 # via -r requirements-dev.in +librt==0.8.1 + # via mypy markdown-it-py==4.0.0 # via rich mccabe==0.7.0 # via flake8 mdurl==0.1.2 # via markdown-it-py -mypy==1.18.2 +mypy==1.19.1 # via -r requirements-dev.in mypy-extensions==1.1.0 # via # black # mypy -nodeenv==1.9.1 +nodeenv==1.10.0 # via pre-commit -packaging==25.0 +packaging==26.0 # via # black # build # pyinstaller # pyinstaller-hooks-contrib # pytest -pathspec==0.12.1 + # wheel +pathspec==1.0.4 # via # black # mypy -pefile==2023.2.7 +pefile==2024.8.26 # via pyinstaller -pip-tools==7.5.2 +pip-tools==7.5.3 # via -r requirements-dev.in -platformdirs==4.5.0 +platformdirs==4.9.4 # via # black + # python-discovery # virtualenv pluggy==1.6.0 # via # pytest # pytest-qt -pre-commit==4.4.0 +pre-commit==4.5.1 # via -r requirements-dev.in pycodestyle==2.14.0 # via flake8 @@ -94,26 +102,26 @@ pygments==2.19.2 # via # pytest # rich -pyinstaller==6.16.0 +pyinstaller==6.19.0 # via -r requirements-dev.in -pyinstaller-hooks-contrib==2025.10 +pyinstaller-hooks-contrib==2026.2 # via pyinstaller pyproject-hooks==1.2.0 # via # build # pip-tools -pyside6==6.10.1 +pyside6==6.10.2 # via -r requirements.txt -pyside6-addons==6.10.1 +pyside6-addons==6.10.2 # via # -r requirements.txt # pyside6 -pyside6-essentials==6.10.1 +pyside6-essentials==6.10.2 # via # -r requirements.txt # pyside6 # pyside6-addons -pytest==9.0.1 +pytest==9.0.2 # via # -r requirements-dev.in # pytest-params @@ -122,7 +130,9 @@ pytest-params==0.3.0 # via -r requirements-dev.in pytest-qt==4.5.0 # via -r requirements-dev.in -pytokens==0.3.0 +python-discovery==1.1.1 + # via virtualenv +pytokens==0.4.1 # via black pywin32-ctypes==0.2.3 # via pyinstaller @@ -132,37 +142,36 @@ pyyaml==6.0.3 # pre-commit requests==2.32.5 # via flit -rich==14.2.0 +rich==14.3.3 # via # bandit # typer # typer-invoke shellingham==1.5.4 # via typer -shiboken6==6.10.1 +shiboken6==6.10.2 # via # -r requirements.txt # pyside6 # pyside6-addons # pyside6-essentials -stevedore==5.6.0 +stevedore==5.7.0 # via bandit tomli-w==1.2.0 # via flit -typer==0.20.0 +typer==0.24.1 # via typer-invoke -typer-invoke==0.4.0 +typer-invoke==0.5.0 # via -r requirements-dev.in typing-extensions==4.15.0 # via # mypy # pytest-qt - # typer -urllib3==2.5.0 +urllib3==2.6.3 # via requests -virtualenv==20.35.4 +virtualenv==21.1.0 # via pre-commit -wheel==0.45.1 +wheel==0.46.3 # via pip-tools # The following packages are considered to be unsafe in a requirements file: diff --git a/admin/requirements/requirements.txt b/admin/requirements/requirements.txt index 30af837..2eee1f8 100644 --- a/admin/requirements/requirements.txt +++ b/admin/requirements/requirements.txt @@ -1,12 +1,12 @@ -pyside6==6.10.1 - # via -r C:/Users/joaon/code/hd_active/admin/requirements/requirements.in -pyside6-addons==6.10.1 +pyside6==6.10.2 + # via -r requirements.in +pyside6-addons==6.10.2 # via pyside6 -pyside6-essentials==6.10.1 +pyside6-essentials==6.10.2 # via # pyside6 # pyside6-addons -shiboken6==6.10.1 +shiboken6==6.10.2 # via # pyside6 # pyside6-addons From 26f265c4a6b9492235b2d0ace511b9d75aa1eabe Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sun, 8 Mar 2026 10:48:48 -0500 Subject: [PATCH 10/16] remove invoke tasks --- tasks.py | 936 ------------------------------------------------------- 1 file changed, 936 deletions(-) delete mode 100644 tasks.py diff --git a/tasks.py b/tasks.py deleted file mode 100644 index 9b2d07f..0000000 --- a/tasks.py +++ /dev/null @@ -1,936 +0,0 @@ -import os -import sys -from pathlib import Path - -from invoke import Collection, Exit, task - -os.environ.setdefault('INVOKE_RUN_ECHO', '1') # Show commands by default - - -PROJECT_ROOT = Path(__file__).parent -PROJECT_NAME = PROJECT_ROOT.name -PROJECT_SOURCE_DIR = PROJECT_ROOT / 'src' -"""Source code for the whole project.""" -SOURCE_DIR = PROJECT_SOURCE_DIR / PROJECT_NAME -"""Source code for the this project's package.""" -ASSETS_DIR = PROJECT_ROOT / 'assets' - -if str(PROJECT_SOURCE_DIR) not in sys.path: - sys.path.insert(0, str(PROJECT_SOURCE_DIR)) - -# Requirements files -REQUIREMENTS_MAIN = 'main' -REQUIREMENTS_FILES = { - REQUIREMENTS_MAIN: 'requirements', - 'dev': 'requirements-dev', - 'docs': 'requirements-docs', -} -""" -Requirements files. -Order matters as most operations with multiple files need ``requirements.txt`` to be processed -first. -Add new requirements files here. -""" - -REQUIREMENTS_TASK_HELP = { - 'requirements': '`.in` file. Full name not required, just the initial name after the dash ' - f'(ex. "dev"). For main file use "{REQUIREMENTS_MAIN}". Available requirements: ' - f'{", ".join(REQUIREMENTS_FILES)}.' -} - -VERSION_FILES = [ - PROJECT_ROOT / 'pyproject.toml', - SOURCE_DIR / '__init__.py', -] -""" -Files that contain the package version. -This version needs to be updated with each release. -""" - -UI_FILES = tuple((ASSETS_DIR / 'ui').glob("**/*.ui")) -""" -Qt ``.ui`` files. -""" - -QRC_FILES = tuple(ASSETS_DIR.glob("**/*.qrc")) -""" -Qt ``.qrc`` resource files. -""" - -# region Executable build configs -BUILD_SPEC_FILE = ASSETS_DIR / 'pyinstaller.spec' -BUILD_WORK_DIR = PROJECT_ROOT / 'build' -BUILD_WORK_APP_DIR = BUILD_WORK_DIR / 'app' -"""See ``BUILD_DIST_APP_DIR`` for more info.""" -BUILD_DIST_DIR = PROJECT_ROOT / 'dist' -BUILD_DIST_APP_DIR = BUILD_DIST_DIR / 'app' -""" -Directory where the executable is built. - -There are two types of distributions: package and executable (app). - -To build the package, ``flit`` is used and at the time of writing it doesn't offer an option to -specify the output directory and it's always ``dist`` (``BUILD_DIST_DIR``) - -To build the executable, ``pyinstaller`` is used and it does allow to specify the output directory. - -To avoid mixing the files when creating both types of distributions, the package files will be in -``BUILD_DIST_DIR`` and the executable files in ``BUILD_DIST_APP_DIR``. -""" -# endregion - - -def _csstr_to_list(csstr: str) -> list[str]: - """ - Convert a comma-separated string to list. - """ - return [s.strip() for s in csstr.split(',')] - - -def _get_requirements_file(requirements: str, extension: str) -> str: - """ - Return the full requirements file name (with extension). - - :param requirements: The requirements file to retrieve. Can be the whole filename - (no extension), ex `'requirements-dev'` or just the initial portion, ex `'dev'`. - Use `'main'` for the `requirements` file. - :param extension: Requirements file extension. Can be either `'in'` or `'txt'`. - """ - filename = REQUIREMENTS_FILES.get(requirements, requirements) - if filename not in REQUIREMENTS_FILES.values(): - raise Exit(f'`{requirements}` is an unknown requirements file.') - - return f'{filename}.{extension.lstrip(".")}' - - -def _get_requirements_files(requirements: str | None, extension: str) -> list[str]: - extension = extension.lstrip('.') - if requirements is None: - requirements_files = list(REQUIREMENTS_FILES) - else: - requirements_files = _csstr_to_list(requirements) - - # Get full filename+extension and sort by the order defined in `REQUIREMENTS_FILES` - filenames = [ - _get_requirements_file(r, extension) for r in REQUIREMENTS_FILES if r in requirements_files - ] - - return filenames - - -def _get_os_name(): - """User-friendly OS name (lowercased).""" - import platform - - system = platform.system().lower() - return {'darwin': 'mac'}.get(system, system) - - -def _get_build_app_files() -> tuple[Path, Path]: - import hd_active - - version = hd_active.__version__ - - # Assumes the distribution directory is empty prior to creating the app - files = [f for f in BUILD_DIST_APP_DIR.glob('*') if f.is_file() and f.suffix.lower() != '.zip'] - if not files: - raise Exit(f'App file not found in {BUILD_DIST_APP_DIR}') - if len(files) > 1: - raise Exit( - f'One file expected in the distribution folder {BUILD_DIST_APP_DIR}.\n' - f'{len(files)} files found:\n' + '\n'.join(str(file) for file in files) - ) - app_file = files[0] - zip_file = BUILD_DIST_APP_DIR / f'{app_file.stem}_{version}_{_get_os_name()}.zip' - - return app_file, zip_file - - -def _get_project_version() -> str: - import re - - pattern = re.compile('''^[ _]*version[ _]*[:=] *['"](.*)['"]''', re.MULTILINE) - versions = {} - for file in VERSION_FILES: - with open(file) as f: - text = f.read() - match = pattern.search(text) - if not match: - raise Exit(f'Could not find version in `{file.relative_to(PROJECT_ROOT)}`.') - versions[file] = match.group(1) - - if len(set(versions.values())) != 1: - raise Exit( - 'Version mismatch in files that contain versions.\n' - + ( - '\n'.join( - f'{file.relative_to(PROJECT_ROOT)}: {version}' - for file, version in versions.items() - ) - ) - ) - - return list(versions.values())[0] - - -def _get_next_version(current_version, part): - from packaging.version import Version - - version = Version(str(current_version)) - - if part == 'major': - new_version = Version(f'{version.major + 1}.0.0') - elif part == 'minor': - new_version = Version(f'{version.major}.{version.minor + 1}.0') - elif part == 'patch': - new_version = Version(f'{version.major}.{version.minor}.{version.micro + 1}') - else: - raise ValueError('`part` must be "major", "minor", or "patch"') - - return new_version - - -def _re_sub_file(file: str | Path, regex: str, repl: str, save: bool = True) -> str: - """ - Regex search/replace text in a file. - - :param file: File to update. - :param regex: Regex pattern, as a string. - The regex needs to return 3 capturing groups: text before, text to replace, text after - (per line). - :param repl: Text to replace with. - :param save: Whether to save the file with the new text. - :return: Updated text. - """ - import re - - pattern = re.compile(regex, re.MULTILINE) - with open(file) as f: - text = f.read() - new_text = pattern.sub(lambda match: f'{match.group(1)}{repl}{match.group(3)}', text) - - if save: - with open(file, 'w') as f: - f.write(new_text) - - return new_text - - -def _update_project_version(version: str): - regex = r'''^([ _]*version[ _]*[:=] *['"])(.*)(['"].*)$''' - for file in VERSION_FILES: - _re_sub_file(file, regex, version) - - -def _get_release_name_and_tag(version: str) -> tuple[str, str]: - """ - Generate release name and tag based on the version. - - :return: Tuple with release name (ex 'v1.2.3') and tag (ex '1.2.3'). - """ - return f'v{version}', version - - -def _get_version_from_release_name(release_name: str) -> str: - if not release_name.startswith('v'): - raise Exit(f'Invalid release name: {release_name}') - return release_name[1:] - - -def _get_latest_release(c) -> tuple[str, str, list[dict]]: - """ - Retrieves the latest release from GitHub. - - :return: Tuple with: release name (ex 'v1.2.3'), tag (ex '1.2.3') and list of assets uploaded. - """ - import json - - release_info_json = c.run('gh release view --json name,tagName,assets').stdout.strip() - release_info = json.loads(release_info_json) - return release_info['name'], release_info['tagName'], release_info['assets'] - - -def _module_path_from_file(file: Path, base_dir: Path) -> str: - if file.is_file(): - _dir = file.parent - elif file.is_dir(): - _dir = file - else: - raise Exit(f'File {file} is not a file or directory.') - - return str(_dir.relative_to(base_dir)).replace(os.sep, '.').strip('.') - - -def _update_imports(): - """ - Update the relative imports in the project to absolute imports. - - When being used as a package, this project works with relative imports, but as an app built - with PyInstaller, relative imports do not work. - """ - import shutil - - # Copy code to build dir - build_source_dir = BUILD_WORK_APP_DIR / PROJECT_SOURCE_DIR.relative_to(PROJECT_ROOT) - shutil.copytree( - PROJECT_SOURCE_DIR, build_source_dir, ignore=shutil.ignore_patterns('__pycache__') - ) - - # Update imports - for root, dirs, files in os.walk(build_source_dir): - root_path = Path(root) - module = _module_path_from_file(root_path, build_source_dir.parent) - for file in files: - file_path = root_path / file - - regex_replace = [ - (r'''^( *from[ ]+)(\.{1})( .*)''', module), # from . import - ( - r'''^( *from[ ]+)(\.{2})( .*)''', - '.'.join(module.split('.')[:-1]), - ), # from .. import - ( - r'''^( *from[ ]+)(\.{3})( .*)''', - '.'.join(module.split('.')[:-2]), - ), # from ... import - ( - r'''^( *from[ ]+)(\.{3})(.*)''', - '.'.join(module.split('.')[:-2]) + '.', - ), - ( - r'''^( *from[ ]+)(\.{2})(.*)''', - '.'.join(module.split('.')[:-1]) + '.', - ), - ( - r'''^( *from[ ]+)(\.{1})(.*)''', - module + '.', - ), - ] - for regex in regex_replace: - _re_sub_file(file_path, regex[0], regex[1]) - - -def _get_branch(c): - """Returns the current branch.""" - return c.run('git branch --show-current').stdout.strip() - - -def _get_default_branch(c): - """Returns the default branch (usually ``main``).""" - return c.run('gh repo view --json defaultBranchRef --jq .defaultBranchRef.name').stdout.strip() - - -def _commit(c, message: str): - - # Commit - c.run('git add ' + ' '.join(f'"{file}"' for file in VERSION_FILES)) - c.run(f'git commit -m "{message}"') - - # Push current branch - branch = _get_branch(c) - c.run(f'git push origin {branch}') - - -def _create_pr(c, title: str, description: str): - """ - Creates a PR in GitHub and merges it after checks pass. - - If checks fail, the PR will remain open and will need to be dealt with manually. - """ - # Create PR - default_branch = _get_default_branch(c) - branch = _get_branch(c) - c.run( - f'gh pr create --title "{title}" --body "{description}" ' - f'--head {branch} --base {default_branch}' - ) - - # Merge PR after checks pass - c.run(f'gh pr merge {branch} --squash --auto') - - -@task -def build_clean(c): - """ - Delete files created from previous builds (`build` and `dist` folders). - """ - import shutil - - # From building the package and/or executable - for d in [BUILD_WORK_DIR, BUILD_DIST_DIR]: - shutil.rmtree(d, ignore_errors=True) - - # From building the package - shutil.rmtree(PROJECT_ROOT / f'{PROJECT_NAME}.egg-info', ignore_errors=True) - - -@task( - help={ - 'version': 'Version in semantic versioning format (ex 1.5.0). ' - 'If `version` is set, then `bump` cannot be used.', - 'bump': 'Portion of the version to increase, can be "major", "minor", or "patch".\n' - 'If `bump` is set, then `version` cannot be used.', - 'mode': 'What do do after the files are updated:\n"nothing": do nothing and the changes ' - 'are not committed (default).\n"commit": commit and push the changes with the message ' - '"bump version".\n"pr": Commit, push, create and merge PR after checks pass.', - 'yes': 'Don\'t ask confirmation to create new branch if necessary.', - }, -) -def build_version(c, version: str = '', bump: str = '', mode: str = 'nothing', yes: bool = False): - """ - Updates the files that contain the project version to the new version. - - Optionally, commit the changes, create a PR and merge it after checks pass. - """ - from packaging.version import Version - - mode = mode.strip().lower() - if mode not in ['nothing', 'commit', 'pr']: - raise Exit('Invalid `mode` choice.') - - v1 = Version(_get_project_version()) - if version and bump: - raise Exit('Either `version` or `bump` can be set, not both.') - if not (version or bump): - try: - bump = {'1': 'major', '2': 'minor', '3': 'patch'}[ - input( - f'Current version is `{v1}`, which portion to bump?' - '\n1 - Major\n2 - Minor\n3 - Patch\n> ' - ) - ] - except KeyError: - raise Exit('Invalid choice') - - if version: - v2 = Version(version) - if v2 <= v1: - raise Exit(f'New version `{v2}` needs to be greater than the existing version `{v1}`.') - else: - try: - v2 = _get_next_version(v1, bump.strip().lower()) - except AttributeError: - raise Exit('Invalid `bump` choice.') - - # Verify branch is not default - branch = _get_branch(c) - default_branch = _get_default_branch(c) - if branch == default_branch: - branch_ok = False - if yes or input( - f'Current branch `{branch}` is the default branch, create new branch? [Y/n] ' - ).strip().lower() in ['', 'y', 'yes']: - c.run(f'git checkout -b release-{v2}') - branch_ok = True - if not branch_ok: - raise Exit(f'Cannot make changes in the default branch `{branch}`.') - - # Update files to new version - _update_project_version(str(v2)) - print( - f'New version is `{v2}`. Modified files :\n' - + '\n'.join(f' {file.relative_to(PROJECT_ROOT)}' for file in VERSION_FILES) - ) - - # Commit/push/pr - if mode == 'nothing': - print('Files not committed, PR not created.') - if mode in ['commit', 'pr']: - print('Commit and push changes.') - _commit(c, f'bump version to {v2}') - if mode == 'pr': - pr_title = f'Release {v2}' - print(f'Create and merge PR `{pr_title}`.') - _create_pr(c, pr_title, f'Preparing for release {v2}') - - -@task( - build_clean, - help={ - 'no_spec': f'Do not use the spec file `{BUILD_SPEC_FILE.relative_to(PROJECT_ROOT)}` and ' - f'create one in the `{BUILD_WORK_DIR.relative_to(PROJECT_ROOT)}` directory with defaults.', - 'no_zip': 'Do not create a ZIP file, which can be used to upload to a GitHub release.', - }, -) -def build_app(c, no_spec: bool = False, no_zip: bool = False): - """ - Build the executable (app) file(s). - """ - _update_imports() - - # Build executable - if no_spec: - build_source_dir = BUILD_WORK_APP_DIR / PROJECT_SOURCE_DIR.relative_to(PROJECT_ROOT) - build_input_file = build_source_dir / PROJECT_NAME / 'main.py' - c.run( - f'pyinstaller ' - f'--onefile "{build_input_file}" --distpath "{BUILD_DIST_APP_DIR}" ' - f'--workpath "{BUILD_WORK_APP_DIR}" --specpath "{BUILD_WORK_APP_DIR}"' - ) - else: - c.run( - f'pyinstaller "{BUILD_SPEC_FILE}" ' - f'--distpath "{BUILD_DIST_APP_DIR}" --workpath "{BUILD_WORK_APP_DIR}"' - ) - - app_file, zip_file = _get_build_app_files() - - # Zip file - if no_zip: - print('ZIP file not created.') - else: - import zipfile - - with zipfile.ZipFile(zip_file, 'w') as f: - f.write(app_file, arcname=app_file.name) - - print(f'App files created in {BUILD_DIST_APP_DIR}') - - -@task( - help={ - 'no_upload': 'Do not upload to Pypi.', - 'yes': 'Don\'t request confirmation to publish to Pypi.', - }, -) -def build_publish(c, no_upload: bool = False, yes: bool = False): - """ - Build package and publish (upload) to Pypi. - - Output in `dist` folder. - """ - # Create distribution files (source and wheel) - c.run('flit build') - # Upload to pypi - if not no_upload: - if ( - yes - or input(f'Publishing version `{_get_project_version()}` to Pypi. Press Y to confirm. ') - .strip() - .lower() - == 'y' - ): - c.run('flit publish') - else: - print('Package not published to Pypi.') - - -@task( - help={ - 'notes': 'Release notes.', - 'notes_file': 'Read release notes from file. Ignores the `-notes` parameter.', - 'yes': 'Don\'t request confirmation to create the release.', - }, -) -def build_release( - c, - notes: str = '', - notes_file: str = '', - yes: bool = False, -): - """ - Create a release and tag in GitHub from the current project version. - - Does not upload artifacts (executable/zip) to the release. Use `build.upload` for that. - """ - from packaging.version import Version - - if notes and notes_file: - raise Exit('Both `--notes` and `--notes-file` are specified. Only one can be specified.') - - if not notes and not notes_file and not yes: - response = input('No release notes or notes file specified, continue? [Y/n]') - response = response.strip().lower() or 'y' - if response not in ['yes', 'y']: - raise Exit('No release notes specified.') - - # Check that there's no release with the current version - version = Version(_get_project_version()) - latest_release, latest_tag, _ = _get_latest_release(c) - latest_version = Version(_get_version_from_release_name(latest_release)) - if str(latest_version) != latest_tag: - raise Exit( - f'Invalid format in latest release or tag: Release: {latest_release}, Tag: {latest_tag}' - ) - - if latest_version >= version: - raise Exit( - f'Release/tag version being created ({version}) needs to be greater than the current ' - f'latest release version ({latest_version}).' - ) - - # Create release (zip file not uploaded) - new_release, new_tag = _get_release_name_and_tag(str(version)) - command = f'gh release create "{new_tag}" --title "{new_release}" --generate-notes' - if notes: - command += f' --notes "{notes}"' - if notes_file: - notes_file_path = Path(notes_file) - command += f' --notes-file "{notes_file_path.resolve(strict=True)}"' - - if ( - yes - or input(f'Creating GitHub release `{new_release}`. Press Y to confirm. ').strip().lower() - == 'y' - ): - c.run(command) - print('GitHub release created. Upload artifacts with `build.upload`.') - else: - print('GitHub release not created.') - - -@task( - help={ - 'label': 'The label that will be displayed in GitHub next to the artifact. The special ' - 'strings "auto" and "none" mean that the label is to be autogenerated (OS specific) or no ' - 'label is attached, respectively. Any other string is what\'s used as label. Use the ' - '`--dry` option to see the label without uploading the artifact.' - }, -) -def build_upload(c, label: str = 'none'): - """ - Upload asset to the GitHub release in the manifest file. - The artifact being uploaded is the Zip file with the executable binary for the current OS. - The release the artifact is uploaded to is specified in the manifest file inside the Zip file. - - The following must already exist: - * The artifact (`inv build.exe`). - * The release in GitHub (`inv build.release`). - """ - from packaging.version import Version - - import hd_active - - _, zip_file = _get_build_app_files() - app_version = Version(hd_active.__version__) - - if not zip_file.exists(): - raise Exit( - f'Zip file not found: {zip_file}\n' - 'Rebuild the app with `inv build.dist` and without the `--no-zip` option.' - ) - - # Verify asset is being uploaded to the correct GH release - latest_release, latest_tag, assets = _get_latest_release(c) - latest_version = Version(_get_version_from_release_name(latest_release)) - if app_version != latest_version: - raise Exit( - f'App version `{app_version}` does not match ' - f'the latest release in GitHub `{latest_version}`.`' - ) - - # Verify asset does not yet exist in the GH release - asset = next((asset for asset in assets if asset['name'] == zip_file.name), None) - if asset: - raise Exit( - f'File `{zip_file.name}` already exists in release `{latest_release}`.\n' - 'To re-upload, the file needs to be deleted from the release first.' - ) - - # Create label - if label.lower() == 'auto': - label = f'#{_get_os_name().title()}' - elif label.lower() == 'none': - label = '' - else: - label = f'#{label}' - - # Upload file - print( - f'Uploading `{zip_file.name}` to release `{latest_release}`' - + (f' with label `{label}`' if label else '') - ) - command = f'gh release upload "{latest_tag}" "{zip_file}{label}"' - - c.run(command) - - -@task -def build_run(c): - """ - Run the built package. - """ - os_name = _get_os_name() - - if os_name == 'windows': - exes = list(BUILD_DIST_APP_DIR.glob('**/*.exe')) - if len(exes) == 0: - raise Exit('No executable found.') - elif len(exes) > 1: - raise Exit('Multiple executables found.') - c.run(str(exes[0])) - elif os_name == 'mac': - app_file, _ = _get_build_app_files() - c.run(str(app_file)) - elif os_name == 'linux': - raise Exit('Running on Linux not yet implemented.') - else: - raise Exit(f'Running on {os_name.title()} is not supported.') - - -@task( - help={ - 'file': '`.ui` file to be converted to `.py`. `.ui` extension not required. ' - 'Can be a comma separated list. If not supplied, all files will be converted. ' - f'Available files: {", ".join(p.stem for p in UI_FILES)}.' - } -) -def ui_py(c, file=None): - """ - Convert Qt `.ui` files into `.py`. - """ - if file: - file_stems = [ - (_f2[:-3] if _f2.lower().endswith('.ui') else _f2) - for _f2 in [_f1.strip() for _f1 in file.split(',')] - ] - else: - file_stems = [p.stem for p in UI_FILES] - - for file_stem in file_stems: - try: - file_path_in = next(p for p in UI_FILES if p.stem == file_stem) - except StopIteration: - raise Exit( - f'File "{file}" not found. Available files: {", ".join(p.stem for p in UI_FILES)}' - ) - - file_path_out = SOURCE_DIR / 'ui/forms' / f'ui_{file_stem}.py' - - c.run(f'pyside6-uic {file_path_in} -o {file_path_out} --from-imports') - - -@task( - help={ - 'file': '`.qrc` file to be converted to `.py`. `.qrc` extension not required. ' - 'Can be a coma separated list of filenames. If not supplied, all files will be converted. ' - f'Available files: {", ".join(p.stem for p in QRC_FILES)}.' - } -) -def ui_rc(c, file=None): - """ - Convert Qt `.qrc` files into `.py`. - """ - if file: - file_stems = [ - (_f2[:-4] if _f2.lower().endswith('.qrc') else _f2) - for _f2 in [_f1.strip() for _f1 in file.split(',')] - ] - else: - file_stems = [p.stem for p in QRC_FILES] - - for file_stem in file_stems: - try: - file_path_in = next(p for p in QRC_FILES if p.stem == file_stem) - except StopIteration: - raise Exit( - f'File "{file}" not found. Available files: {", ".join(p.stem for p in QRC_FILES)}' - ) - - file_path_out = SOURCE_DIR / 'ui/forms' / f'{file_stem}_rc.py' - - c.run(f'pyside6-rcc {file_path_in} -o {file_path_out}') - - -@task( - help={ - 'file': f'`.ui` file to be edited. Available files: {", ".join(p.stem for p in UI_FILES)}.' - } -) -def ui_edit(c, file): - """ - Edit a file in QT Designer. - """ - file_stem = file[:-3] if file.lower().endswith('.ui') else file - try: - ui_file_path = next(p for p in UI_FILES if p.stem == file_stem) - except StopIteration: - raise Exit( - f'File "{file}" not found. Available files: {", ".join(p.stem for p in UI_FILES)}' - ) - - c.run(f'pyside6-designer {ui_file_path}', asynchronous=True) - - -@task -def lint_black(c, path='.'): - c.run(f'black {path}') - - -@task -def lint_flake8(c, path='.'): - c.run(f'flake8 {path}') - - -@task -def lint_isort(c, path='.'): - c.run(f'isort {path}') - - -@task -def lint_mypy(c, path='.'): - c.run(f'mypy {path}') - - -@task -def lint_all(c): - """ - Run all linters. - Config for each of the tools is in ``pyproject.toml``. - """ - lint_isort(c) - lint_black(c) - lint_flake8(c) - lint_mypy(c, 'src') - lint_mypy(c, 'tests') - print('Done') - - -@task -def test_unit(c): - """ - Run unit tests. - """ - c.run('python -m pytest') - - -@task(help=REQUIREMENTS_TASK_HELP) -def pip_compile(c, requirements=None): - """ - Compile requirements file(s). - """ - for filename in _get_requirements_files(requirements, 'in'): - c.run(f'pip-compile {filename}') - - -@task(help=REQUIREMENTS_TASK_HELP) -def pip_sync(c, requirements=None): - """ - Synchronize environment with requirements file. - """ - c.run(f'pip-sync {" ".join(_get_requirements_files(requirements, "txt"))}') - - -@task( - help=REQUIREMENTS_TASK_HELP | {'package': 'Package to upgrade. Can be a comma separated list.'} -) -def pip_package(c, requirements, package): - """ - Upgrade package. - """ - packages = [p.strip() for p in package.split(',')] - for filename in _get_requirements_files(requirements, 'in'): - c.run(f'pip-compile --upgrade-package {" --upgrade-package ".join(packages)} {filename}') - - -@task(help=REQUIREMENTS_TASK_HELP) -def pip_upgrade(c, requirements=None): - """ - Try to upgrade all dependencies to their latest versions. - """ - for filename in _get_requirements_files(requirements, 'in'): - c.run(f'pip-compile --upgrade {filename}') - - -@task -def precommit_install(c): - """ - Install pre-commit into the git hooks, which will cause pre-commit to run on automatically. - This should be the first thing to do after cloning this project and installing requirements. - """ - c.run('pre-commit install') - - -@task -# `upgrade` instead of `update` to maintain similar naming to `pip-compile upgrade` -def precommit_upgrade(c): - """ - Upgrade pre-commit config to the latest repos' versions. - """ - c.run('pre-commit autoupdate') - - -@task(help={'hook': 'Name of hook to run. Default is to run all.'}) -def precommit_run(c, hook=None): - """ - Manually run pre-commit hooks. - """ - hook = hook or '--all-files' - c.run(f'pre-commit run {hook}') - - -@task -def docs_serve(c): - """ - Start documentation local server. - """ - c.run('mkdocs serve') - - -@task -def docs_deploy(c): - """ - Publish documentation to GitHub Pages at https://joaonc.github.io/hd_active - """ - c.run('mkdocs gh-deploy') - - -@task -def docs_clean(c): - """ - Delete documentation website static files. - """ - import shutil - - shutil.rmtree(PROJECT_ROOT / 'site', ignore_errors=True) - - -ns = Collection() # Main namespace - -test_collection = Collection('test') -test_collection.add_task(test_unit, 'unit') - -build_collection = Collection('build') -build_collection.add_task(build_clean, 'clean') -build_collection.add_task(build_version, 'version') -build_collection.add_task(build_app, 'app') -build_collection.add_task(build_release, 'release') -build_collection.add_task(build_run, 'run') -build_collection.add_task(build_upload, 'upload') -build_collection.add_task(build_publish, 'publish') - -lint_collection = Collection('lint') -lint_collection.add_task(lint_all, 'all') -lint_collection.add_task(lint_black, 'black') -lint_collection.add_task(lint_flake8, 'flake8') -lint_collection.add_task(lint_isort, 'isort') -lint_collection.add_task(lint_mypy, 'mypy') - -pip_collection = Collection('pip') -pip_collection.add_task(pip_compile, 'compile') -pip_collection.add_task(pip_package, 'package') -pip_collection.add_task(pip_sync, 'sync') -pip_collection.add_task(pip_upgrade, 'upgrade') - -precommit_collection = Collection('precommit') -precommit_collection.add_task(precommit_run, 'run') -precommit_collection.add_task(precommit_install, 'install') -precommit_collection.add_task(precommit_upgrade, 'upgrade') - -docs_collection = Collection('docs') -docs_collection.add_task(docs_serve, 'serve') -docs_collection.add_task(docs_deploy, 'deploy') -docs_collection.add_task(docs_clean, 'clean') - -ui_collection = Collection('ui') -ui_collection.add_task(ui_py, 'py') -ui_collection.add_task(ui_rc, 'rc') -ui_collection.add_task(ui_edit, 'edit') - -ns.add_collection(build_collection) -ns.add_collection(docs_collection) -ns.add_collection(lint_collection) -ns.add_collection(pip_collection) -ns.add_collection(precommit_collection) -ns.add_collection(test_collection) -ns.add_collection(ui_collection) From fce6def1171bf6d931bb0efbd4735a30cb34a366 Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sun, 8 Mar 2026 10:48:58 -0500 Subject: [PATCH 11/16] min python 3.12 --- pyproject.toml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 13767b4..0bdf29a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ [tool.black] line-length = 100 -target-version = ['py311'] +target-version = ['py312'] skip-string-normalization = true extend-exclude=''' ( @@ -28,7 +28,7 @@ testpaths = ['tests'] # 1 The regex for all folders needs to be in a one line string. # 2 The `.` doesn't need to be escaped. Escape with `\\.` for a fully compatible regex. exclude = '^venv*|^.venv*|.git|.eggs|build|dist|.cache|.pytest_cache|.mypy_cache|.vscode|.idea|tasks.py|src/ui/forms' -python_version = '3.11' +python_version = '3.12' warn_return_any = true warn_unused_configs = true # Let mypy find modules inside the 'src' directory when importing 'hd_active'. @@ -39,7 +39,7 @@ disable_error_code = 'annotation-unchecked' [[tool.mypy.overrides]] ignore_missing_imports = true -module = ['invoke', 'factory', 'pytest_check', 'pytest_params', 'PySide6.*'] +module = ['factory', 'pytest_check', 'pytest_params', 'PySide6.*'] [tool.typer-invoke] # Paths from where `pyproject.toml` is located (root of project) @@ -70,7 +70,7 @@ readme = 'README.md' authors = [{name = 'Joao Coelho'}] license = {file = 'LICENSE.txt'} dependencies = ['pyside6'] -requires-python = '>=3.10,<=3.14' +requires-python = '>=3.12,<=3.15' classifiers = [ "License :: OSI Approved :: MIT License", ] @@ -94,9 +94,9 @@ exclude = [ ] # Errors being ignored: # - E203 is not PEP8 compliant and clashes with black -# - E701,E704 multiple statements on one line (colon) +# - E701 multiple statements on one line (colon) # Conflict with black, where empty class definitions (with Ellipsis) are formatted to be in # one line in black, ex: class FileUpdateError(Exception): ... # - W503 line break before binary operator clashes with black # - F811 redefinition of unused import happens when importing pytest fixtures in test modules -ignore = ['E203', 'E701', 'E704', 'W503', 'F811'] +ignore = ['E203', 'E701', 'W503', 'F811'] From f32c8ec71660c260640a0c2b6dfb14189e67dc7f Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sun, 8 Mar 2026 11:27:33 -0500 Subject: [PATCH 12/16] adopt uv and ruff --- .pre-commit-config.yaml | 13 ++-- admin/build.py | 16 +++++ admin/lint.py | 25 +++----- admin/pip.py | 74 ++++++++++++++-------- admin/requirements/requirements-dev.in | 10 +-- admin/requirements/requirements-dev.txt | 80 +++++------------------- admin/requirements/requirements-docs.txt | 34 +++------- pyproject.toml | 62 ++++++++---------- 8 files changed, 129 insertions(+), 185 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d0d8e2f..6264c25 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,20 +2,15 @@ default_language_version: python: python3.12 default_stages: [ push ] repos: -- repo: https://github.com/psf/black - rev: 25.1.0 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.5 hooks: - - id: black + - id: ruff-check + - id: ruff-format args: [ --check ] -- repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - args: [ --check-only ] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.2.0 hooks: - id: check-added-large-files - id: debug-statements - - id: double-quote-string-fixer - id: end-of-file-fixer diff --git a/admin/build.py b/admin/build.py index c41f262..5ca9434 100644 --- a/admin/build.py +++ b/admin/build.py @@ -191,6 +191,22 @@ def build_clean(): shutil.rmtree(BUILD_DIST_DIR, ignore_errors=True) +@app.command(name='publish') +def build_publish( + upload: Annotated[bool, typer.Option(help='Upload to PyPI after build.')] = True, + yes: Annotated[bool, typer.Option(help='Skip publish confirmation.')] = False, + dry: DryAnnotation = False, +): + run('uv', 'build', dry=dry) + if not upload: + return + msg = f'Publishing version `{_get_project_version()}` to PyPI. Press Y to confirm. ' + if yes or input(msg).strip().lower() == 'y': + run('uv', 'publish', dry=dry) + else: + logger.info('Package not published to PyPI.') + + @app.command(name='version') def build_version( version: Annotated[ diff --git a/admin/lint.py b/admin/lint.py index f971e9f..005c590 100644 --- a/admin/lint.py +++ b/admin/lint.py @@ -15,19 +15,14 @@ ) -@app.command(name='black') -def lint_black(path='.', dry: DryAnnotation = False): - run('black', path, dry=dry) - - -@app.command(name='flake8') -def lint_flake8(path='.', dry: DryAnnotation = False): - run('flake8', path, dry=dry) - - -@app.command(name='isort') -def lint_isort(path='.', dry: DryAnnotation = False): - run('isort', path, dry=dry) +@app.command(name='ruff') +def lint_ruff(path='.', check: bool = False, dry: DryAnnotation = False): + if check: + run('ruff', 'check', path, dry=dry) + run('ruff', 'format', '--check', path, dry=dry) + else: + run('ruff', 'check', '--fix', path, dry=dry) + run('ruff', 'format', path, dry=dry) @app.command(name='mypy') @@ -47,9 +42,7 @@ def lint_all(dry: DryAnnotation = False): Config for each of the tools is in ``pyproject.toml``. """ - lint_isort(dry=dry) - lint_black(dry=dry) - lint_flake8(dry=dry) + lint_ruff(check=True, dry=dry) lint_mypy(dry=dry) logger.info('Done') diff --git a/admin/pip.py b/admin/pip.py index 2714cdc..902fd43 100644 --- a/admin/pip.py +++ b/admin/pip.py @@ -2,6 +2,7 @@ """ Python packages related tasks. """ + from enum import StrEnum from pathlib import Path from typing import Annotated @@ -9,7 +10,7 @@ import typer from admin import PROJECT_ROOT -from admin.utils import DryAnnotation, install_package, logger, multiple_parameters, run +from admin.utils import DryAnnotation, logger, multiple_parameters, run REQUIREMENTS_DIR = PROJECT_ROOT / 'admin' / 'requirements' @@ -40,12 +41,6 @@ class RequirementsType(StrEnum): OUT = 'txt' -REQUIREMENTS_TASK_HELP = { - 'requirements': '`.in` file. Full name not required, just the initial name after the dash ' - f'(ex. "{Requirements.DEV.name}"). For main file use "{Requirements.MAIN.name}". ' - f'Available requirements: {", ".join(Requirements)}.' -} - RequirementsAnnotation = Annotated[ list[str] | None, typer.Argument( @@ -65,7 +60,7 @@ def _get_requirements_file( else: try: reqs = Requirements[requirements.upper()] # noqa - except ValueError: + except KeyError: try: reqs = Requirements(requirements.lower()) except ValueError: @@ -77,13 +72,13 @@ def _get_requirements_file( else: reqs_type = RequirementsType(requirements_type.lstrip('.').lower()) - return REQUIREMENTS_DIR / f'{reqs}.{reqs_type}' + return REQUIREMENTS_DIR / f'{reqs.value}.{reqs_type.value}' def _get_requirements_files( requirements: list[str | Requirements] | None, requirements_type: str | RequirementsType ) -> list[Path]: - """Get full filename+extension and sort by the order defined in ``Requirements``""" + """Get full filename+extension and sort by the order defined in ``Requirements``.""" requirements_files = list(Requirements) if requirements is None else requirements return [_get_requirements_file(r, requirements_type) for r in requirements_files] @@ -95,7 +90,7 @@ def pip_compile( bool, typer.Option( help=f'Delete the existing requirements `{RequirementsType.OUT.value}` files, forcing ' - f'a clean compilation.' + f'a clean compilation.', ), ] = False, dry: DryAnnotation = False, @@ -103,16 +98,23 @@ def pip_compile( """ Compile requirements file(s). """ - install_package('piptools', 'pip-tools', dry=dry) - if clean and not dry: for filename in _get_requirements_files(requirements, RequirementsType.OUT): filename.unlink(missing_ok=True) - dry_option = ['--dry-run'] if dry else [] for filename in _get_requirements_files(requirements, RequirementsType.IN): + output_file = filename.with_suffix('.txt') run( - 'pip-compile', '--no-header', '--no-strip-extras', *dry_option, str(filename), dry=False + 'uv', + 'pip', + 'compile', + '--no-header', + '--no-strip-extras', + filename.name, + '-o', + output_file.name, + dry=dry, + cwd=REQUIREMENTS_DIR, ) @@ -121,8 +123,7 @@ def pip_sync(requirements: RequirementsAnnotation = None, dry: DryAnnotation = F """ Synchronize environment with requirements file. """ - install_package('piptools', 'pip-tools', dry=dry) - run('pip-sync', *_get_requirements_files(requirements, RequirementsType.OUT), dry=dry) + run('uv', 'pip', 'sync', *_get_requirements_files(requirements, RequirementsType.OUT), dry=dry) @app.command(name='package') @@ -136,31 +137,52 @@ def pip_package( """ Upgrade one or more packages. """ - install_package('piptools', 'pip-tools', dry=dry) - for filename in _get_requirements_files(requirements, RequirementsType.IN): + output_file = filename.with_suffix('.txt') run( - 'pip-compile', + 'uv', + 'pip', + 'compile', *multiple_parameters('--upgrade-package', *package), - filename, + str(filename), + '-o', + str(output_file), dry=dry, ) @app.command(name='upgrade') -def pip_upgrade(requirements, dry: DryAnnotation = False): +def pip_upgrade(requirements: RequirementsAnnotation = None, dry: DryAnnotation = False): """ Try to upgrade all dependencies to their latest versions. Equivalent to ``compile`` with ``--clean`` option. Use ``package`` to only upgrade individual packages, - Ex ``pip package dev mypy flake8``. + Ex ``pip package dev mypy ruff``. """ - install_package('piptools', 'pip-tools', dry=dry) - for filename in _get_requirements_files(requirements, RequirementsType.IN): - run(['pip-compile', '--no-strip-extras', '--upgrade', filename], dry=dry) + output_file = filename.with_suffix('.txt') + run( + 'uv', + 'pip', + 'compile', + '--no-strip-extras', + '--upgrade', + str(filename), + '-o', + str(output_file), + dry=dry, + ) + + +@app.command(name='install') +def pip_install(requirements: RequirementsAnnotation, dry: DryAnnotation = False): + """ + Equivalent to ``uv pip install -r ``. + """ + requirements_files = _get_requirements_files(requirements, RequirementsType.OUT) # type: ignore + run('uv', 'pip', 'install', *multiple_parameters('-r', *requirements_files), dry=dry) if __name__ == '__main__': diff --git a/admin/requirements/requirements-dev.in b/admin/requirements/requirements-dev.in index e557958..fd461ad 100644 --- a/admin/requirements/requirements-dev.in +++ b/admin/requirements/requirements-dev.in @@ -1,16 +1,14 @@ -r requirements.txt # Dev tools -pip-tools # Package management pre-commit # Pre-commit hooks +typer # CLI framework used by typer-invoke typer-invoke # Tasks +uv # Package management/build/publish # Linting bandit # Linter -black # Linter -flake8 # Linter -flake8-pyproject # Flake8 plugin -isort # Linter +ruff # Linter and formatter mypy # Code inspector # Test @@ -18,8 +16,6 @@ pytest # Test framework pytest-params # Easier test case parameters pytest-qt # Qt plugin to allow user interaction -# Build and publish -flit # Build, install and upload to Pypi # Installer pyinstaller # Installer framework diff --git a/admin/requirements/requirements-dev.txt b/admin/requirements/requirements-dev.txt index f32e81a..1300945 100644 --- a/admin/requirements/requirements-dev.txt +++ b/admin/requirements/requirements-dev.txt @@ -4,88 +4,48 @@ annotated-doc==0.0.4 # via typer bandit==1.9.4 # via -r requirements-dev.in -black==26.3.0 - # via -r requirements-dev.in -build==1.4.0 - # via pip-tools -certifi==2026.2.25 - # via requests cfgv==3.5.0 # via pre-commit -charset-normalizer==3.4.5 - # via requests click==8.3.1 - # via - # black - # pip-tools - # typer + # via typer colorama==0.4.6 # via # bandit - # build # click # pytest distlib==0.4.0 # via virtualenv -docutils==0.22.4 - # via flit filelock==3.25.0 # via # python-discovery # virtualenv -flake8==7.3.0 - # via - # -r requirements-dev.in - # flake8-pyproject -flake8-pyproject==1.2.4 - # via -r requirements-dev.in -flit==3.12.0 - # via -r requirements-dev.in -flit-core==3.12.0 - # via flit identify==2.6.17 # via pre-commit -idna==3.11 - # via requests iniconfig==2.3.0 # via pytest -isort==8.0.1 - # via -r requirements-dev.in librt==0.8.1 # via mypy markdown-it-py==4.0.0 # via rich -mccabe==0.7.0 - # via flake8 mdurl==0.1.2 # via markdown-it-py mypy==1.19.1 # via -r requirements-dev.in mypy-extensions==1.1.0 - # via - # black - # mypy + # via mypy nodeenv==1.10.0 # via pre-commit packaging==26.0 # via - # black - # build # pyinstaller # pyinstaller-hooks-contrib # pytest - # wheel pathspec==1.0.4 - # via - # black - # mypy + # via mypy pefile==2024.8.26 # via pyinstaller -pip-tools==7.5.3 - # via -r requirements-dev.in platformdirs==4.9.4 # via - # black # python-discovery # virtualenv pluggy==1.6.0 @@ -94,10 +54,6 @@ pluggy==1.6.0 # pytest-qt pre-commit==4.5.1 # via -r requirements-dev.in -pycodestyle==2.14.0 - # via flake8 -pyflakes==3.4.0 - # via flake8 pygments==2.19.2 # via # pytest @@ -106,10 +62,6 @@ pyinstaller==6.19.0 # via -r requirements-dev.in pyinstaller-hooks-contrib==2026.2 # via pyinstaller -pyproject-hooks==1.2.0 - # via - # build - # pip-tools pyside6==6.10.2 # via -r requirements.txt pyside6-addons==6.10.2 @@ -132,21 +84,23 @@ pytest-qt==4.5.0 # via -r requirements-dev.in python-discovery==1.1.1 # via virtualenv -pytokens==0.4.1 - # via black pywin32-ctypes==0.2.3 # via pyinstaller pyyaml==6.0.3 # via # bandit # pre-commit -requests==2.32.5 - # via flit rich==14.3.3 # via # bandit # typer # typer-invoke +ruff==0.15.5 + # via -r requirements-dev.in +setuptools==82.0.0 + # via + # pyinstaller + # pyinstaller-hooks-contrib shellingham==1.5.4 # via typer shiboken6==6.10.2 @@ -157,23 +111,17 @@ shiboken6==6.10.2 # pyside6-essentials stevedore==5.7.0 # via bandit -tomli-w==1.2.0 - # via flit typer==0.24.1 - # via typer-invoke + # via + # -r requirements-dev.in + # typer-invoke typer-invoke==0.5.0 # via -r requirements-dev.in typing-extensions==4.15.0 # via # mypy # pytest-qt -urllib3==2.6.3 - # via requests +uv==0.10.9 + # via -r requirements-dev.in virtualenv==21.1.0 # via pre-commit -wheel==0.46.3 - # via pip-tools - -# The following packages are considered to be unsafe in a requirements file: -# pip -# setuptools diff --git a/admin/requirements/requirements-docs.txt b/admin/requirements/requirements-docs.txt index 3e300b1..0fe22af 100644 --- a/admin/requirements/requirements-docs.txt +++ b/admin/requirements/requirements-docs.txt @@ -1,22 +1,12 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile 'requirements-docs.in' -# babel==2.17.0 # via mkdocs-material backrefs==5.9 # via mkdocs-material certifi==2025.10.5 - # via - # -c requirements-dev.txt - # requests + # via requests charset-normalizer==3.4.4 - # via - # -c requirements-dev.txt - # requests -click==8.3.0 + # via requests +click==8.3.1 # via # -c requirements-dev.txt # mkdocs @@ -29,9 +19,7 @@ colorama==0.4.6 ghp-import==2.1.0 # via mkdocs idna==3.11 - # via - # -c requirements-dev.txt - # requests + # via requests jinja2==3.1.6 # via # mkdocs @@ -64,17 +52,17 @@ mkdocs-material==9.6.23 # via -r requirements-docs.in mkdocs-material-extensions==1.3.1 # via mkdocs-material -packaging==25.0 +packaging==26.0 # via # -c requirements-dev.txt # mkdocs paginate==0.5.7 # via mkdocs-material -pathspec==0.12.1 +pathspec==1.0.4 # via # -c requirements-dev.txt # mkdocs -platformdirs==4.5.0 +platformdirs==4.9.4 # via # -c requirements-dev.txt # mkdocs-get-deps @@ -96,16 +84,12 @@ pyyaml==6.0.3 pyyaml-env-tag==1.1 # via mkdocs requests==2.32.5 - # via - # -c requirements-dev.txt - # mkdocs-material + # via mkdocs-material selectolax==0.4.0 # via mkdocs-glightbox six==1.17.0 # via python-dateutil urllib3==2.5.0 - # via - # -c requirements-dev.txt - # requests + # via requests watchdog==6.0.0 # via mkdocs diff --git a/pyproject.toml b/pyproject.toml index 0bdf29a..2890bd0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,22 +1,27 @@ [tool.bandit] - -[tool.black] +[tool.ruff] line-length = 100 -target-version = ['py312'] -skip-string-normalization = true -extend-exclude=''' -( - src/hd_active/ui/forms - | \.venv.* - | \venv.* -) -''' +target-version = 'py312' +exclude = [ + 'src/hd_active/ui/forms', + '.venv*', + 'venv*', + '.git', + '.github', + 'assets', + 'build', + 'dist', + 'docs', + 'site', + '__pycache__', +] -[tool.isort] -extend_skip = ['src/hd_active/ui/forms'] -profile = 'black' -sections = 'FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER' -skip_glob = ['**/.venv*', '**/venv*', '/build'] +[tool.ruff.lint] +select = ['E', 'F', 'W', 'I'] +extend-ignore = ['E203', 'E701', 'F811'] + +[tool.ruff.format] +quote-style = 'single' [tool.pytest.ini_options] # Markers: See documentation on what markers represent. @@ -59,8 +64,11 @@ log_level = 'INFO' log_format = '%(message)s' [build-system] -requires = ['flit_core >=3.2,<4'] -build-backend = 'flit_core.buildapi' +requires = ['uv_build>=0.10.4,<0.11.0'] +build-backend = 'uv_build' + +[tool.uv.build-backend] +module-name = 'hd_active' [project] name = 'hd_active' @@ -82,21 +90,3 @@ Documentation = 'https://joaonc.github.io/hd_active' [project.scripts] hd_active = 'hd_active.main:main' -[tool.flit.module] -name = 'hd_active' - - -[tool.flake8] -max-line-length = 100 -exclude = [ - '.git', '.github', '.venv*', 'venv*', '__pycache__', - 'assets', 'src/hd_active/ui/forms', 'docs', 'site', 'build', -] -# Errors being ignored: -# - E203 is not PEP8 compliant and clashes with black -# - E701 multiple statements on one line (colon) -# Conflict with black, where empty class definitions (with Ellipsis) are formatted to be in -# one line in black, ex: class FileUpdateError(Exception): ... -# - W503 line break before binary operator clashes with black -# - F811 redefinition of unused import happens when importing pytest fixtures in test modules -ignore = ['E203', 'E701', 'W503', 'F811'] From d19b4a6c7ae108b3c10d646c8c964a4d2f1f3dff Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sun, 8 Mar 2026 11:31:38 -0500 Subject: [PATCH 13/16] linting --- admin/build.py | 6 +++--- admin/lint.py | 32 +++++++++++++++++++++++++++---- admin/pip.py | 1 - admin/precommit.py | 3 ++- admin/ui.py | 1 + src/hd_active/hd_active_config.py | 2 +- tests/test_hd_active_config.py | 12 ++++++------ 7 files changed, 41 insertions(+), 16 deletions(-) diff --git a/admin/build.py b/admin/build.py index 5ca9434..a08f19f 100644 --- a/admin/build.py +++ b/admin/build.py @@ -20,7 +20,7 @@ def _update_project_version(version: str): - regex = r'''^([ _]*version[ _]*[:=] *['"])(.*)(['"].*)$''' + regex = r"""^([ _]*version[ _]*[:=] *['"])(.*)(['"].*)$""" for file in VERSION_FILES: _re_sub_file(file, regex, version) @@ -28,7 +28,7 @@ def _update_project_version(version: str): def _get_project_version() -> str: import re - pattern = re.compile('''^[ _]*version[ _]*[:=] *['"](.*)['"]''', re.MULTILINE) + pattern = re.compile("""^[ _]*version[ _]*[:=] *['"](.*)['"]""", re.MULTILINE) versions = {} for file in VERSION_FILES: with open(file) as f: @@ -237,7 +237,7 @@ def build_version( yes: Annotated[ bool, typer.Option( - help='Don\'t ask confirmation to create new branch if necessary.', + help="Don't ask confirmation to create new branch if necessary.", show_default=False, ), ] = False, diff --git a/admin/lint.py b/admin/lint.py index 005c590..3c6a49a 100644 --- a/admin/lint.py +++ b/admin/lint.py @@ -3,6 +3,8 @@ Linting and static type checking. """ +from typing import Annotated + import typer from admin.utils import DryAnnotation, logger, run @@ -16,7 +18,17 @@ @app.command(name='ruff') -def lint_ruff(path='.', check: bool = False, dry: DryAnnotation = False): +def lint_ruff( + path: Annotated[str, typer.Argument(help='Path to directory or file to lint.')] = '.', + check: Annotated[ + bool, + typer.Option( + help='Check-only mode: report violations without fixing or reformatting. ' + 'Exits non-zero if any issues are found. Use this in CI.', + ), + ] = False, + dry: DryAnnotation = False, +): if check: run('ruff', 'check', path, dry=dry) run('ruff', 'format', '--check', path, dry=dry) @@ -26,7 +38,10 @@ def lint_ruff(path='.', check: bool = False, dry: DryAnnotation = False): @app.command(name='mypy') -def lint_mypy(path=None, dry: DryAnnotation = False): +def lint_mypy( + path: Annotated[str | None, typer.Argument(help='Path to type-check.')] = None, + dry: DryAnnotation = False, +): if path: run('mypy', path, dry=dry) else: @@ -36,13 +51,22 @@ def lint_mypy(path=None, dry: DryAnnotation = False): @app.command(name='all') -def lint_all(dry: DryAnnotation = False): +def lint_all( + check: Annotated[ + bool, + typer.Option( + help='Check-only mode: report violations without fixing or reformatting. ' + 'Exits non-zero if any issues are found. Use this in CI.', + ), + ] = False, + dry: DryAnnotation = False, +): """ Run all linters. Config for each of the tools is in ``pyproject.toml``. """ - lint_ruff(check=True, dry=dry) + lint_ruff(check=check, dry=dry) lint_mypy(dry=dry) logger.info('Done') diff --git a/admin/pip.py b/admin/pip.py index 902fd43..bace696 100644 --- a/admin/pip.py +++ b/admin/pip.py @@ -36,7 +36,6 @@ class Requirements(StrEnum): class RequirementsType(StrEnum): - IN = 'in' OUT = 'txt' diff --git a/admin/precommit.py b/admin/precommit.py index 45de924..f559b04 100644 --- a/admin/precommit.py +++ b/admin/precommit.py @@ -2,6 +2,7 @@ """ Precommit linting and static type checking. """ + from typing import Annotated import typer @@ -46,7 +47,7 @@ def precommit_run( Manually run pre-commit hooks. """ hook = hook or '--all-files' - run(dry, 'pre-commit' 'run', hook) + run(dry, 'pre-commitrun', hook) if __name__ == '__main__': diff --git a/admin/ui.py b/admin/ui.py index 4027afd..016883a 100644 --- a/admin/ui.py +++ b/admin/ui.py @@ -2,6 +2,7 @@ """ UI operations for Qt. """ + from typing import Annotated import typer diff --git a/src/hd_active/hd_active_config.py b/src/hd_active/hd_active_config.py index 9e76b63..1b7538d 100644 --- a/src/hd_active/hd_active_config.py +++ b/src/hd_active/hd_active_config.py @@ -26,7 +26,7 @@ def __init__(self, file_name: str): self.read() def __str__(self): - return f'drive paths: {", ".join(self.drive_paths)}' f'\nwait: {self.wait}s' + return f'drive paths: {", ".join(self.drive_paths)}\nwait: {self.wait}s' def read(self): files_read = self.config.read(self.file_name) diff --git a/tests/test_hd_active_config.py b/tests/test_hd_active_config.py index 619b082..ee23b66 100644 --- a/tests/test_hd_active_config.py +++ b/tests/test_hd_active_config.py @@ -35,16 +35,16 @@ def test_defaults(read_mock): [ pytest.param( ( - '''[HD Active] -drives = e:\\''', + """[HD Active] +drives = e:\\""", ['e:\\'], ), id='one drive only', ), pytest.param( ( - '''[HD Active] -drives = e:\\,f, g/''', + """[HD Active] +drives = e:\\,f, g/""", ['e:\\', 'f', 'g/'], ), id='multiple drives', @@ -76,9 +76,9 @@ def test_file_doesnt_exist(): [ pytest.param( ( - '''[HD Active] + """[HD Active] wait_between_access = 7 -drives = e:\\,f:\\''', +drives = e:\\,f:\\""", 'drive paths: e:\\, f:\\\nwait: 7.0s', ), id='two drives', From 140c9d132fdfd4395d21f6ac49c2e1be46990008 Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sun, 8 Mar 2026 11:36:44 -0500 Subject: [PATCH 14/16] gh action update --- .github/workflows/linting.yml | 24 +++++++++++++----------- docs/development.md | 6 +++--- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 027c49b..22404df 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -1,5 +1,5 @@ name: Linting -# Linting tools use `pyproject.toml` and `setup.cfg` for config. +# Linting tools use `pyproject.toml` for config. on: - pull_request @@ -7,6 +7,9 @@ on: jobs: linting: runs-on: ubuntu-latest + env: + UV_SYSTEM_PYTHON: '1' + steps: - name: Checkout code uses: actions/checkout@v5 @@ -14,16 +17,15 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: '3.11' + python-version: '3.12' + + - name: Install uv + uses: astral-sh/setup-uv@v6 - name: Install requirements - run: pip install -r admin/requirements/requirements-dev.in + run: | + uv pip install typer-invoke + inv pip install requirements-dev - - name: isort - run: isort . - - name: black - run: black . - - name: flake8 - run: flake8 . - - name: mypy - run: mypy src && mypy tests + - name: Lint and type check + run: inv lint all --check diff --git a/docs/development.md b/docs/development.md index 934230d..1bce6ba 100644 --- a/docs/development.md +++ b/docs/development.md @@ -5,7 +5,7 @@ To get started with development of this project: 1. Clone Virtual environment recommended, but optional. Developed with Python 3.7, but should be straightforward to upgrade to newer versions. -2. `pip install -U -r requirements-dev.txt` +2. `uv pip install -r admin/requirements/requirements-dev.txt` 3. `pre-commit install` ## UI @@ -49,7 +49,7 @@ This project uses **pyinvoke** ([main page](https://www.pyinvoke.org/) | [docs]( with development. ### Using invoke -After the installing the dev requirements (which include `invoke`), try the commands below. +After installing the dev requirements, try the commands below. List all available tasks: ``` @@ -78,7 +78,7 @@ code and debug `tasks.py` as any other Python file. ## Documentation Install documentation requirements with: ``` -pip install -r requirements-docs.txt +uv pip install -r admin/requirements/requirements-docs.txt ``` You can then edit the `.md` files under the `docs` directory and, if more need to be added, update From 3141130e62ef920268e54139a27bae699dc11419 Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sun, 8 Mar 2026 11:40:47 -0500 Subject: [PATCH 15/16] gh action update --- .github/workflows/build-app.yml | 33 +++++++++++++++++++++++++++------ .github/workflows/release.yml | 2 +- .github/workflows/tests.yml | 7 ++++++- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-app.yml b/.github/workflows/build-app.yml index 25fab67..28482d7 100644 --- a/.github/workflows/build-app.yml +++ b/.github/workflows/build-app.yml @@ -38,6 +38,8 @@ jobs: mac_os: runs-on: macos-latest if: github.event.inputs.mac_os == 'true' + env: + UV_SYSTEM_PYTHON: '1' steps: - name: Checkout code @@ -46,10 +48,15 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: '3.11' + python-version: '3.12' + + - name: Install uv + uses: astral-sh/setup-uv@v5 - name: Install requirements - run: pip install -r requirements-dev.txt + run: | + uv pip install typer-invoke + inv pip install requirements-dev - name: Build app run: inv build.app @@ -62,6 +69,8 @@ jobs: windows: runs-on: windows-latest if: github.event.inputs.windows == 'true' + env: + UV_SYSTEM_PYTHON: '1' steps: - name: Checkout code @@ -70,10 +79,15 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: '3.11' + python-version: '3.12' + + - name: Install uv + uses: astral-sh/setup-uv@v5 - name: Install requirements - run: pip install -r admin/requirements/requirements-dev.in + run: | + uv pip install typer-invoke + inv pip install requirements-dev - name: Build app run: inv build.app @@ -86,6 +100,8 @@ jobs: linux: runs-on: ubuntu-latest if: github.event.inputs.linux == 'true' + env: + UV_SYSTEM_PYTHON: '1' steps: - name: Checkout code @@ -101,10 +117,15 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: '3.11' + python-version: '3.12' + + - name: Install uv + uses: astral-sh/setup-uv@v5 - name: Install requirements - run: pip install -r requirements-dev.txt + run: | + uv pip install typer-invoke + inv pip install requirements-dev - name: Build app run: inv build.app diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a2c788b..93a278a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,7 +62,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: '3.11' + python-version: '3.12' - name: Install requirements run: pip install -r admin/requirements/requirements-dev.in diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a91956f..f407360 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,10 +6,12 @@ on: jobs: pytest: runs-on: windows-latest + env: + UV_SYSTEM_PYTHON: '1' strategy: matrix: # Min and max versions supported - python-version: ['3.10', '3.13'] + python-version: ['3.12', '3.14'] name: Python ${{ matrix.python-version }} steps: - name: Checkout code @@ -29,6 +31,9 @@ jobs: python-version: ${{ matrix.python-version }} architecture: x64 + - name: Install uv + uses: astral-sh/setup-uv@v5 + - name: Install requirements run: pip install -r admin/requirements/requirements-dev.in From cfa86a543efef06e9083ff215e0e2a811fb65f39 Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sun, 8 Mar 2026 11:41:16 -0500 Subject: [PATCH 16/16] gh action update --- .github/workflows/tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index f407360..5bac53b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -35,7 +35,9 @@ jobs: uses: astral-sh/setup-uv@v5 - name: Install requirements - run: pip install -r admin/requirements/requirements-dev.in + run: | + uv pip install typer-invoke + inv pip install requirements-dev - name: Run tests on Linux and Mac # Test folder(s) configured in `pyproject.toml`