Skip to content

Commit

Permalink
RFC: refactor theme-handling internals
Browse files Browse the repository at this point in the history
  • Loading branch information
neutrinoceros committed Dec 5, 2024
1 parent 0cbd0bd commit 2a74ea5
Show file tree
Hide file tree
Showing 13 changed files with 101 additions and 66 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
45 changes: 17 additions & 28 deletions src/idefix_cli/__main__.py
Original file line number Diff line number Diff line change
@@ -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]]
Expand Down Expand Up @@ -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")
)
Expand All @@ -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"))
99 changes: 72 additions & 27 deletions src/idefix_cli/_theme.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,95 @@
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
ERROR: str
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]
2 changes: 1 addition & 1 deletion tests/test_app_structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
2 changes: 1 addition & 1 deletion tests/test_clean.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
2 changes: 1 addition & 1 deletion tests/test_clone.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion tests/test_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
2 changes: 1 addition & 1 deletion tests/test_digest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_run.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down
2 changes: 1 addition & 1 deletion tests/test_ux.py
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion tests/test_write.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 2a74ea5

Please sign in to comment.