From 13743a5cf536ee9f813fd2c1d8892f3322de27ad Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sun, 8 Mar 2026 11:53:41 -0500 Subject: [PATCH 1/3] admin scripts fixes --- admin/build.py | 26 ++++++++++--------- admin/utils.py | 69 +++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 77 insertions(+), 18 deletions(-) diff --git a/admin/build.py b/admin/build.py index a08f19f..721b7f4 100644 --- a/admin/build.py +++ b/admin/build.py @@ -121,29 +121,31 @@ def _get_latest_release(dry: bool) -> tuple[str, str, list[dict]]: """ import json - release_info_json = ( - run('gh', 'release', 'view', '--json', 'name,tagName,assets', dry=dry, capture_output=True) - .stdout.decode() # type: ignore - .strip() - ) + release_info_json = run( + 'gh', 'release', 'view', '--json', 'name,tagName,assets', dry=dry, capture_output=True + ).stdout 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('git', 'branch', '--show-current', dry=False, capture_output=True) - .stdout.decode() # type: ignore - .strip() - ) + return run('git', 'branch', '--show-current', dry=False, capture_output=True).stdout def _get_default_branch(): """Returns the default branch (usually ``main``).""" return run( - False, 'gh', 'repo', 'view', '--json', 'defaultBranchRef', '--jq', '.defaultBranchRef.name' - ) + 'gh', + 'repo', + 'view', + '--json', + 'defaultBranchRef', + '--jq', + '.defaultBranchRef.name', + dry=False, + capture_output=True, + ).stdout def _commit(message: str, dry: bool): diff --git a/admin/utils.py b/admin/utils.py index c7371e1..bb1636c 100644 --- a/admin/utils.py +++ b/admin/utils.py @@ -1,15 +1,21 @@ import logging +import os import subprocess import sys +from dataclasses import dataclass from enum import Enum from itertools import chain from typing import Annotated import typer from rich.logging import RichHandler +from rich.text import Text from admin import PROJECT_ROOT +EMPTY_STR = object() +"""Sentinel object to represent an empty string.""" + DryAnnotation = Annotated[ bool, typer.Option( @@ -27,6 +33,23 @@ class OS(str, Enum): Windows = 'win' +@dataclass +class StripOutput: + strip_ansi: bool = True + normal_strip: bool = True + extra_chars: str | None = None + + def strip(self, text: str) -> str: + if self.strip_ansi: + text = strip_ansi(text) + if self.normal_strip: + text = text.strip() + if self.extra_chars: + text = text.strip(self.extra_chars) + + return text + + def get_os() -> OS: """ Similar to ``sys.platform`` and ``platform.system()``, but less ambiguous by returning an Enum @@ -41,16 +64,37 @@ def get_os() -> OS: return OS.Linux -def run(*args, dry: bool = False, **kwargs) -> subprocess.CompletedProcess | None: +def run( + *args, + dry: bool = False, + extra_env: dict[str, str] | None = None, + strip_output: StripOutput | None = StripOutput(), + **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. + ``.stdout`` to get the output as a string. + + Notes: + + * Args are converted to strings using ``str(...)``. + * Empty strings and ``None`` are removed from the command. + If you want to explicitly include an empty string, use ``EMPTY_STR`` instead. + * ``stdout`` and ``stderr`` will be stripped of ANSI escape sequences by default. """ - logger.info(' '.join(map(str, args))) + final_args: list[str] = [] + for arg in args: + if arg in ['', None]: + continue + if arg == EMPTY_STR: + final_args.append('') + else: + final_args.append(str(arg)) + logger.info(' '.join(f'"{a}"' if (not a or ' ' in a) else a for a in final_args)) if dry: return None @@ -58,20 +102,29 @@ def run(*args, dry: bool = False, **kwargs) -> subprocess.CompletedProcess | Non defaults = dict( cwd=PROJECT_ROOT, capture_output=False, + text=True, check=True, + env=os.environ.copy() | (extra_env or {}), ) + final_kwargs = defaults | kwargs try: - return subprocess.run(args, **(defaults | kwargs)) # type: ignore + result = subprocess.run(final_args, **final_kwargs) # type: ignore except subprocess.CalledProcessError as e: msg = str(e) if e.stdout: - msg += f'\nSTDOUT:\n{e.stdout.decode()}' + msg += f'\nSTDOUT:\n{e.stdout}' if e.stderr: - msg += f'\nSTDERR:\n{e.stderr.decode()}' + msg += f'\nSTDERR:\n{e.stderr}' logger.error(msg) raise typer.Exit(1) + if final_kwargs.get('capture_output') and strip_output: + result.stdout = strip_output.strip(result.stdout) + result.stderr = strip_output.strip(result.stderr) + + return result # type: ignore + def run_async(*args, dry: bool = False, **kwargs) -> subprocess.Popen | None: """ @@ -129,6 +182,10 @@ def multiple_parameters(parameter: str, *options) -> list[str]: return list(chain.from_iterable(zip([parameter] * len(options), map(str, options)))) +def strip_ansi(text: str) -> str: + return Text.from_ansi(text).plain + + def get_logger(name: str | None = 'typer-invoke', level=logging.DEBUG) -> logging.Logger: """Set up logging configuration with Rich handler and custom formatting.""" From a8b6475e65886933c598df1a8d158d3aa709e853 Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sun, 8 Mar 2026 11:53:50 -0500 Subject: [PATCH 2/3] bump version to 0.2.0 --- pyproject.toml | 2 +- src/hd_active/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2890bd0..7f0417a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -72,7 +72,7 @@ module-name = 'hd_active' [project] name = 'hd_active' -version = '0.1.1' +version = '0.2.0' description = 'Prevent external HDs from becoming inactive (sleeping).' readme = 'README.md' authors = [{name = 'Joao Coelho'}] diff --git a/src/hd_active/__init__.py b/src/hd_active/__init__.py index df9144c..7fd229a 100644 --- a/src/hd_active/__init__.py +++ b/src/hd_active/__init__.py @@ -1 +1 @@ -__version__ = '0.1.1' +__version__ = '0.2.0' From 908aa01de32d9a73628f8eebaa4673ce1c85954d Mon Sep 17 00:00:00 2001 From: Joao Coelho Date: Sun, 8 Mar 2026 11:56:38 -0500 Subject: [PATCH 3/3] fix --- admin/build.py | 1 + src/__init__.py | 0 2 files changed, 1 insertion(+) delete mode 100644 src/__init__.py diff --git a/admin/build.py b/admin/build.py index 721b7f4..ec82d36 100644 --- a/admin/build.py +++ b/admin/build.py @@ -202,6 +202,7 @@ def build_publish( 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) diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index e69de29..0000000