11from __future__ import annotations
22
3+ import dataclasses
34import importlib
45import sys
5- from collections .abc import Generator , Iterable , Mapping
6+ from collections .abc import Iterator , Mapping
67from pathlib import Path
7- from typing import Any , Protocol , Union
8+ from typing import TYPE_CHECKING , Any , Protocol , Union , runtime_checkable
89
9- __all__ = ["load_dynamic_metadata" , "load_provider" ]
10+ from .info import ALL_FIELDS
11+
12+ if TYPE_CHECKING :
13+ from collections .abc import Generator , Iterable
14+
15+ StrMapping = Mapping [str , Any ]
16+ else :
17+ StrMapping = Mapping
18+
19+
20+ __all__ = ["load_dynamic_metadata" , "load_provider" , "process_dynamic_metadata" ]
1021
1122
1223def __dir__ () -> list [str ]:
1324 return __all__
1425
1526
27+ @runtime_checkable
1628class DynamicMetadataProtocol (Protocol ):
1729 def dynamic_metadata (
18- self , fields : Iterable [str ], settings : dict [str , Any ]
30+ self ,
31+ fields : Iterable [str ],
32+ settings : dict [str , Any ],
33+ project : Mapping [str , Any ],
1934 ) -> dict [str , Any ]: ...
2035
2136
37+ @runtime_checkable
2238class DynamicMetadataRequirementsProtocol (DynamicMetadataProtocol , Protocol ):
2339 def get_requires_for_dynamic_metadata (
2440 self , settings : dict [str , Any ]
2541 ) -> list [str ]: ...
2642
2743
44+ @runtime_checkable
2845class DynamicMetadataWheelProtocol (DynamicMetadataProtocol , Protocol ):
29- def dynamic_wheel (
30- self , field : str , settings : Mapping [str , Any ] | None = None
31- ) -> bool : ...
32-
33-
34- class DynamicMetadataRequirementsWheelProtocol (
35- DynamicMetadataRequirementsProtocol , DynamicMetadataWheelProtocol , Protocol
36- ): ...
46+ def dynamic_wheel (self , field : str , settings : Mapping [str , Any ]) -> bool : ...
3747
3848
3949DMProtocols = Union [
4050 DynamicMetadataProtocol ,
4151 DynamicMetadataRequirementsProtocol ,
4252 DynamicMetadataWheelProtocol ,
43- DynamicMetadataRequirementsWheelProtocol ,
4453]
4554
4655
@@ -63,13 +72,79 @@ def load_provider(
6372
6473
6574def load_dynamic_metadata (
66- metadata : Mapping [str , Mapping [str , str ]],
75+ metadata : Mapping [str , Mapping [str , Any ]],
6776) -> Generator [tuple [str , DMProtocols | None , dict [str , str ]], None , None ]:
6877 for field , orig_config in metadata .items ():
6978 if "provider" in orig_config :
79+ if field not in ALL_FIELDS :
80+ msg = f"{ field } is not a valid field"
81+ raise KeyError (msg )
7082 config = dict (orig_config )
7183 provider = config .pop ("provider" )
7284 provider_path = config .pop ("provider-path" , None )
73- yield field , load_provider (provider , provider_path ), config
85+ loaded_provider = load_provider (provider , provider_path )
86+ yield field , loaded_provider , config
7487 else :
7588 yield field , None , dict (orig_config )
89+
90+
91+ @dataclasses .dataclass
92+ class DynamicPyProject (StrMapping ):
93+ settings : dict [str , dict [str , Any ]]
94+ project : dict [str , Any ]
95+ providers : dict [str , DMProtocols ]
96+
97+ def __getitem__ (self , key : str ) -> Any :
98+ # Try to get the settings from either the static file or dynamic metadata provider
99+ if key in self .project :
100+ return self .project [key ]
101+
102+ # Check if we are in a loop, i.e. something else is already requesting
103+ # this key while trying to get another key
104+ if key not in self .providers :
105+ dep_type = "missing" if key in self .settings else "circular"
106+ msg = f"Encountered a { dep_type } dependency at { key } "
107+ raise ValueError (msg )
108+
109+ provider = self .providers .pop (key )
110+ self .project [key ] = provider .dynamic_metadata (key , self .settings [key ], self )
111+ self .project ["dynamic" ].remove (key )
112+
113+ return self .project [key ]
114+
115+ def __iter__ (self ) -> Iterator [str ]:
116+ # Iterate over the keys of the static settings
117+ yield from [* self .project .keys (), * self .providers .keys ()]
118+
119+ def __len__ (self ) -> int :
120+ return len (self .project ) + len (self .providers )
121+
122+ def __contains__ (self , key : object ) -> bool :
123+ return key in self .project or key in self .providers
124+
125+
126+ def process_dynamic_metadata (
127+ project : Mapping [str , Any ],
128+ metadata : Mapping [str , Mapping [str , Any ]],
129+ ) -> dict [str , Any ]:
130+ """Process dynamic metadata.
131+
132+ This function loads the dynamic metadata providers and calls them to
133+ generate the dynamic metadata. It takes the original project table and
134+ returns a new project table. Empty providers are not supported; you
135+ need to implement this yourself for now if you support that.
136+ """
137+
138+ initial = {f : (p , s ) for (f , p , s ) in load_dynamic_metadata (metadata )}
139+ for f , (p , _ ) in initial .items ():
140+ if p is None :
141+ msg = f"{ f } does not have a provider"
142+ raise KeyError (msg )
143+
144+ settings = DynamicPyProject (
145+ settings = {f : s for f , (p , s ) in initial .items () if p is not None },
146+ project = dict (project ),
147+ providers = {k : p for k , (p , _ ) in initial .items () if p is not None },
148+ )
149+
150+ return dict (settings )
0 commit comments