From 16b192dc0572374b7c95a324a25a544267549072 Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Mon, 27 Oct 2025 08:59:31 -0500 Subject: [PATCH 01/10] update github scripts --- .github/workflows/build-app.yml | 12 ++++++------ .github/workflows/docs.yml | 4 ++-- .github/workflows/linting.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- .github/workflows/tests.yml | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-app.yml b/.github/workflows/build-app.yml index 8ee4a36..7f1e632 100644 --- a/.github/workflows/build-app.yml +++ b/.github/workflows/build-app.yml @@ -41,10 +41,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.11' @@ -65,10 +65,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.11' @@ -89,7 +89,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install libEGL # Implementation of the EGL (Embedded-Systems Graphics Library) API. @@ -99,7 +99,7 @@ jobs: sudo apt-get install -y libegl1-mesa - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.11' diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index aff4d7e..7f5f18c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,10 +17,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.12' diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 5e88599..e69970f 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -9,10 +9,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.11' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b3655d..d9aebb2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,10 +57,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.11' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3b1dac6..ae27fd2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -13,7 +13,7 @@ jobs: name: Python ${{ matrix.python-version }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install libEGL (Linux only) # Implementation of the EGL (Embedded-Systems Graphics Library) API. @@ -24,7 +24,7 @@ jobs: if: runner.os == 'Linux' - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} architecture: x64 From 7dd2f81278c9dd7b3a617935267a138d0cc5ab40 Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Mon, 27 Oct 2025 08:59:44 -0500 Subject: [PATCH 02/10] update requirements --- requirements-dev.in | 1 + requirements-dev.txt | 11 +++++++---- requirements.txt | 8 ++++---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/requirements-dev.in b/requirements-dev.in index 06a8eed..c3c8d29 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -4,6 +4,7 @@ invoke # Tasks pip-tools # Package management pre-commit # Pre-commit hooks +typer-invoke # Tasks # Linting bandit # Linter diff --git a/requirements-dev.txt b/requirements-dev.txt index b31ece1..f4dfb43 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -109,13 +109,13 @@ pyproject-hooks==1.2.0 # via # build # pip-tools -pyside6==6.9.1 +pyside6==6.10.0 # via -r requirements.txt -pyside6-addons==6.9.1 +pyside6-addons==6.10.0 # via # -r requirements.txt # pyside6 -pyside6-essentials==6.9.1 +pyside6-essentials==6.10.0 # via # -r requirements.txt # pyside6 @@ -125,6 +125,7 @@ pytest==8.4.2 # -r requirements-dev.in # pytest-params # pytest-qt + # typer-invoke pytest-params==0.3.0 # via -r requirements-dev.in pytest-qt==4.5.0 @@ -141,7 +142,7 @@ requests==2.32.5 # via flit rich==14.2.0 # via bandit -shiboken6==6.9.1 +shiboken6==6.10.0 # via # -r requirements.txt # pyside6 @@ -151,6 +152,8 @@ stevedore==5.5.0 # via bandit tomli-w==1.2.0 # via flit +typer-invoke==0.0.1 + # via -r requirements-dev.in typing-extensions==4.15.0 # via # mypy diff --git a/requirements.txt b/requirements.txt index 0a6e3cc..9b50461 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,15 +4,15 @@ # # pip-compile requirements.in # -pyside6==6.9.2 +pyside6==6.10.0 # via -r requirements.in -pyside6-addons==6.9.2 +pyside6-addons==6.10.0 # via pyside6 -pyside6-essentials==6.9.2 +pyside6-essentials==6.10.0 # via # pyside6 # pyside6-addons -shiboken6==6.9.2 +shiboken6==6.10.0 # via # pyside6 # pyside6-addons From ae03afcd34f8a6b5f6466fbb2e94255968d7be06 Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Fri, 7 Nov 2025 23:32:21 -0600 Subject: [PATCH 03/10] admin scripts --- admin/__init__.py | 4 + admin/build.py | 298 ++++++++++++++++++++++++++++++++++++++++++++++ admin/pip.py | 161 +++++++++++++++++++++++++ admin/utils.py | 98 +++++++++++++++ 4 files changed, 561 insertions(+) create mode 100644 admin/__init__.py create mode 100644 admin/build.py create mode 100644 admin/pip.py create mode 100644 admin/utils.py diff --git a/admin/__init__.py b/admin/__init__.py new file mode 100644 index 0000000..057669c --- /dev/null +++ b/admin/__init__.py @@ -0,0 +1,4 @@ +from pathlib import Path + +PROJECT_ROOT = Path(__file__).parents[1] +SOURCE_DIR = PROJECT_ROOT / 'src' diff --git a/admin/build.py b/admin/build.py new file mode 100644 index 0000000..de6a7f4 --- /dev/null +++ b/admin/build.py @@ -0,0 +1,298 @@ +from pathlib import Path +from typing import Annotated + +import typer + +from admin import PROJECT_ROOT, SOURCE_DIR +from admin.utils import DryAnnotation, logger, run + +BUILD_DIST_DIR = PROJECT_ROOT / 'dist' +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. +""" + +app = typer.Typer() + + +def _update_project_version(version: str): + regex = r'''^([ _]*version[ _]*[:=] *['"])(.*)(['"].*)$''' + for file in VERSION_FILES: + _re_sub_file(file, regex, version) + + +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: + logger.error(f'Could not find version in `{file.relative_to(PROJECT_ROOT)}`.') + raise typer.Exit(1) + versions[file] = match.group(1) + + if len(set(versions.values())) != 1: + logger.error( + 'Version mismatch in files that contain versions.\n' + + ( + '\n'.join( + f'{file.relative_to(PROJECT_ROOT)}: {version}' + for file, version in versions.items() + ) + ) + ) + raise typer.Exit(1) + + 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 _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'): + logger.error(f'Invalid release name: {release_name}') + raise typer.Exit(1) + 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 _get_branch(): + """Returns the current branch.""" + return run(False, 'git', 'branch', '--show-current') + + +def _get_default_branch(): + """Returns the default branch (usually ``main``).""" + return run( + False, 'gh', 'repo', 'view', '--json', 'defaultBranchRef', '--jq', '.defaultBranchRef.name' + ) + + +def _commit(message: str, dry: bool): + # Commit + run(dry, 'git', 'add', *VERSION_FILES) + run(dry, 'git', 'commit', '-m', message) + + # Push current branch + branch = _get_branch() + run(dry, 'git', 'push', 'origin', branch) + + +def _create_pr(title: str, description: str, dry: bool): + """ + 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() + branch = _get_branch() + run( + dry, + 'gh', + 'pr', + 'create', + '--title', + title, + '--body', + description, + '--head', + branch, + '--base', + default_branch, + ) + + # Merge PR after checks pass + run(dry, 'gh', 'pr', 'merge', 'branch', '--squash', '--auto') + + +@app.command(name='clean') +def build_clean(): + import shutil + + shutil.rmtree(BUILD_DIST_DIR, ignore_errors=True) + + +@app.command(name='version') +def build_version( + version: Annotated[ + str, + typer.Option( + help='Version in semantic versioning format (ex 1.5.0). ' + 'If `version` is set, then `bump` cannot be used.', + show_default=False, + ), + ] = '', + bump: Annotated[ + str, + typer.Option( + help='Portion of the version to increase, can be "major", "minor", or "patch". ' + 'If `bump` is set, then `version` cannot be used.', + show_default=False, + ), + ] = '', + mode: Annotated[ + str, + typer.Option( + help='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.' + ), + ] = 'nothing', + yes: Annotated[ + bool, + typer.Option( + help='Don\'t ask confirmation to create new branch if necessary.', + show_default=False, + ), + ] = False, + dry: DryAnnotation = 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']: + logger.error('Invalid `mode` choice.') + raise typer.Exit(1) + + v1 = Version(_get_project_version()) + if version and bump: + logger.error('Either `version` or `bump` can be set, not both.') + raise typer.Exit(1) + 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: + logger.error('Invalid choice') + raise typer.Exit(1) + + if version: + v2 = Version(version) + if v2 <= v1: + logger.error( + f'New version `{v2}` needs to be greater than the existing version `{v1}`.' + ) + raise typer.Exit(1) + else: + try: + v2 = _get_next_version(v1, bump.strip().lower()) + except AttributeError: + logger.error('Invalid `bump` choice.') + raise typer.Exit(1) + + # Verify branch is not default + branch = _get_branch() + default_branch = _get_default_branch() + 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']: + run(dry, 'git', 'checkout', '-b', f'release-{v2}') + branch_ok = True + if not branch_ok: + logger.error(f'Cannot make changes in the default branch `{branch}`.') + raise typer.Exit(1) + + # 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(f'bump version to {v2}', dry) + if mode == 'pr': + pr_title = f'Release {v2}' + print(f'Create and merge PR `{pr_title}`.') + _create_pr(pr_title, f'Preparing for release {v2}', dry) + + +if __name__ == '__main__': + app() diff --git a/admin/pip.py b/admin/pip.py new file mode 100644 index 0000000..0672313 --- /dev/null +++ b/admin/pip.py @@ -0,0 +1,161 @@ +#!python +""" +Python packages related tasks. +""" +from enum import StrEnum +from pathlib import Path +from typing import Annotated + +import typer + +from admin import PROJECT_ROOT +from admin.utils import DryAnnotation, install_package, logger, run + +app = typer.Typer( + help=__doc__, + no_args_is_help=True, + add_completion=False, + rich_markup_mode='markdown', +) + + +class Requirements(StrEnum): + """ + Requirements files. + + Order matters as most operations with multiple files need ``requirements.txt`` to be processed + first. + Add new requirements files here. + """ + + MAIN = 'requirements' + DEV = 'requirements-dev' + + +class RequirementsType(StrEnum): + + IN = 'in' + 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( + help='Requirement file(s) to compile. If not set, all files are compiled.\nValues can be ' + + ', '.join([f'`{x.name.lower()}`' for x in Requirements]), + show_default=False, + ), +] + + +def _get_requirements_file( + requirements: str | Requirements, requirements_type: str | RequirementsType +) -> Path: + """Return the full requirements file path.""" + if isinstance(requirements, Requirements): + reqs = requirements + else: + try: + reqs = Requirements[requirements.upper()] # noqa + except ValueError: + try: + reqs = Requirements(requirements.lower()) + except ValueError: + logger.error(f'`{requirements}` is an unknown requirements file.') + raise typer.Exit(1) + + if isinstance(requirements_type, RequirementsType): + reqs_type = requirements_type + else: + reqs_type = RequirementsType(requirements_type.lstrip('.').lower()) + + base_path = PROJECT_ROOT / 'admin' / 'requirements' + return base_path / f'{reqs}.{reqs_type}' + + +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``""" + requirements_files = list(Requirements) if requirements is None else requirements + return [_get_requirements_file(r, requirements_type) for r in requirements_files] + + +@app.command(name='compile') +def pip_compile( + requirements: RequirementsAnnotation = None, + clean: Annotated[ + bool, + typer.Option( + help=f'Delete the existing requirements `{RequirementsType.OUT.value}` files, forcing ' + f'a clean compilation.' + ), + ] = False, + dry: DryAnnotation = False, +): + """ + Compile requirements file(s). + """ + install_package('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): + run(False, 'pip-compile', *dry_option, str(filename)) + + +@app.command(name='sync') +def pip_sync(requirements: RequirementsAnnotation = None, dry: DryAnnotation = False): + """ + Synchronize environment with requirements file. + """ + install_package('pip-tools', dry=dry) + run(dry, 'pip-sync', *_get_requirements_files(requirements, RequirementsType.OUT)) + + +@app.command(name='package') +def pip_package( + requirements: RequirementsAnnotation, + packages: Annotated[ + list[str], typer.Option('--packages', '-p', help='One or more packages to upgrade.') + ], + dry: DryAnnotation = False, +): + """ + Upgrade one or more packages. + """ + install_package('pip-tools', dry=dry) + + for filename in _get_requirements_files(requirements, RequirementsType.IN): + run( + dry, 'pip-compile', '--upgrade-package', *' --upgrade-package '.join(packages), filename + ) + + +@app.command(name='upgrade') +def pip_upgrade(requirements, 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``. + """ + install_package('pip-tools', dry=dry) + + for filename in _get_requirements_files(requirements, RequirementsType.IN): + run(dry, ['pip-compile', '--upgrade', filename]) + + +if __name__ == '__main__': + app() diff --git a/admin/utils.py b/admin/utils.py new file mode 100644 index 0000000..a236d4f --- /dev/null +++ b/admin/utils.py @@ -0,0 +1,98 @@ +import logging +import subprocess +import sys +from enum import Enum +from typing import Annotated + +import typer +from rich.logging import RichHandler + +from admin import PROJECT_ROOT + +DryAnnotation = Annotated[ + bool, + typer.Option( + help='Show the command that would be run without running it.', + show_default=False, + ), +] + + +class OS(str, Enum): + """Operating System.""" + + Linux = 'linux' + MacOS = 'mac' + Windows = 'win' + + +def get_os() -> OS: + """ + Similar to ``sys.platform`` and ``platform.system()``, but less ambiguous by returning an Enum + instead of a string. + + Doesn't make granular distinctions of linux variants, OS versions, etc. + """ + if sys.platform == 'darwin': + return OS.MacOS + if sys.platform == 'win32': + return OS.Windows + return OS.Linux + + +def run(dry: bool, *args) -> subprocess.CompletedProcess | None: + logger.info(' '.join(map(str, args))) + + if dry: + return None + + try: + return subprocess.run(args, cwd=PROJECT_ROOT, check=True) + 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 + + 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.""" + if is_package_installed(package): + logger.debug(f'Package `{package}` is already installed.') + return + + run(dry, sys.executable, '-m', 'pip', 'install', package) + + +def get_logger(name=None, level=logging.DEBUG) -> logging.Logger: + """Set up logging configuration with Rich handler and custom formatting.""" + + # Create logger + _logger = logging.getLogger('typer-invoke') + _logger.setLevel(level) + _logger.handlers.clear() + handler = RichHandler( + level=level, + show_time=False, + show_level=True, + markup=True, + rich_tracebacks=False, + ) + + # Set custom format string and add handler + formatter = logging.Formatter(fmt='%(message)s', datefmt='[%X]') # Time format: [HH:MM:SS] + handler.setFormatter(formatter) + _logger.addHandler(handler) + + # Prevent logs from being handled by root logger (avoid duplicate output) + _logger.propagate = False + + return _logger + + +logger = get_logger() From 6da76d2c77e2e26cc30b41e1708a508666b831c0 Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sun, 9 Nov 2025 14:02:03 -0600 Subject: [PATCH 04/10] admin scripts --- admin/lint.py | 54 ++++++++++++++++++++++++++ admin/requirements/requirements-dev.in | 25 ++++++++++++ admin/requirements/requirements.in | 1 + admin/test.py | 29 ++++++++++++++ pyproject.toml | 14 +++++++ tasks.py | 2 +- 6 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 admin/lint.py create mode 100644 admin/requirements/requirements-dev.in create mode 100644 admin/requirements/requirements.in create mode 100644 admin/test.py diff --git a/admin/lint.py b/admin/lint.py new file mode 100644 index 0000000..d16c13c --- /dev/null +++ b/admin/lint.py @@ -0,0 +1,54 @@ +#!python +""" +Linting and static type checking. +""" + +import typer + +from admin.utils import DryAnnotation, logger, run + +app = typer.Typer( + help=__doc__, + no_args_is_help=True, + add_completion=False, + rich_markup_mode='markdown', +) + + +@app.command(name='black') +def lint_black(path='.', dry: DryAnnotation = False): + run(dry, 'black', path) + + +@app.command(name='flake8') +def lint_flake8(path='.', dry: DryAnnotation = False): + run(dry, 'flake8', path) + + +@app.command(name='isort') +def lint_isort(path='.', dry: DryAnnotation = False): + run(dry, 'isort', path) + + +@app.command(name='mypy') +def lint_mypy(path='.', dry: DryAnnotation = False): + run(dry, 'mypy', path) + + +@app.command(name='all') +def lint_all(dry: DryAnnotation = False): + """ + Run all linters. + + Config for each of the tools is in ``pyproject.toml``. + """ + lint_isort(dry=dry) + lint_black(dry=dry) + lint_flake8(dry=dry) + lint_mypy(dry=dry) + + logger.info('Done') + + +if __name__ == '__main__': + app() diff --git a/admin/requirements/requirements-dev.in b/admin/requirements/requirements-dev.in new file mode 100644 index 0000000..e557958 --- /dev/null +++ b/admin/requirements/requirements-dev.in @@ -0,0 +1,25 @@ +-r requirements.txt + +# Dev tools +pip-tools # Package management +pre-commit # Pre-commit hooks +typer-invoke # Tasks + +# Linting +bandit # Linter +black # Linter +flake8 # Linter +flake8-pyproject # Flake8 plugin +isort # Linter +mypy # Code inspector + +# Test +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.in b/admin/requirements/requirements.in new file mode 100644 index 0000000..9137a62 --- /dev/null +++ b/admin/requirements/requirements.in @@ -0,0 +1 @@ +pyside6 diff --git a/admin/test.py b/admin/test.py new file mode 100644 index 0000000..f4c6d3b --- /dev/null +++ b/admin/test.py @@ -0,0 +1,29 @@ +#!python +""" +Testing with `pytest`. +""" + +import typer + +from admin.utils import DryAnnotation, run + +app = typer.Typer( + help=__doc__, + no_args_is_help=True, + add_completion=False, + rich_markup_mode='markdown', +) + + +@app.command(name='unit') +def test_unit(dry: DryAnnotation = False): + """ + Run unit tests. + + Unit test configuration in ``pyproject.toml``. + """ + run(dry, 'pytest', '.') + + +if __name__ == '__main__': + app() diff --git a/pyproject.toml b/pyproject.toml index 859c713..1900860 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,20 @@ disable_error_code = 'annotation-unchecked' ignore_missing_imports = true module = ['invoke', 'factory', 'pytest_check', 'pytest_params', 'PySide6.*'] +[tool.typer-invoke] +# Paths from where `pyproject.toml` is located (root of project) +modules = [ + 'admin.build', + 'admin.lint', + 'admin.pip', + 'admin.test', +] +no_args_is_help = true +add_completion = false +rich_markup_mode = 'markdown' +logging_level = 'INFO' +logging_format = '%(message)s' + [build-system] requires = ['flit_core >=3.2,<4'] build-backend = 'flit_core.buildapi' diff --git a/tasks.py b/tasks.py index a3adfc9..d40bf93 100644 --- a/tasks.py +++ b/tasks.py @@ -776,7 +776,7 @@ def lint_mypy(c, path='.'): def lint_all(c): """ Run all linters. - Config for each of the tools is in ``pyproject.toml`` and ``setup.cfg``. + Config for each of the tools is in ``pyproject.toml``. """ lint_isort(c) lint_black(c) From dadd85c901a623b09aa36b6d6647b2e1b5f59482 Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sun, 9 Nov 2025 14:05:51 -0600 Subject: [PATCH 05/10] admin scripts --- admin/precommit.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + 2 files changed, 54 insertions(+) create mode 100644 admin/precommit.py diff --git a/admin/precommit.py b/admin/precommit.py new file mode 100644 index 0000000..45de924 --- /dev/null +++ b/admin/precommit.py @@ -0,0 +1,53 @@ +#!python +""" +Precommit linting and static type checking. +""" +from typing import Annotated + +import typer + +from admin.utils import DryAnnotation, run + +app = typer.Typer( + help=__doc__, + no_args_is_help=True, + add_completion=False, + rich_markup_mode='markdown', +) + + +@app.command(name='install') +def precommit_install(dry: DryAnnotation = False): + """ + 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. + """ + run(dry, 'pre-commit', 'install') + + +# `upgrade` instead of `update` to maintain similar naming to `pip-compile upgrade` +@app.command(name='upgrade') +def precommit_upgrade(dry: DryAnnotation = False): + """ + Upgrade pre-commit config to the latest repos' versions. + """ + run(dry, 'pre-commit', 'autoupdate') + + +@app.command(name='run') +def precommit_run( + hook: Annotated[ + str | None, + typer.Option(help='Name of hook to run. Default is to run all.', show_default=False), + ] = None, + dry: DryAnnotation = False, +): + """ + Manually run pre-commit hooks. + """ + hook = hook or '--all-files' + run(dry, 'pre-commit' 'run', hook) + + +if __name__ == '__main__': + app() diff --git a/pyproject.toml b/pyproject.toml index 1900860..59b56df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ modules = [ 'admin.build', 'admin.lint', 'admin.pip', + 'admin.precommit', 'admin.test', ] no_args_is_help = true From 228ab141ccf90453ff3af6a8d5834f86e97a9941 Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sun, 9 Nov 2025 14:13:46 -0600 Subject: [PATCH 06/10] requirements --- admin/pip.py | 6 +- admin/requirements/requirements-dev.txt | 176 +++++++++++++++++++++++ admin/requirements/requirements-docs.in | 6 + admin/requirements/requirements-docs.txt | 111 ++++++++++++++ admin/requirements/requirements.txt | 19 +++ 5 files changed, 316 insertions(+), 2 deletions(-) create mode 100644 admin/requirements/requirements-dev.txt create mode 100644 admin/requirements/requirements-docs.in create mode 100644 admin/requirements/requirements-docs.txt create mode 100644 admin/requirements/requirements.txt diff --git a/admin/pip.py b/admin/pip.py index 0672313..2ccbf2f 100644 --- a/admin/pip.py +++ b/admin/pip.py @@ -11,6 +11,8 @@ from admin import PROJECT_ROOT from admin.utils import DryAnnotation, install_package, logger, run +REQUIREMENTS_DIR = PROJECT_ROOT / 'admin' / 'requirements' + app = typer.Typer( help=__doc__, no_args_is_help=True, @@ -30,6 +32,7 @@ class Requirements(StrEnum): MAIN = 'requirements' DEV = 'requirements-dev' + DOCS = 'requirements-docs' class RequirementsType(StrEnum): @@ -75,8 +78,7 @@ def _get_requirements_file( else: reqs_type = RequirementsType(requirements_type.lstrip('.').lower()) - base_path = PROJECT_ROOT / 'admin' / 'requirements' - return base_path / f'{reqs}.{reqs_type}' + return REQUIREMENTS_DIR / f'{reqs}.{reqs_type}' def _get_requirements_files( diff --git a/admin/requirements/requirements-dev.txt b/admin/requirements/requirements-dev.txt new file mode 100644 index 0000000..357b145 --- /dev/null +++ b/admin/requirements/requirements-dev.txt @@ -0,0 +1,176 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile 'requirements-dev.in' +# +altgraph==0.17.4 + # via pyinstaller +bandit==1.8.6 + # via -r requirements-dev.in +black==25.9.0 + # via -r requirements-dev.in +build==1.3.0 + # via pip-tools +certifi==2025.10.5 + # via requests +cfgv==3.4.0 + # via pre-commit +charset-normalizer==3.4.4 + # via requests +click==8.3.0 + # via + # black + # pip-tools + # typer +colorama==0.4.6 + # via + # bandit + # build + # click + # pytest +distlib==0.4.0 + # via virtualenv +docutils==0.22.3 + # via flit +filelock==3.20.0 + # via virtualenv +flake8==7.3.0 + # via + # -r requirements-dev.in + # flake8-pyproject +flake8-pyproject==1.2.3 + # 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 + # via pre-commit +idna==3.11 + # via requests +iniconfig==2.3.0 + # via pytest +isort==7.0.0 + # via -r requirements-dev.in +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 + # via -r requirements-dev.in +mypy-extensions==1.1.0 + # via + # black + # mypy +nodeenv==1.9.1 + # via pre-commit +packaging==25.0 + # via + # black + # build + # pyinstaller + # pyinstaller-hooks-contrib + # pytest +pathspec==0.12.1 + # via + # black + # mypy +pefile==2023.2.7 + # via pyinstaller +pip-tools==7.5.1 + # via -r requirements-dev.in +platformdirs==4.5.0 + # via + # black + # virtualenv +pluggy==1.6.0 + # via + # pytest + # pytest-qt +pre-commit==4.4.0 + # via -r requirements-dev.in +pycodestyle==2.14.0 + # via flake8 +pyflakes==3.4.0 + # via flake8 +pygments==2.19.2 + # via + # pytest + # rich +pyinstaller==6.16.0 + # via -r requirements-dev.in +pyinstaller-hooks-contrib==2025.9 + # via pyinstaller +pyproject-hooks==1.2.0 + # via + # build + # pip-tools +pyside6==6.10.0 + # via -r requirements.txt +pyside6-addons==6.10.0 + # via + # -r requirements.txt + # pyside6 +pyside6-essentials==6.10.0 + # via + # -r requirements.txt + # pyside6 + # pyside6-addons +pytest==9.0.0 + # via + # -r requirements-dev.in + # pytest-params + # pytest-qt +pytest-params==0.3.0 + # via -r requirements-dev.in +pytest-qt==4.5.0 + # via -r requirements-dev.in +pytokens==0.3.0 + # 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.2.0 + # via + # bandit + # typer + # typer-invoke +shellingham==1.5.4 + # via typer +shiboken6==6.10.0 + # via + # -r requirements.txt + # pyside6 + # pyside6-addons + # pyside6-essentials +stevedore==5.5.0 + # via bandit +tomli-w==1.2.0 + # via flit +typer==0.20.0 + # via typer-invoke +typer-invoke==0.3.0 + # via -r requirements-dev.in +typing-extensions==4.15.0 + # via + # mypy + # pytest-qt + # typer +urllib3==2.5.0 + # via requests +virtualenv==20.35.4 + # via pre-commit +wheel==0.45.1 + # via pip-tools + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/admin/requirements/requirements-docs.in b/admin/requirements/requirements-docs.in new file mode 100644 index 0000000..fe01791 --- /dev/null +++ b/admin/requirements/requirements-docs.in @@ -0,0 +1,6 @@ +-c requirements-dev.txt + +mdx_truly_sane_lists +mkdocs +mkdocs-glightbox +mkdocs-material diff --git a/admin/requirements/requirements-docs.txt b/admin/requirements/requirements-docs.txt new file mode 100644 index 0000000..3e300b1 --- /dev/null +++ b/admin/requirements/requirements-docs.txt @@ -0,0 +1,111 @@ +# +# 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 +charset-normalizer==3.4.4 + # via + # -c requirements-dev.txt + # requests +click==8.3.0 + # via + # -c requirements-dev.txt + # mkdocs +colorama==0.4.6 + # via + # -c requirements-dev.txt + # click + # mkdocs + # mkdocs-material +ghp-import==2.1.0 + # via mkdocs +idna==3.11 + # via + # -c requirements-dev.txt + # requests +jinja2==3.1.6 + # via + # mkdocs + # mkdocs-material +markdown==3.10 + # via + # mdx-truly-sane-lists + # mkdocs + # mkdocs-material + # pymdown-extensions +markupsafe==3.0.3 + # via + # jinja2 + # mkdocs +mdx-truly-sane-lists==1.3 + # via -r requirements-docs.in +mergedeep==1.3.4 + # via + # mkdocs + # mkdocs-get-deps +mkdocs==1.6.1 + # via + # -r requirements-docs.in + # mkdocs-material +mkdocs-get-deps==0.2.0 + # via mkdocs +mkdocs-glightbox==0.5.2 + # via -r requirements-docs.in +mkdocs-material==9.6.23 + # via -r requirements-docs.in +mkdocs-material-extensions==1.3.1 + # via mkdocs-material +packaging==25.0 + # via + # -c requirements-dev.txt + # mkdocs +paginate==0.5.7 + # via mkdocs-material +pathspec==0.12.1 + # via + # -c requirements-dev.txt + # mkdocs +platformdirs==4.5.0 + # via + # -c requirements-dev.txt + # mkdocs-get-deps +pygments==2.19.2 + # via + # -c requirements-dev.txt + # mkdocs-material +pymdown-extensions==10.16.1 + # via mkdocs-material +python-dateutil==2.9.0.post0 + # via ghp-import +pyyaml==6.0.3 + # via + # -c requirements-dev.txt + # mkdocs + # mkdocs-get-deps + # pymdown-extensions + # pyyaml-env-tag +pyyaml-env-tag==1.1 + # via mkdocs +requests==2.32.5 + # via + # -c requirements-dev.txt + # 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 +watchdog==6.0.0 + # via mkdocs diff --git a/admin/requirements/requirements.txt b/admin/requirements/requirements.txt new file mode 100644 index 0000000..2b14004 --- /dev/null +++ b/admin/requirements/requirements.txt @@ -0,0 +1,19 @@ +# +# 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 + # via pyside6 +pyside6-essentials==6.10.0 + # via + # pyside6 + # pyside6-addons +shiboken6==6.10.0 + # via + # pyside6 + # pyside6-addons + # pyside6-essentials From da86c931a70a3c6c9030bc1666a4cfacea7f7660 Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sun, 9 Nov 2025 14:16:59 -0600 Subject: [PATCH 07/10] readme update --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1bef8ee..75e8974 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,13 @@ This page doesn't go into how to do that. Install the development packages: ``` -python -m pip install -r requirements-dev.txt +pip typer-invoke && inv pip sync ``` -This project uses [pyinvoke](https://www.pyinvoke.org/) to facilitate common tasks. +This project uses [typer-invoke](https://github.com/joaonc/typer-invoke) to facilitate common tasks. For a list of tasks: ``` -inv --list +inv --help ``` ## Licensing From 9e16559f961f0ff2fae11d5b41602bf2fccc0a4a Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sun, 9 Nov 2025 22:24:32 -0600 Subject: [PATCH 08/10] remove requirements --- requirements-dev.in | 26 ------- requirements-dev.txt | 170 ------------------------------------------ requirements-docs.in | 6 -- requirements-docs.txt | 111 --------------------------- requirements.in | 1 - requirements.txt | 19 ----- 6 files changed, 333 deletions(-) delete mode 100644 requirements-dev.in delete mode 100644 requirements-dev.txt delete mode 100644 requirements-docs.in delete mode 100644 requirements-docs.txt delete mode 100644 requirements.in delete mode 100644 requirements.txt diff --git a/requirements-dev.in b/requirements-dev.in deleted file mode 100644 index c3c8d29..0000000 --- a/requirements-dev.in +++ /dev/null @@ -1,26 +0,0 @@ --r requirements.txt - -# Dev tools -invoke # Tasks -pip-tools # Package management -pre-commit # Pre-commit hooks -typer-invoke # Tasks - -# Linting -bandit # Linter -black # Linter -flake8 # Linter -flake8-pyproject # Flake8 plugin -isort # Linter -mypy # Code inspector - -# Test -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/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index f4dfb43..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,170 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile requirements-dev.in -# -altgraph==0.17.4 - # via pyinstaller -bandit==1.8.6 - # via -r requirements-dev.in -black==25.9.0 - # via -r requirements-dev.in -build==1.3.0 - # via pip-tools -certifi==2025.10.5 - # via requests -cfgv==3.4.0 - # via pre-commit -charset-normalizer==3.4.4 - # via requests -click==8.3.0 - # via - # black - # pip-tools -colorama==0.4.6 - # via - # bandit - # build - # click - # pytest -distlib==0.4.0 - # via virtualenv -docutils==0.22.2 - # via flit -filelock==3.20.0 - # via virtualenv -flake8==7.3.0 - # via - # -r requirements-dev.in - # flake8-pyproject -flake8-pyproject==1.2.3 - # 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 - # via pre-commit -idna==3.11 - # via requests -iniconfig==2.3.0 - # via pytest -invoke==2.2.1 - # via -r requirements-dev.in -isort==7.0.0 - # via -r requirements-dev.in -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 - # via -r requirements-dev.in -mypy-extensions==1.1.0 - # via - # black - # mypy -nodeenv==1.9.1 - # via pre-commit -packaging==25.0 - # via - # black - # build - # pyinstaller - # pyinstaller-hooks-contrib - # pytest -pathspec==0.12.1 - # via - # black - # mypy -pefile==2023.2.7 - # via pyinstaller -pip-tools==7.5.1 - # via -r requirements-dev.in -platformdirs==4.5.0 - # via - # black - # virtualenv -pluggy==1.6.0 - # via - # pytest - # pytest-qt -pre-commit==4.3.0 - # via -r requirements-dev.in -pycodestyle==2.14.0 - # via flake8 -pyflakes==3.4.0 - # via flake8 -pygments==2.19.2 - # via - # pytest - # rich -pyinstaller==6.16.0 - # via -r requirements-dev.in -pyinstaller-hooks-contrib==2025.9 - # via pyinstaller -pyproject-hooks==1.2.0 - # via - # build - # pip-tools -pyside6==6.10.0 - # via -r requirements.txt -pyside6-addons==6.10.0 - # via - # -r requirements.txt - # pyside6 -pyside6-essentials==6.10.0 - # via - # -r requirements.txt - # pyside6 - # pyside6-addons -pytest==8.4.2 - # via - # -r requirements-dev.in - # pytest-params - # pytest-qt - # typer-invoke -pytest-params==0.3.0 - # via -r requirements-dev.in -pytest-qt==4.5.0 - # via -r requirements-dev.in -pytokens==0.2.0 - # 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.2.0 - # via bandit -shiboken6==6.10.0 - # via - # -r requirements.txt - # pyside6 - # pyside6-addons - # pyside6-essentials -stevedore==5.5.0 - # via bandit -tomli-w==1.2.0 - # via flit -typer-invoke==0.0.1 - # via -r requirements-dev.in -typing-extensions==4.15.0 - # via - # mypy - # pytest-qt -urllib3==2.5.0 - # via requests -virtualenv==20.35.3 - # via pre-commit -wheel==0.45.1 - # via pip-tools - -# The following packages are considered to be unsafe in a requirements file: -# pip -# setuptools diff --git a/requirements-docs.in b/requirements-docs.in deleted file mode 100644 index fe01791..0000000 --- a/requirements-docs.in +++ /dev/null @@ -1,6 +0,0 @@ --c requirements-dev.txt - -mdx_truly_sane_lists -mkdocs -mkdocs-glightbox -mkdocs-material diff --git a/requirements-docs.txt b/requirements-docs.txt deleted file mode 100644 index 50dd204..0000000 --- a/requirements-docs.txt +++ /dev/null @@ -1,111 +0,0 @@ -# -# 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 -charset-normalizer==3.4.4 - # via - # -c requirements-dev.txt - # requests -click==8.3.0 - # via - # -c requirements-dev.txt - # mkdocs -colorama==0.4.6 - # via - # -c requirements-dev.txt - # click - # mkdocs - # mkdocs-material -ghp-import==2.1.0 - # via mkdocs -idna==3.11 - # via - # -c requirements-dev.txt - # requests -jinja2==3.1.6 - # via - # mkdocs - # mkdocs-material -markdown==3.9 - # via - # mdx-truly-sane-lists - # mkdocs - # mkdocs-material - # pymdown-extensions -markupsafe==3.0.3 - # via - # jinja2 - # mkdocs -mdx-truly-sane-lists==1.3 - # via -r requirements-docs.in -mergedeep==1.3.4 - # via - # mkdocs - # mkdocs-get-deps -mkdocs==1.6.1 - # via - # -r requirements-docs.in - # mkdocs-material -mkdocs-get-deps==0.2.0 - # via mkdocs -mkdocs-glightbox==0.5.1 - # via -r requirements-docs.in -mkdocs-material==9.6.22 - # via -r requirements-docs.in -mkdocs-material-extensions==1.3.1 - # via mkdocs-material -packaging==25.0 - # via - # -c requirements-dev.txt - # mkdocs -paginate==0.5.7 - # via mkdocs-material -pathspec==0.12.1 - # via - # -c requirements-dev.txt - # mkdocs -platformdirs==4.5.0 - # via - # -c requirements-dev.txt - # mkdocs-get-deps -pygments==2.19.2 - # via - # -c requirements-dev.txt - # mkdocs-material -pymdown-extensions==10.16.1 - # via mkdocs-material -python-dateutil==2.9.0.post0 - # via ghp-import -pyyaml==6.0.3 - # via - # -c requirements-dev.txt - # mkdocs - # mkdocs-get-deps - # pymdown-extensions - # pyyaml-env-tag -pyyaml-env-tag==1.1 - # via mkdocs -requests==2.32.5 - # via - # -c requirements-dev.txt - # mkdocs-material -selectolax==0.3.29 - # via mkdocs-glightbox -six==1.17.0 - # via python-dateutil -urllib3==2.5.0 - # via - # -c requirements-dev.txt - # requests -watchdog==6.0.0 - # via mkdocs diff --git a/requirements.in b/requirements.in deleted file mode 100644 index 9137a62..0000000 --- a/requirements.in +++ /dev/null @@ -1 +0,0 @@ -pyside6 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 9b50461..0000000 --- a/requirements.txt +++ /dev/null @@ -1,19 +0,0 @@ -# -# 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 - # via pyside6 -pyside6-essentials==6.10.0 - # via - # pyside6 - # pyside6-addons -shiboken6==6.10.0 - # via - # pyside6 - # pyside6-addons - # pyside6-essentials From 0150b77078dd122f23482f49a7437a295fd33ecd Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sun, 9 Nov 2025 22:24:47 -0600 Subject: [PATCH 09/10] docs admin scripts --- admin/docs.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + 2 files changed, 50 insertions(+) create mode 100644 admin/docs.py diff --git a/admin/docs.py b/admin/docs.py new file mode 100644 index 0000000..006ef59 --- /dev/null +++ b/admin/docs.py @@ -0,0 +1,49 @@ +#!python +""" +Create and publish documentation. +""" + +import typer + +from admin import PROJECT_ROOT +from admin.utils import DryAnnotation, run, logger + +app = typer.Typer( + help=__doc__, + no_args_is_help=True, + add_completion=False, + rich_markup_mode='markdown', +) + + +@app.command(name='serve') +def docs_serve(dry: DryAnnotation = False): + """ + Start documentation local server. + """ + run(dry, 'mkdocs', 'serve') + + +@app.command(name='deploy') +def docs_deploy(dry: DryAnnotation = False): + """ + Publish documentation to GitHub Pages at https://joaonc.github.io/hd_active + """ + run(dry, 'mkdocs', 'gh-deploy') + + +@app.command(name='clean') +def docs_clean(dry: DryAnnotation = False): + """ + Delete documentation website static files. + """ + import shutil + + docs_dir = PROJECT_ROOT / 'site' + logger.info(f'Deleting {docs_dir}') + if not dry: + shutil.rmtree(PROJECT_ROOT / 'site', ignore_errors=True) + + +if __name__ == '__main__': + app() diff --git a/pyproject.toml b/pyproject.toml index 59b56df..0cd1fe8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,7 @@ module = ['invoke', 'factory', 'pytest_check', 'pytest_params', 'PySide6.*'] # Paths from where `pyproject.toml` is located (root of project) modules = [ 'admin.build', + 'admin.docs', 'admin.lint', 'admin.pip', 'admin.precommit', From 864d5f50fb753efcdcffc8616c13831cc8bd49ef Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sun, 9 Nov 2025 22:38:47 -0600 Subject: [PATCH 10/10] pip install script updates --- .github/workflows/build-app.yml | 4 +++- .github/workflows/docs.yml | 4 +++- .github/workflows/linting.yml | 4 +++- .github/workflows/release.yml | 4 +++- .github/workflows/tests.yml | 4 ++-- admin/docs.py | 2 +- admin/pip.py | 18 ++++++++++++++++++ 7 files changed, 33 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build-app.yml b/.github/workflows/build-app.yml index 7f1e632..6db50ba 100644 --- a/.github/workflows/build-app.yml +++ b/.github/workflows/build-app.yml @@ -73,7 +73,9 @@ jobs: python-version: '3.11' - name: Install requirements - run: pip install -r requirements-dev.txt + run: | + pip install typer-invoke + inv pip install dev - name: Build app run: inv build.app diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7f5f18c..d6cee4d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -25,7 +25,9 @@ jobs: python-version: '3.12' - name: Install requirements - run: pip install -U -r requirements-docs.txt + run: | + pip install typer-invoke + inv pip install docs - name: Deploy docs run: mkdocs gh-deploy --force diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index e69970f..6c4d4c9 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -17,7 +17,9 @@ jobs: python-version: '3.11' - name: Install requirements - run: pip install -r requirements-dev.txt + run: | + pip install typer-invoke + inv pip install dev - name: isort run: isort . diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d9aebb2..debcabe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,7 +65,9 @@ jobs: python-version: '3.11' - name: Install requirements - run: pip install -r requirements-dev.txt + run: | + pip install typer-invoke + inv pip install dev - name: Set git user run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ae27fd2..5a08002 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -30,8 +30,8 @@ jobs: architecture: x64 - name: Install requirements run: | - python -m pip install -U pip - python -m pip install -r requirements.txt -r requirements-dev.txt + pip install typer-invoke + inv pip install dev - name: Run tests on Linux and Mac # Test folder(s) configured in `pyproject.toml` # Skip Windows OS tests diff --git a/admin/docs.py b/admin/docs.py index 006ef59..728ea2f 100644 --- a/admin/docs.py +++ b/admin/docs.py @@ -6,7 +6,7 @@ import typer from admin import PROJECT_ROOT -from admin.utils import DryAnnotation, run, logger +from admin.utils import DryAnnotation, logger, run app = typer.Typer( help=__doc__, diff --git a/admin/pip.py b/admin/pip.py index 2ccbf2f..8162477 100644 --- a/admin/pip.py +++ b/admin/pip.py @@ -115,6 +115,24 @@ def pip_compile( 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)))), + ) + + @app.command(name='sync') def pip_sync(requirements: RequirementsAnnotation = None, dry: DryAnnotation = False): """