laco-typer 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.4
2
+ Name: laco-typer
3
+ Version: 1.0.0
4
+ Summary: Typer CLI generation from @L.params blocks.
5
+ Author-email: Kurt Stolle <kurt@khws.io>
6
+ Requires-Python: >=3.13
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: laco>=1.0.0
9
+ Requires-Dist: typer>=0.12
10
+
11
+ # Laco-Typer
12
+
13
+ Typer CLI generation from `@L.params` blocks.
14
+
15
+ Part of the [laco](https://github.com/khwstolle/laco) project — see the [root README](../../README.md) for an overview.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pip install laco-typer
21
+ ```
22
+
23
+ ## Features
24
+
25
+ `make_app()`, `register()`
26
+
27
+ ## Usage
28
+
29
+ See [`docs/index.md`](docs/index.md) for the full guide.
@@ -0,0 +1,19 @@
1
+ # Laco-Typer
2
+
3
+ Typer CLI generation from `@L.params` blocks.
4
+
5
+ Part of the [laco](https://github.com/khwstolle/laco) project — see the [root README](../../README.md) for an overview.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install laco-typer
11
+ ```
12
+
13
+ ## Features
14
+
15
+ `make_app()`, `register()`
16
+
17
+ ## Usage
18
+
19
+ See [`docs/index.md`](docs/index.md) for the full guide.
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["setuptools>=75", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "laco-typer"
7
+ version = "1.0.0"
8
+ description = "Typer CLI generation from @L.params blocks."
9
+ readme = "README.md"
10
+ requires-python = ">=3.13"
11
+ authors = [{ name = "Kurt Stolle", email = "kurt@khws.io" }]
12
+ dependencies = ["laco>=1.0.0", "typer>=0.12"]
13
+
14
+ [tool.setuptools.packages.find]
15
+ where = ["sources"]
16
+ namespaces = true
17
+
18
+ [tool.uv.sources]
19
+ laco = { workspace = true }
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,21 @@
1
+ """Typer CLI generation from @L.params blocks for laco.
2
+
3
+ Examples
4
+ --------
5
+ ::
6
+
7
+ import laco.integrations.typer as laco_typer
8
+ import typer
9
+
10
+ app = laco_typer.make_app("configs/train.py", callback=train)
11
+ app()
12
+
13
+ # Or register onto an existing Typer app:
14
+ root = typer.Typer()
15
+ laco_typer.register(root, "configs/train.py", callback=train)
16
+ """
17
+
18
+ from laco.integrations.typer._core import make_app as make_app
19
+ from laco.integrations.typer._core import register as register
20
+
21
+ __all__ = ["make_app", "register"]
@@ -0,0 +1,147 @@
1
+ """Implementation for laco-typer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typing
6
+
7
+ if typing.TYPE_CHECKING:
8
+ import typer
9
+ from omegaconf import DictConfig
10
+
11
+
12
+ def _build_typer_command(
13
+ config_path: str,
14
+ callback: typing.Callable[[DictConfig], typing.Any],
15
+ ) -> typing.Callable[..., None]:
16
+ """Return a Typer-compatible command function with typed Option parameters."""
17
+ import inspect
18
+
19
+ import typer as _typer
20
+ from laco._io import _load_with_namespace
21
+ from laco._overrides import apply_overrides
22
+ from laco.language import LacoParam, ParamsWrapper
23
+
24
+ cfg_base, nsp = _load_with_namespace(config_path)
25
+
26
+ params_blocks: list[tuple[str, ParamsWrapper]] = [
27
+ (k, v)
28
+ for k, v in nsp.items()
29
+ if isinstance(v, ParamsWrapper) and not v._exclude and not k.startswith("_")
30
+ ]
31
+ module_params: list[tuple[str, LacoParam]] = [
32
+ (k, v)
33
+ for k, v in nsp.items()
34
+ if isinstance(v, LacoParam) and not k.startswith("_")
35
+ ]
36
+
37
+ # Build (cli_name, config_key, default, annotation, help) tuples
38
+ params_spec: list[tuple[str, str, typing.Any, type, str | None]] = []
39
+
40
+ for var_name, wrapper in params_blocks:
41
+ prefix = wrapper._prefix
42
+ if prefix is None:
43
+ flag_prefix = var_name + "_"
44
+ elif prefix == "":
45
+ flag_prefix = ""
46
+ else:
47
+ flag_prefix = prefix + "_"
48
+ annotations = (
49
+ typing.get_type_hints(wrapper._cls) if hasattr(wrapper, "_cls") else {}
50
+ )
51
+ for field_name, raw_val in wrapper._dict.items():
52
+ if field_name in wrapper._hidden:
53
+ continue
54
+ laco_p = raw_val if isinstance(raw_val, LacoParam) else None
55
+ default = laco_p.default if laco_p else raw_val
56
+ ann = annotations.get(
57
+ field_name, type(default) if default is not None else str
58
+ )
59
+ help_text = laco_p.help if laco_p else None
60
+ cli_name = f"{flag_prefix}{field_name}"
61
+ config_key = f"{var_name}.{field_name}"
62
+ params_spec.append((cli_name, config_key, default, ann, help_text))
63
+
64
+ for field_name, laco_p in module_params:
65
+ default = laco_p.default
66
+ ann = type(default) if default is not None else str
67
+ params_spec.append((field_name, field_name, default, ann, laco_p.help))
68
+
69
+ # Dynamically build the function signature using inspect.Parameter
70
+ sig_params = [
71
+ inspect.Parameter(
72
+ name=cli_name,
73
+ kind=inspect.Parameter.KEYWORD_ONLY,
74
+ default=_typer.Option(default, help=help_text or ""),
75
+ annotation=ann,
76
+ )
77
+ for cli_name, _config_key, default, ann, help_text in params_spec
78
+ ]
79
+
80
+ def _command(**kwargs: typing.Any) -> None:
81
+ overrides: list[str] = []
82
+ for cli_name, config_key, default, _ann, _help in params_spec:
83
+ val = kwargs[cli_name]
84
+ if val != default:
85
+ v = repr(val) if isinstance(val, str) else str(val)
86
+ overrides.append(f"{config_key}={v}")
87
+ cfg = apply_overrides(cfg_base, overrides) if overrides else cfg_base
88
+ callback(cfg)
89
+
90
+ _command.__signature__ = inspect.Signature(sig_params) # type: ignore[attr-defined]
91
+ _command.__name__ = callback.__name__
92
+ _command.__doc__ = callback.__doc__ or ""
93
+ return _command
94
+
95
+
96
+ def make_app(
97
+ config_path: str,
98
+ callback: typing.Callable[[DictConfig], typing.Any],
99
+ *,
100
+ name: str | None = None,
101
+ ) -> typer.Typer:
102
+ """Create a Typer application from a laco config file.
103
+
104
+ Parameters
105
+ ----------
106
+ config_path : str
107
+ Path to the laco config file.
108
+ callback : Callable
109
+ Function called with the resolved DictConfig.
110
+ name : str | None
111
+ Optional Typer app name.
112
+
113
+ Returns
114
+ -------
115
+ typer.Typer
116
+ App ready to be invoked.
117
+ """
118
+ import typer as _typer
119
+
120
+ app = _typer.Typer(name=name)
121
+ cmd = _build_typer_command(config_path, callback)
122
+ app.command()(cmd)
123
+ return app
124
+
125
+
126
+ def register(
127
+ app: typer.Typer,
128
+ config_path: str,
129
+ callback: typing.Callable[[DictConfig], typing.Any],
130
+ *,
131
+ name: str | None = None,
132
+ ) -> None:
133
+ """Register a laco config as a command on an existing Typer app.
134
+
135
+ Parameters
136
+ ----------
137
+ app : typer.Typer
138
+ Existing Typer application.
139
+ config_path : str
140
+ Path to the laco config file.
141
+ callback : Callable
142
+ Function called with the resolved DictConfig.
143
+ name : str | None
144
+ Command name override. Defaults to ``callback.__name__``.
145
+ """
146
+ cmd = _build_typer_command(config_path, callback)
147
+ app.command(name=name)(cmd)
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.4
2
+ Name: laco-typer
3
+ Version: 1.0.0
4
+ Summary: Typer CLI generation from @L.params blocks.
5
+ Author-email: Kurt Stolle <kurt@khws.io>
6
+ Requires-Python: >=3.13
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: laco>=1.0.0
9
+ Requires-Dist: typer>=0.12
10
+
11
+ # Laco-Typer
12
+
13
+ Typer CLI generation from `@L.params` blocks.
14
+
15
+ Part of the [laco](https://github.com/khwstolle/laco) project — see the [root README](../../README.md) for an overview.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ pip install laco-typer
21
+ ```
22
+
23
+ ## Features
24
+
25
+ `make_app()`, `register()`
26
+
27
+ ## Usage
28
+
29
+ See [`docs/index.md`](docs/index.md) for the full guide.
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ sources/laco/integrations/typer/__init__.py
4
+ sources/laco/integrations/typer/_core.py
5
+ sources/laco_typer.egg-info/PKG-INFO
6
+ sources/laco_typer.egg-info/SOURCES.txt
7
+ sources/laco_typer.egg-info/dependency_links.txt
8
+ sources/laco_typer.egg-info/requires.txt
9
+ sources/laco_typer.egg-info/top_level.txt
10
+ tests/test_laco_typer.py
@@ -0,0 +1,2 @@
1
+ laco>=1.0.0
2
+ typer>=0.12
@@ -0,0 +1,77 @@
1
+ """Tests for laco-typer."""
2
+
3
+ import inspect
4
+
5
+
6
+ def test_import():
7
+ import laco.integrations.typer # noqa: F401
8
+
9
+
10
+ def test_public_api():
11
+ import laco.integrations.typer as laco_typer
12
+
13
+ assert set(laco_typer.__all__) == {"make_app", "register"}
14
+
15
+
16
+ def test_make_app_signature():
17
+ import laco.integrations.typer as laco_typer
18
+
19
+ sig = inspect.signature(laco_typer.make_app)
20
+ params = list(sig.parameters)
21
+
22
+ assert "config_path" in params
23
+ assert "callback" in params
24
+ assert "name" in params
25
+
26
+
27
+ def test_register_signature():
28
+ import laco.integrations.typer as laco_typer
29
+
30
+ sig = inspect.signature(laco_typer.register)
31
+ params = list(sig.parameters)
32
+
33
+ assert "app" in params
34
+ assert "config_path" in params
35
+ assert "callback" in params
36
+ assert "name" in params
37
+
38
+
39
+ def test_make_app_with_mock_laco(tmp_path):
40
+ from unittest.mock import MagicMock, patch
41
+
42
+ import laco.integrations.typer as laco_typer
43
+
44
+ config_file = tmp_path / "config.py"
45
+ config_file.write_text("lr = 1e-3\n")
46
+ callback = MagicMock()
47
+
48
+ mock_cfg = MagicMock()
49
+ mock_nsp = {}
50
+
51
+ with patch("laco._io._load_with_namespace", return_value=(mock_cfg, mock_nsp)):
52
+ import typer
53
+
54
+ app = laco_typer.make_app(str(config_file), callback)
55
+ assert isinstance(app, typer.Typer)
56
+
57
+
58
+ def test_register_with_mock_laco(tmp_path):
59
+ from unittest.mock import MagicMock, patch
60
+
61
+ import laco.integrations.typer as laco_typer
62
+ import typer
63
+
64
+ config_file = tmp_path / "config.py"
65
+ config_file.write_text("lr = 1e-3\n")
66
+ callback = MagicMock()
67
+ callback.__name__ = "train"
68
+ callback.__doc__ = "Train the model."
69
+
70
+ root = typer.Typer()
71
+ mock_cfg = MagicMock()
72
+ mock_nsp = {}
73
+
74
+ with patch("laco._io._load_with_namespace", return_value=(mock_cfg, mock_nsp)):
75
+ laco_typer.register(root, str(config_file), callback)
76
+
77
+ assert len(root.registered_commands) == 1