@@ -102,6 +102,54 @@ def _validate_envkey(v: typing.Any) -> str:
102102GlobalChangelog = Mapping [Variant , list [str ]]
103103VariantChangelog = 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
106154MODEL_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"""
0 commit comments