Skip to content

Commit f5c8cbc

Browse files
authored
Merge pull request #699 from tiran/annotations
feat: Add annotation support for packages and variants
2 parents 375dae2 + 2077cf8 commit f5c8cbc

3 files changed

Lines changed: 153 additions & 0 deletions

File tree

src/fromager/packagesettings.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,54 @@ def _validate_envkey(v: typing.Any) -> str:
102102
GlobalChangelog = Mapping[Variant, list[str]]
103103
VariantChangelog = Mapping[PackageVersion, list[str]]
104104

105+
# Annotations
106+
RawAnnotations = Mapping[str, str]
107+
108+
109+
class Annotations(Mapping):
110+
"""Read-only mapping for package annotations"""
111+
112+
__slots__ = "_mapping"
113+
114+
def __init__(
115+
self,
116+
package: RawAnnotations | None = None,
117+
variant: RawAnnotations | None = None,
118+
) -> None:
119+
self._mapping: RawAnnotations = {}
120+
if package:
121+
self._mapping.update(package)
122+
if variant:
123+
self._mapping.update(variant)
124+
125+
def __getitem__(self, key: str) -> str:
126+
return self._mapping[key]
127+
128+
def __iter__(self) -> typing.Iterator[str]:
129+
return iter(self._mapping)
130+
131+
def __len__(self) -> int:
132+
return len(self._mapping)
133+
134+
def __repr__(self):
135+
return repr(self._mapping)
136+
137+
def getbool(self, key: str) -> bool:
138+
"""Get bool from string value
139+
140+
raises :exc:`KeyError` when key is missing and :exc`ValueError` when
141+
the value is not 1, true, on, yes, 0, false, off, no.
142+
"""
143+
value = self[key]
144+
match value.lower():
145+
case "1" | "true" | "on" | "yes":
146+
return True
147+
case "0" | "false" | "off" | "no":
148+
return False
149+
case _:
150+
raise ValueError(value)
151+
152+
105153
# common settings
106154
MODEL_CONFIG = pydantic.ConfigDict(
107155
# don't accept unknown keys
@@ -279,6 +327,13 @@ class VariantInfo(pydantic.BaseModel):
279327

280328
model_config = MODEL_CONFIG
281329

330+
annotations: RawAnnotations | None = None
331+
"""Arbitrary metadata for variants
332+
333+
Variant annotation keys have a higher precedence than package
334+
annotation keys.
335+
"""
336+
282337
env: EnvVars = Field(default_factory=dict)
283338
"""Additional env vars (overrides package env vars)"""
284339

@@ -362,6 +417,9 @@ class PackageSettings(pydantic.BaseModel):
362417
has_config: bool
363418
"""package has override setting"""
364419

420+
annotations: RawAnnotations | None = None
421+
"""Arbitrary metadata for a package"""
422+
365423
build_dir: BuildDirectory | None = None
366424
"""sub-directory with setup.py or pyproject.toml"""
367425

@@ -568,6 +626,7 @@ def __init__(self, settings: Settings, ps: PackageSettings) -> None:
568626
self._ps = ps
569627
self._plugin_module: types.ModuleType | None | typing.Literal[False] = False
570628
self._patches: PatchMap | None = None
629+
self._annotations: Annotations | None = None
571630

572631
@property
573632
def package(self) -> NormalizedName:
@@ -579,6 +638,31 @@ def variant(self) -> Variant:
579638
"""Variant name"""
580639
return self._variant
581640

641+
@property
642+
def annotations(self) -> Annotations:
643+
"""Get Package and variant annotations
644+
645+
Annotations can be used to attach arbitrary metadata to packages and
646+
package variants. The feature is inspired by Kubernetes's
647+
annotations. Variant keys have a higher precedence than package keys.
648+
649+
The prefix ``fromager.`` is reserved for future use by Fromager.
650+
651+
::
652+
653+
annotations:
654+
"downstream.maintainer": "Platform Team"
655+
variants:
656+
cuda:
657+
annotations:
658+
"downstream.maintainer": "CUDA Accelerator Team"
659+
"""
660+
if self._annotations is None:
661+
vi = self._ps.variants.get(self.variant)
662+
va = vi.annotations if vi is not None else None
663+
self._annotations = Annotations(self._ps.annotations, va)
664+
return self._annotations
665+
582666
@property
583667
def plugin(self) -> types.ModuleType | None:
584668
"""Get Fromager plugin module"""

tests/test_packagesettings.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from fromager import build_environment, context
1212
from fromager.packagesettings import (
13+
Annotations,
1314
BuildDirectory,
1415
EnvVars,
1516
GitOptions,
@@ -28,6 +29,10 @@
2829
TEST_PREBUILT_PKG = "test-prebuilt-pkg"
2930

3031
FULL_EXPECTED: dict[str, typing.Any] = {
32+
"annotations": {
33+
"fromager.test.value": "somevalue",
34+
"fromager.test.override": "variant override",
35+
},
3136
"build_dir": pathlib.Path("python"),
3237
"build_options": {
3338
"build_ext_parallel": True,
@@ -76,16 +81,23 @@
7681
},
7782
"variants": {
7883
"cpu": {
84+
"annotations": {
85+
"fromager.test.override": "cpu override",
86+
},
7987
"env": {"EGG": "spam ${EGG}", "EGG_AGAIN": "$EGG"},
8088
"wheel_server_url": "https://wheel.test/simple",
8189
"pre_built": False,
8290
},
8391
"rocm": {
92+
"annotations": {
93+
"fromager.test.override": "amd override",
94+
},
8495
"env": {"SPAM": ""},
8596
"wheel_server_url": None,
8697
"pre_built": True,
8798
},
8899
"cuda": {
100+
"annotations": None,
89101
"env": {},
90102
"wheel_server_url": None,
91103
"pre_built": False,
@@ -95,6 +107,7 @@
95107

96108
EMPTY_EXPECTED: dict[str, typing.Any] = {
97109
"name": "test-empty-pkg",
110+
"annotations": None,
98111
"build_dir": None,
99112
"build_options": {
100113
"build_ext_parallel": False,
@@ -130,6 +143,7 @@
130143

131144
PREBUILT_PKG_EXPECTED: dict[str, typing.Any] = {
132145
"name": "test-prebuilt-pkg",
146+
"annotations": None,
133147
"build_dir": None,
134148
"build_options": {
135149
"build_ext_parallel": False,
@@ -164,6 +178,7 @@
164178
},
165179
"variants": {
166180
"cpu": {
181+
"annotations": None,
167182
"env": {},
168183
"pre_built": True,
169184
"wheel_server_url": None,
@@ -722,3 +737,50 @@ def test_package_build_info_exclusive_build(testdata_context: context.WorkContex
722737
def test_resolver_dist_validator():
723738
with pytest.raises(pydantic.ValidationError):
724739
ResolverDist(include_wheels=False, ignore_platform=True)
740+
741+
742+
def test_annotation_type() -> None:
743+
ann = Annotations(None, None)
744+
assert not ann
745+
assert len(ann) == 0
746+
assert ann == {}
747+
with pytest.raises(TypeError):
748+
ann["key"] = "value" # type: ignore
749+
750+
ann = Annotations({"ka": "va", "kb": "vb"}, {"kb": "otherb", "kc": "vc"})
751+
assert ann
752+
assert len(ann) == 3
753+
assert ann == {"ka": "va", "kb": "otherb", "kc": "vc"}
754+
755+
ann = Annotations({"t": "yes", "f": "no", "invalid": "invalid"}, {})
756+
assert ann.getbool("t") is True
757+
assert ann.getbool("f") is False
758+
with pytest.raises(ValueError):
759+
ann.getbool("invalid")
760+
with pytest.raises(KeyError):
761+
ann.getbool("missing")
762+
763+
764+
def test_pbi_annotations(testdata_context: context.WorkContext) -> None:
765+
pbi = testdata_context.settings.package_build_info(TEST_PKG)
766+
assert pbi.annotations == {
767+
"fromager.test.value": "somevalue",
768+
"fromager.test.override": "cpu override",
769+
}
770+
771+
testdata_context.settings.variant = Variant("cuda")
772+
pbi = testdata_context.settings.package_build_info(TEST_PKG)
773+
assert pbi.annotations == {
774+
"fromager.test.value": "somevalue",
775+
"fromager.test.override": "variant override",
776+
}
777+
778+
testdata_context.settings.variant = Variant("rocm")
779+
pbi = testdata_context.settings.package_build_info(TEST_PKG)
780+
assert pbi.annotations == {
781+
"fromager.test.value": "somevalue",
782+
"fromager.test.override": "amd override",
783+
}
784+
785+
pbi = testdata_context.settings.package_build_info(TEST_EMPTY_PKG)
786+
assert pbi.annotations == {}

tests/testdata/context/overrides/settings/test_pkg.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
annotations:
2+
fromager.test.value: somevalue
3+
fromager.test.override: variant override
14
build_dir: python
25
build_options:
36
build_ext_parallel: true
@@ -38,11 +41,15 @@ resolver_dist:
3841
ignore_platform: true
3942
variants:
4043
cpu:
44+
annotations:
45+
fromager.test.override: cpu override
4146
env:
4247
EGG: "spam ${EGG}"
4348
EGG_AGAIN: "$EGG"
4449
wheel_server_url: https://wheel.test/simple
4550
rocm:
51+
annotations:
52+
fromager.test.override: amd override
4653
env:
4754
SPAM: ""
4855
pre_built: True

0 commit comments

Comments
 (0)