diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ad0bcb..319c28d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - DEP: drop `[isolated]` extra (external tooling should handle dependency pinning) - BLD: drop package level `__version__` attribute - BLD: migrate build backend from `setuptools` to `hatchling` +- RFC: refactor theme-handling internals ## [5.2.1] - 2024-09-20 diff --git a/pyproject.toml b/pyproject.toml index 35b7b1d..225ddbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,8 +30,8 @@ file = "README.md" content-type = "text/markdown" [project.scripts] -idfx = "idefix_cli.__main__:main" -baballe = "idefix_cli.__main__:alt_main" +idfx = "idefix_cli.__main__:idfx_entry_point" +baballe = "idefix_cli.__main__:baballe_entry_point" [project.urls] Homepage = "https://github.com/neutrinoceros/idefix_cli" diff --git a/src/idefix_cli/__main__.py b/src/idefix_cli/__main__.py index 56a2abd..1dff546 100644 --- a/src/idefix_cli/__main__.py +++ b/src/idefix_cli/__main__.py @@ -1,15 +1,14 @@ import inspect import os import sys -import unicodedata from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser from importlib.metadata import version from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path from types import FunctionType, ModuleType -from typing import Any, Final +from typing import Any, Final, Literal -from idefix_cli._theme import set_theme +from idefix_cli._theme import theme_ctx from idefix_cli.lib import get_config_file, get_option, print_error, print_warning CommandMap = dict[str, tuple[FunctionType, bool]] @@ -113,14 +112,10 @@ def _setup_commands(parser: ArgumentParser) -> CommandMap: return cmddict -def main( - argv: list[str] | None = None, - parser: ArgumentParser | None = None, -) -> Any: +def cli(caller: Literal["idfx", "baballe"], argv: list[str] | None = None) -> Any: # the return value is deleguated to sub commands so its type is arbitrary # In practice it should be either 'int' or 'typing.NoReturn' - if parser is None: - parser = ArgumentParser(prog="idfx", allow_abbrev=False) + parser = ArgumentParser(prog=caller, allow_abbrev=False) parser.add_argument( "-v", "--version", action="version", version=version("idefix-cli") ) @@ -145,26 +140,20 @@ def main( return cmd(*unknown_args, **vars(known_args)) -def alt_main(argv: list[str] | None = None) -> Any: - print( - unicodedata.lookup("BASEBALL") - + unicodedata.lookup("BLACK RIGHT-POINTING TRIANGLE"), - file=sys.stderr, - ) +def main(caller: Literal["idfx", "baballe"], argv: list[str] | None = None) -> Any: + theme_name = {"idfx": "default", "baballe": "baballe"}[caller] + + with theme_ctx(theme_name): + return cli(caller, argv) + + +def idfx_entry_point(argv: list[str] | None = None) -> Any: + return main(caller="idfx", argv=argv) - set_theme("baballe") - try: - retv = main(argv, parser=ArgumentParser(prog="baballe", allow_abbrev=False)) - finally: - set_theme("default") - print( - unicodedata.lookup("BLACK LEFT-POINTING TRIANGLE") - + unicodedata.lookup("BASEBALL"), - file=sys.stderr, - ) - return retv +def baballe_entry_point(argv: list[str] | None = None) -> Any: + return main(caller="baballe", argv=argv) -if __name__ == "__main__": - sys.exit(main()) +if __name__ == "__main__": # pragma: no cover + sys.exit(main(caller="idfx")) diff --git a/src/idefix_cli/_theme.py b/src/idefix_cli/_theme.py index fe54102..d4ab976 100644 --- a/src/idefix_cli/_theme.py +++ b/src/idefix_cli/_theme.py @@ -1,14 +1,13 @@ import sys import unicodedata +from contextlib import contextmanager +from dataclasses import dataclass from typing import Literal, TypedDict -if sys.version_info >= (3, 11): - from typing import assert_never -else: - from typing_extensions import assert_never +__all__ = ["get_symbol", "theme_ctx"] -class Theme(TypedDict): +class SymbolSet(TypedDict): LAUNCH: str SUCCESS: str WARNING: str @@ -16,35 +15,81 @@ class Theme(TypedDict): HINT: str -Default = Theme( - LAUNCH=unicodedata.lookup("ROCKET"), # 🚀 - SUCCESS=unicodedata.lookup("PARTY POPPER"), # 🎉 - WARNING=unicodedata.lookup("HEAVY EXCLAMATION MARK SYMBOL"), # ❗ - ERROR=unicodedata.lookup("COLLISION SYMBOL"), # 💥 - HINT=unicodedata.lookup("LEFT-POINTING MAGNIFYING GLASS"), # 🔍 +@dataclass(frozen=True, slots=True, kw_only=True) +class Theme: + name: str + symbols: SymbolSet + enter_msg: str | None + exit_msg: str | None + + +class ThemeRegistry: + def __init__(self): + self._registry: dict[str, Theme] = {} + + def register( + self, + name: str, + *, + symbols: SymbolSet, + enter: str | None = None, + exit: str | None = None, + ): + self._registry[name] = Theme( + name=name, symbols=symbols, enter_msg=enter, exit_msg=exit + ) + + def __getitem__(self, item: str) -> Theme: + return self._registry[item] + + +themes = ThemeRegistry() +themes.register( + name="default", + symbols={ + "LAUNCH": unicodedata.lookup("ROCKET"), # 🚀 + "SUCCESS": unicodedata.lookup("PARTY POPPER"), # 🎉 + "WARNING": unicodedata.lookup("HEAVY EXCLAMATION MARK SYMBOL"), # ❗ + "ERROR": unicodedata.lookup("COLLISION SYMBOL"), # 💥 + "HINT": unicodedata.lookup("LEFT-POINTING MAGNIFYING GLASS"), # 🔍 + }, ) -Baballe = Theme( - LAUNCH=unicodedata.lookup("GUIDE DOG"), # 🦮 - SUCCESS=unicodedata.lookup("POODLE"), # 🐩 - WARNING=unicodedata.lookup("PAW PRINTS"), # 🐾 - ERROR=unicodedata.lookup("HOT DOG"), # 🌭 - HINT=unicodedata.lookup("CRYSTAL BALL"), # 🔮 +themes.register( + name="baballe", + symbols={ + "LAUNCH": unicodedata.lookup("GUIDE DOG"), # 🦮 + "SUCCESS": unicodedata.lookup("POODLE"), # 🐩 + "WARNING": unicodedata.lookup("PAW PRINTS"), # 🐾 + "ERROR": unicodedata.lookup("HOT DOG"), # 🌭 + "HINT": unicodedata.lookup("CRYSTAL BALL"), # 🔮 + }, + enter=unicodedata.lookup("BASEBALL") + + unicodedata.lookup("BLACK RIGHT-POINTING TRIANGLE"), + exit=unicodedata.lookup("BLACK LEFT-POINTING TRIANGLE") + + unicodedata.lookup("BASEBALL"), ) -THEME = Default +THEME = themes["default"] + +def get_symbol(key: Literal["LAUNCH", "SUCCESS", "WARNING", "ERROR", "HINT"]) -> str: + return THEME.symbols[key] -def set_theme(theme: Literal["default", "baballe"]) -> None: + +@contextmanager +def theme_ctx(name: str): global THEME - if theme == "default": - THEME = Default - elif theme == "baballe": - THEME = Baballe - else: - assert_never(theme) + old_name = THEME.name + THEME = themes[name] + if THEME.enter_msg is not None: + print(THEME.enter_msg, file=sys.stderr) + try: + yield + finally: + if THEME.exit_msg is not None: + print(THEME.exit_msg, file=sys.stderr) -def get_symbol(key: Literal["LAUNCH", "SUCCESS", "WARNING", "ERROR", "HINT"]) -> str: - return THEME[key] + THEME = themes[old_name] diff --git a/tests/test_app_structure.py b/tests/test_app_structure.py index 868c1dc..1368af8 100644 --- a/tests/test_app_structure.py +++ b/tests/test_app_structure.py @@ -7,7 +7,7 @@ import pytest -from idefix_cli.__main__ import _setup_commands, main +from idefix_cli.__main__ import _setup_commands, idfx_entry_point as main from idefix_cli.lib import print_error diff --git a/tests/test_clean.py b/tests/test_clean.py index 1ef4336..ebb7b41 100644 --- a/tests/test_clean.py +++ b/tests/test_clean.py @@ -2,7 +2,7 @@ import pytest -from idefix_cli.__main__ import main +from idefix_cli.__main__ import idfx_entry_point as main from idefix_cli._commands.clean import gpatterns, kokkos_files diff --git a/tests/test_clone.py b/tests/test_clone.py index 32d5477..275bf9a 100644 --- a/tests/test_clone.py +++ b/tests/test_clone.py @@ -3,7 +3,7 @@ import pytest -from idefix_cli.__main__ import main +from idefix_cli.__main__ import idfx_entry_point as main DATADIR = Path(__file__).parent / "data" BASE_SETUP = DATADIR / "OrszagTang3D" diff --git a/tests/test_conf.py b/tests/test_conf.py index e6351ac..99b9e7f 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -4,7 +4,7 @@ from packaging.version import Version from pytest_check import check -from idefix_cli.__main__ import main +from idefix_cli.__main__ import idfx_entry_point as main from idefix_cli._commands.conf import substitute_cmake_args diff --git a/tests/test_digest.py b/tests/test_digest.py index c06f901..f177868 100644 --- a/tests/test_digest.py +++ b/tests/test_digest.py @@ -5,7 +5,7 @@ import pytest -from idefix_cli.__main__ import main +from idefix_cli.__main__ import idfx_entry_point as main from idefix_cli._commands.digest import command as digest if sys.version_info >= (3, 11): diff --git a/tests/test_read.py b/tests/test_read.py index 0f5ec6e..5ac1108 100644 --- a/tests/test_read.py +++ b/tests/test_read.py @@ -2,7 +2,7 @@ from pytest_check import check -from idefix_cli.__main__ import main +from idefix_cli.__main__ import idfx_entry_point as main def test_read_not_a_file(tmp_path, capsys): diff --git a/tests/test_run.py b/tests/test_run.py index 0e26d69..ee33072 100644 --- a/tests/test_run.py +++ b/tests/test_run.py @@ -1,6 +1,6 @@ import pytest -from idefix_cli.__main__ import main +from idefix_cli.__main__ import idfx_entry_point as main from idefix_cli._commands.run import get_highest_power_of_two diff --git a/tests/test_ux.py b/tests/test_ux.py index 598e6e4..1c74f12 100644 --- a/tests/test_ux.py +++ b/tests/test_ux.py @@ -1,7 +1,7 @@ import pytest from pytest_check import check -from idefix_cli.__main__ import main +from idefix_cli.__main__ import idfx_entry_point as main HELP_MESSAGE = ( "usage: idfx [-h] [-v] {clean,clone,conf,digest,read,run,switch,write} ...\n" diff --git a/tests/test_write.py b/tests/test_write.py index 3640f43..aadfbbd 100644 --- a/tests/test_write.py +++ b/tests/test_write.py @@ -6,7 +6,7 @@ import pytest from pytest_check import check -from idefix_cli.__main__ import main +from idefix_cli.__main__ import idfx_entry_point as main jq_available = subprocess.run(["which", "jq"]).returncode == 0