Skip to content

Commit 2efdf9c

Browse files
authored
feat: format-jinja-imports (#77)
1 parent 1d617c0 commit 2efdf9c

File tree

4 files changed

+106
-1
lines changed

4 files changed

+106
-1
lines changed

docs/version_source.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,26 @@ You may configure the following options under `[tool.uv-dynamic-versioning]`:
148148
"""
149149
```
150150
151+
- `format-jinja-imports` (array of tables, default: empty):
152+
This defines additional things to import and make available to the `format-jinja` template.
153+
Each table must contain a `module` key and may also contain an `item` key. Consider this example:
154+
155+
```toml
156+
format-jinja-imports = [
157+
{ module = "foo" },
158+
{ module = "bar", item = "baz" },
159+
]
160+
```
161+
162+
This is roughly equivalent to:
163+
164+
```python
165+
import foo
166+
from bar import baz
167+
```
168+
169+
`foo` and `baz` would then become available in the Jinja formatting.
170+
151171
- `style` (string, default: unset): One of: `pep440`, `semver`, `pvp`. These are pre-configured output formats. If you set both a `style` and a `format`, then the format will be validated against the style's rules. If `style` is unset, the default output format will follow PEP 440, but a custom `format` will only be validated if `style` is set explicitly.
152172
Regardless of the style you choose, the dynamic version is ultimately subject to Hatchling's validation as well, and Hatchling is designed around PEP 440 versions. Hatchling can usually understand SemVer/etc input, but sometimes, Hatchling may reject an otherwise valid version format.
153173
- `latest-tag` (boolean, default: false): If true, then only check the latest tag for a version, rather than looking through all the tags until a suitable one is found to match the `pattern`.

src/uv_dynamic_versioning/schemas.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,29 @@ def from_dict(cls, data: dict) -> FromFile:
7171
return cls(**validated_data)
7272

7373

74+
@dataclass
75+
class FormatJinjaImport:
76+
module: str
77+
item: str | None = None
78+
79+
def _validate_module(self):
80+
if not isinstance(self.module, str):
81+
raise ValueError("module must be a string")
82+
83+
def _validate_item(self):
84+
if self.item is not None and not isinstance(self.item, str):
85+
raise ValueError("item must be a string or None")
86+
87+
def __post_init__(self):
88+
self._validate_module()
89+
self._validate_item()
90+
91+
@classmethod
92+
def from_dict(cls, data: dict) -> FormatJinjaImport:
93+
validated_data = _normalize(cls, data)
94+
return cls(**validated_data)
95+
96+
7497
@dataclass
7598
class UvDynamicVersioning:
7699
vcs: Vcs = Vcs.Any
@@ -81,6 +104,7 @@ class UvDynamicVersioning:
81104
pattern_prefix: str | None = None
82105
format: str | None = None
83106
format_jinja: str | None = None
107+
format_jinja_imports: list[FormatJinjaImport] | None = None
84108
style: Style | None = None
85109
latest_tag: bool = False
86110
strict: bool = False
@@ -143,6 +167,17 @@ def _validate_format_jinja(self):
143167
if self.format_jinja is not None and not isinstance(self.format_jinja, str):
144168
raise ValueError("format-jinja must be a string or None")
145169

170+
def _validate_format_jinja_imports(self):
171+
if self.format_jinja_imports is not None:
172+
if not isinstance(self.format_jinja_imports, list):
173+
raise ValueError("format-jinja-imports must be a list or None")
174+
175+
for item in self.format_jinja_imports:
176+
if not isinstance(item, FormatJinjaImport):
177+
raise ValueError(
178+
"format-jinja-imports must contain only FormatJinjaImport instances"
179+
)
180+
146181
def _validate_style(self):
147182
if self.style is not None and not isinstance(self.style, Style):
148183
raise ValueError(f"style is invalid - {self.style}")
@@ -197,6 +232,7 @@ def __post_init__(self):
197232
self._validate_pattern_prefix()
198233
self._validate_format()
199234
self._validate_format_jinja()
235+
self._validate_format_jinja_imports()
200236
self._validate_style()
201237
self._validate_tag_dir()
202238
self._validate_tag_branch()
@@ -239,6 +275,15 @@ def from_dict(cls, data: dict) -> UvDynamicVersioning:
239275
validated_data["from_file"]
240276
)
241277

278+
if "format_jinja_imports" in validated_data and isinstance(
279+
validated_data["format_jinja_imports"], list
280+
):
281+
validated_data["format_jinja_imports"] = [
282+
FormatJinjaImport.from_dict(item)
283+
for item in validated_data["format_jinja_imports"]
284+
if isinstance(item, dict)
285+
]
286+
242287
return cls(**validated_data)
243288

244289

src/uv_dynamic_versioning/template.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
import re
66
from datetime import datetime
7+
from importlib import import_module
78

89
import jinja2
910
from dunamai import (
@@ -65,4 +66,14 @@ def render_template(
6566
"serialize_pvp": serialize_pvp,
6667
"serialize_semver": serialize_semver,
6768
}
68-
return jinja2.Template(template).render(**default_context)
69+
70+
custom_context = {}
71+
if config.format_jinja_imports:
72+
for entry in config.format_jinja_imports:
73+
module = import_module(entry.module)
74+
if entry.item is not None:
75+
custom_context[entry.item] = getattr(module, entry.item)
76+
else:
77+
custom_context[entry.module] = module
78+
79+
return jinja2.Template(template).render(**default_context, **custom_context)

tests/test_main.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,32 @@ def test_get_version_with_invalid_combination_of_format_jinja_and_style():
6060
assert config.style == Style.Pep440
6161
with pytest.raises(ValueError):
6262
get_version(config)
63+
64+
65+
def test_get_version_with_format_jinja_imports_with_module_only():
66+
config = schemas.UvDynamicVersioning.from_dict(
67+
{
68+
"format-jinja": "{{ math.pow(2, 2) }}",
69+
"format-jinja-imports": [
70+
{
71+
"module": "math",
72+
}
73+
],
74+
}
75+
)
76+
assert get_version(config)[0] == "4.0"
77+
78+
79+
def test_get_version_with_format_jinja_imports_with_item():
80+
config = schemas.UvDynamicVersioning.from_dict(
81+
{
82+
"format-jinja": "{{ pow(2, 2) }}",
83+
"format-jinja-imports": [
84+
{
85+
"module": "math",
86+
"item": "pow",
87+
}
88+
],
89+
}
90+
)
91+
assert get_version(config)[0] == "4.0"

0 commit comments

Comments
 (0)