Skip to content

Commit f30a587

Browse files
authored
Merge pull request #26 from koxudaxi/annotate-render-template-parameters
Annotate structured template render parameters
2 parents b2455fd + 7b352d7 commit f30a587

9 files changed

Lines changed: 124 additions & 56 deletions

File tree

json-tstring/src/json_tstring/_runtime.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from __future__ import annotations
22

33
from string.templatelib import Template
4-
from typing import Literal, Protocol, TypeIs, cast
4+
from typing import Annotated, Literal, Protocol, TypeIs, cast
55

66
from tstring_bindings import tstring_bindings as _bindings
77
from tstring_core import JsonValue, RenderResult
88

99
type JsonProfile = Literal["rfc8259"]
10+
type JsonTemplate = Annotated[Template, "json"]
1011

1112
_CONTRACT_VERSION = 1
1213
_REQUIRED_SYMBOLS = {
@@ -17,16 +18,18 @@
1718

1819

1920
class _RenderJson(Protocol):
20-
def __call__(self, template: Template, *, profile: JsonProfile) -> JsonValue: ...
21+
def __call__(
22+
self, template: JsonTemplate, *, profile: JsonProfile
23+
) -> JsonValue: ...
2124

2225

2326
class _RenderJsonText(Protocol):
24-
def __call__(self, template: Template, *, profile: JsonProfile) -> str: ...
27+
def __call__(self, template: JsonTemplate, *, profile: JsonProfile) -> str: ...
2528

2629

2730
class _RenderJsonResultPayload(Protocol):
2831
def __call__(
29-
self, template: Template, *, profile: JsonProfile
32+
self, template: JsonTemplate, *, profile: JsonProfile
3033
) -> tuple[str, JsonValue]: ...
3134

3235

@@ -60,7 +63,7 @@ def _is_template(value: object) -> TypeIs[Template]:
6063
return isinstance(value, Template)
6164

6265

63-
def _validate_template(template: object, api_name: str) -> Template:
66+
def _validate_template(template: object, api_name: str) -> JsonTemplate:
6467
if _is_template(template):
6568
return template
6669
raise TypeError(
@@ -78,19 +81,21 @@ def _resolve_profile(profile: JsonProfile | str | None) -> JsonProfile:
7881

7982

8083
def render_data(
81-
template: Template, *, profile: JsonProfile | str | None = None
84+
template: JsonTemplate, *, profile: JsonProfile | str | None = None
8285
) -> JsonValue:
8386
checked = _validate_template(template, "render_data")
8487
return _render_json(checked, profile=_resolve_profile(profile))
8588

8689

87-
def render_text(template: Template, *, profile: JsonProfile | str | None = None) -> str:
90+
def render_text(
91+
template: JsonTemplate, *, profile: JsonProfile | str | None = None
92+
) -> str:
8893
checked = _validate_template(template, "render_text")
8994
return _render_json_text(checked, profile=_resolve_profile(profile))
9095

9196

9297
def render_result(
93-
template: Template, *, profile: JsonProfile | str | None = None
98+
template: JsonTemplate, *, profile: JsonProfile | str | None = None
9499
) -> RenderResult[JsonValue]:
95100
checked = _validate_template(template, "render_result")
96101
text, data = _render_json_result_payload(checked, profile=_resolve_profile(profile))

json-tstring/tests/test_json_pep750_binding_regressions.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from string.templatelib import Template
4-
from typing import Any
4+
from typing import Annotated, Any, get_args, get_origin, get_type_hints
55

66
import pytest
77

@@ -14,6 +14,10 @@
1414
from json_tstring import render_text as render_json_text
1515

1616

17+
def _template_hint_value(hint: object) -> object:
18+
return getattr(hint, "__value__", hint)
19+
20+
1721
class DualRenderValue:
1822
def __init__(self, *, repr_text: str, str_text: str) -> None:
1923
self._repr_text = repr_text
@@ -120,6 +124,15 @@ def test_json_binding_errors_expose_structured_diagnostics() -> None:
120124
)
121125

122126

127+
def test_json_render_apis_expose_annotated_template_parameters() -> None:
128+
for render_api in (render_json_data, render_json_text, render_json_result):
129+
template_hint = _template_hint_value(
130+
get_type_hints(render_api, include_extras=True)["template"]
131+
)
132+
assert get_origin(template_hint) is Annotated
133+
assert get_args(template_hint) == (Template, "json")
134+
135+
123136
def test_tstring_core_deprecated_root_helpers_remain_compatible() -> None:
124137
import tstring_core._render as render_helpers
125138

rust/python-bindings/python/tstring_bindings/__init__.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
from string.templatelib import Template
4-
from typing import Protocol, cast
4+
from typing import Annotated, Protocol, cast
55

66
from . import tstring_bindings as _bindings
77
from ._profiles import (
@@ -14,29 +14,33 @@
1414
)
1515
from ._types import JsonValue, TomlValue, YamlValue
1616

17+
type JsonTemplate = Annotated[Template, "json"]
18+
type TomlTemplate = Annotated[Template, "toml"]
19+
type YamlTemplate = Annotated[Template, "yaml"]
20+
1721

1822
class _RenderJson(Protocol):
19-
def __call__(self, template: Template, profile: JsonProfile) -> JsonValue: ...
23+
def __call__(self, template: JsonTemplate, profile: JsonProfile) -> JsonValue: ...
2024

2125

2226
class _RenderJsonText(Protocol):
23-
def __call__(self, template: Template, profile: JsonProfile) -> str: ...
27+
def __call__(self, template: JsonTemplate, profile: JsonProfile) -> str: ...
2428

2529

2630
class _RenderToml(Protocol):
27-
def __call__(self, template: Template, profile: TomlProfile) -> TomlValue: ...
31+
def __call__(self, template: TomlTemplate, profile: TomlProfile) -> TomlValue: ...
2832

2933

3034
class _RenderTomlText(Protocol):
31-
def __call__(self, template: Template, profile: TomlProfile) -> str: ...
35+
def __call__(self, template: TomlTemplate, profile: TomlProfile) -> str: ...
3236

3337

3438
class _RenderYaml(Protocol):
35-
def __call__(self, template: Template, profile: YamlProfile) -> YamlValue: ...
39+
def __call__(self, template: YamlTemplate, profile: YamlProfile) -> YamlValue: ...
3640

3741

3842
class _RenderYamlText(Protocol):
39-
def __call__(self, template: Template, profile: YamlProfile) -> str: ...
43+
def __call__(self, template: YamlTemplate, profile: YamlProfile) -> str: ...
4044

4145

4246
class _BindingsContract(Protocol):
@@ -101,37 +105,37 @@ def _require_extension_contract() -> _BindingsContract:
101105

102106

103107
def render_json(
104-
template: Template, *, profile: JsonProfile | str | None = None
108+
template: JsonTemplate, *, profile: JsonProfile | str | None = None
105109
) -> JsonValue:
106110
return _render_json(template, resolve_json_profile(profile))
107111

108112

109113
def render_json_text(
110-
template: Template, *, profile: JsonProfile | str | None = None
114+
template: JsonTemplate, *, profile: JsonProfile | str | None = None
111115
) -> str:
112116
return _render_json_text(template, resolve_json_profile(profile))
113117

114118

115119
def render_toml(
116-
template: Template, *, profile: TomlProfile | str | None = None
120+
template: TomlTemplate, *, profile: TomlProfile | str | None = None
117121
) -> TomlValue:
118122
return _render_toml(template, resolve_toml_profile(profile))
119123

120124

121125
def render_toml_text(
122-
template: Template, *, profile: TomlProfile | str | None = None
126+
template: TomlTemplate, *, profile: TomlProfile | str | None = None
123127
) -> str:
124128
return _render_toml_text(template, resolve_toml_profile(profile))
125129

126130

127131
def render_yaml(
128-
template: Template, *, profile: YamlProfile | str | None = None
132+
template: YamlTemplate, *, profile: YamlProfile | str | None = None
129133
) -> YamlValue:
130134
return _render_yaml(template, resolve_yaml_profile(profile))
131135

132136

133137
def render_yaml_text(
134-
template: Template, *, profile: YamlProfile | str | None = None
138+
template: YamlTemplate, *, profile: YamlProfile | str | None = None
135139
) -> str:
136140
return _render_yaml_text(template, resolve_yaml_profile(profile))
137141

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
from __future__ import annotations
22

33
from string.templatelib import Template
4-
from typing import Any
4+
from typing import Annotated, Any
55

66
from ._profiles import JsonProfile, TomlProfile, YamlProfile
77

8+
type JsonTemplate = Annotated[Template, "json"]
9+
type TomlTemplate = Annotated[Template, "toml"]
10+
type YamlTemplate = Annotated[Template, "yaml"]
11+
812
type ExceptionSpan = tuple[tuple[int, int], tuple[int, int]]
913
type ExceptionDiagnostic = dict[str, object]
1014

@@ -19,20 +23,20 @@ class TemplateSemanticError(TemplateError): ...
1923
class UnrepresentableValueError(TemplateSemanticError): ...
2024

2125
def render_json(
22-
template: Template, *, profile: JsonProfile | str | None = ...
26+
template: JsonTemplate, *, profile: JsonProfile | str | None = ...
2327
) -> Any: ...
2428
def render_json_text(
25-
template: Template, *, profile: JsonProfile | str | None = ...
29+
template: JsonTemplate, *, profile: JsonProfile | str | None = ...
2630
) -> str: ...
2731
def render_toml(
28-
template: Template, *, profile: TomlProfile | str | None = ...
32+
template: TomlTemplate, *, profile: TomlProfile | str | None = ...
2933
) -> Any: ...
3034
def render_toml_text(
31-
template: Template, *, profile: TomlProfile | str | None = ...
35+
template: TomlTemplate, *, profile: TomlProfile | str | None = ...
3236
) -> str: ...
3337
def render_yaml(
34-
template: Template, *, profile: YamlProfile | str | None = ...
38+
template: YamlTemplate, *, profile: YamlProfile | str | None = ...
3539
) -> Any: ...
3640
def render_yaml_text(
37-
template: Template, *, profile: YamlProfile | str | None = ...
41+
template: YamlTemplate, *, profile: YamlProfile | str | None = ...
3842
) -> str: ...
Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from __future__ import annotations
22

33
from string.templatelib import Template
4-
from typing import Any
4+
from typing import Annotated, Any
55

66
# Non-public extension module retained for internal imports and packaging
77
# compatibility. Public callers should use `tstring_bindings`.
88

9+
type JsonTemplate = Annotated[Template, "json"]
10+
type TomlTemplate = Annotated[Template, "toml"]
11+
type YamlTemplate = Annotated[Template, "yaml"]
912
type ExceptionSpan = tuple[tuple[int, int], tuple[int, int]]
1013
type ExceptionDiagnostic = dict[str, object]
1114

@@ -22,18 +25,18 @@ class UnrepresentableValueError(TemplateSemanticError): ...
2225
__contract_version__: int
2326
__contract_symbols__: tuple[str, ...]
2427

25-
def render_json(template: Template, profile: str = ...) -> Any: ...
26-
def render_json_text(template: Template, profile: str = ...) -> str: ...
28+
def render_json(template: JsonTemplate, profile: str = ...) -> Any: ...
29+
def render_json_text(template: JsonTemplate, profile: str = ...) -> str: ...
2730
def _render_json_result_payload(
28-
template: Template, profile: str = ...
31+
template: JsonTemplate, profile: str = ...
2932
) -> tuple[str, Any]: ...
30-
def render_toml(template: Template, profile: str = ...) -> Any: ...
31-
def render_toml_text(template: Template, profile: str = ...) -> str: ...
33+
def render_toml(template: TomlTemplate, profile: str = ...) -> Any: ...
34+
def render_toml_text(template: TomlTemplate, profile: str = ...) -> str: ...
3235
def _render_toml_result_payload(
33-
template: Template, profile: str = ...
36+
template: TomlTemplate, profile: str = ...
3437
) -> tuple[str, Any]: ...
35-
def render_yaml(template: Template, profile: str = ...) -> Any: ...
36-
def render_yaml_text(template: Template, profile: str = ...) -> str: ...
38+
def render_yaml(template: YamlTemplate, profile: str = ...) -> Any: ...
39+
def render_yaml_text(template: YamlTemplate, profile: str = ...) -> str: ...
3740
def _render_yaml_result_payload(
38-
template: Template, profile: str = ...
41+
template: YamlTemplate, profile: str = ...
3942
) -> tuple[str, Any]: ...

toml-tstring/src/toml_tstring/_runtime.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from __future__ import annotations
22

33
from string.templatelib import Template
4-
from typing import Literal, Protocol, TypeIs, cast
4+
from typing import Annotated, Literal, Protocol, TypeIs, cast
55

66
from tstring_bindings import tstring_bindings as _bindings
77
from tstring_core import RenderResult, TomlValue
88

99
type TomlProfile = Literal["1.0", "1.1"]
10+
type TomlTemplate = Annotated[Template, "toml"]
1011

1112
_CONTRACT_VERSION = 1
1213
_REQUIRED_SYMBOLS = {
@@ -17,16 +18,18 @@
1718

1819

1920
class _RenderToml(Protocol):
20-
def __call__(self, template: Template, *, profile: TomlProfile) -> TomlValue: ...
21+
def __call__(
22+
self, template: TomlTemplate, *, profile: TomlProfile
23+
) -> TomlValue: ...
2124

2225

2326
class _RenderTomlText(Protocol):
24-
def __call__(self, template: Template, *, profile: TomlProfile) -> str: ...
27+
def __call__(self, template: TomlTemplate, *, profile: TomlProfile) -> str: ...
2528

2629

2730
class _RenderTomlResultPayload(Protocol):
2831
def __call__(
29-
self, template: Template, *, profile: TomlProfile
32+
self, template: TomlTemplate, *, profile: TomlProfile
3033
) -> tuple[str, TomlValue]: ...
3134

3235

@@ -60,7 +63,7 @@ def _is_template(value: object) -> TypeIs[Template]:
6063
return isinstance(value, Template)
6164

6265

63-
def _validate_template(template: object, api_name: str) -> Template:
66+
def _validate_template(template: object, api_name: str) -> TomlTemplate:
6467
if _is_template(template):
6568
return template
6669
raise TypeError(
@@ -82,19 +85,21 @@ def _resolve_profile(profile: TomlProfile | str | None) -> TomlProfile:
8285

8386

8487
def render_data(
85-
template: Template, *, profile: TomlProfile | str | None = None
88+
template: TomlTemplate, *, profile: TomlProfile | str | None = None
8689
) -> TomlValue:
8790
checked = _validate_template(template, "render_data")
8891
return _render_toml(checked, profile=_resolve_profile(profile))
8992

9093

91-
def render_text(template: Template, *, profile: TomlProfile | str | None = None) -> str:
94+
def render_text(
95+
template: TomlTemplate, *, profile: TomlProfile | str | None = None
96+
) -> str:
9297
checked = _validate_template(template, "render_text")
9398
return _render_toml_text(checked, profile=_resolve_profile(profile))
9499

95100

96101
def render_result(
97-
template: Template, *, profile: TomlProfile | str | None = None
102+
template: TomlTemplate, *, profile: TomlProfile | str | None = None
98103
) -> RenderResult[TomlValue]:
99104
checked = _validate_template(template, "render_result")
100105
text, data = _render_toml_result_payload(checked, profile=_resolve_profile(profile))

toml-tstring/tests/test_toml_pep750_binding_regressions.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@
22

33
# ruff: noqa: E501
44
from datetime import UTC, datetime
5+
from string.templatelib import Template
6+
from typing import Annotated, get_args, get_origin, get_type_hints
57

68
import pytest
79

810
from toml_tstring import TemplateParseError, render_data, render_result, render_text
911

1012

13+
def _template_hint_value(hint: object) -> object:
14+
return getattr(hint, "__value__", hint)
15+
16+
1117
class DualRenderValue:
1218
def __init__(self, *, repr_text: str, str_text: str) -> None:
1319
self._repr_text = repr_text
@@ -84,3 +90,12 @@ def test_toml_metadata_path_materialization_reports_parse_errors_consistently()
8490

8591
with pytest.raises(TemplateParseError, match="invalid formatted TOML payload"):
8692
render_result(t"value = {broken!r}\n")
93+
94+
95+
def test_toml_render_apis_expose_annotated_template_parameters() -> None:
96+
for render_api in (render_data, render_text, render_result):
97+
template_hint = _template_hint_value(
98+
get_type_hints(render_api, include_extras=True)["template"]
99+
)
100+
assert get_origin(template_hint) is Annotated
101+
assert get_args(template_hint) == (Template, "toml")

0 commit comments

Comments
 (0)