11from __future__ import annotations
22
3+ import dataclasses
34import importlib
5+ import inspect
46import sys
7+ from collections .abc import Iterator , Mapping
58from pathlib import Path
69from typing import TYPE_CHECKING , Any , Protocol , Union , runtime_checkable
710
8- from graphlib import TopologicalSorter
9-
1011if TYPE_CHECKING :
11- from collections .abc import Generator , Iterable , Mapping
12+ from collections .abc import Generator , Iterable
13+
14+ StrMapping = Mapping [str , Any ]
15+ else :
16+ StrMapping = Mapping
1217
1318from ..metadata import _ALL_FIELDS
1419
@@ -22,7 +27,10 @@ def __dir__() -> list[str]:
2227@runtime_checkable
2328class DynamicMetadataProtocol (Protocol ):
2429 def dynamic_metadata (
25- self , fields : Iterable [str ], settings : dict [str , Any ], metadata : dict [str , Any ]
30+ self ,
31+ fields : Iterable [str ],
32+ settings : dict [str , Any ],
33+ metadata : Mapping [str , Any ],
2634 ) -> dict [str , Any ]: ...
2735
2836
@@ -40,20 +48,10 @@ def dynamic_wheel(
4048 ) -> bool : ...
4149
4250
43- @runtime_checkable
44- class DynamicMetadataNeeds (DynamicMetadataProtocol , Protocol ):
45- def dynamic_metadata_needs (
46- self ,
47- field : str ,
48- settings : Mapping [str , object ] | None = None ,
49- ) -> list [str ]: ...
50-
51-
5251DMProtocols = Union [
5352 DynamicMetadataProtocol ,
5453 DynamicMetadataRequirementsProtocol ,
5554 DynamicMetadataWheelProtocol ,
56- DynamicMetadataNeeds ,
5755]
5856
5957
@@ -77,39 +75,76 @@ def load_provider(
7775
7876def _load_dynamic_metadata (
7977 metadata : Mapping [str , Mapping [str , str ]],
80- ) -> Generator [
81- tuple [str , DMProtocols | None , dict [str , str ], frozenset [str ]], None , None
82- ]:
78+ ) -> Generator [tuple [str , DMProtocols , dict [str , str ]], None , None ]:
8379 for field , orig_config in metadata .items ():
84- if "provider" in orig_config :
85- if field not in _ALL_FIELDS :
86- msg = f"{ field } is not a valid field"
87- raise KeyError (msg )
88- config = dict (orig_config )
89- provider = config .pop ("provider" )
90- provider_path = config .pop ("provider-path" , None )
91- loaded_provider = load_provider (provider , provider_path )
92- needs = frozenset (
93- loaded_provider .dynamic_metadata_needs (field , config )
94- if isinstance (loaded_provider , DynamicMetadataNeeds )
95- else []
80+ if "provider" not in orig_config :
81+ msg = "Missing provider in dynamic metadata"
82+ raise KeyError (msg )
83+
84+ if field not in _ALL_FIELDS :
85+ msg = f"{ field } is not a valid field"
86+ raise KeyError (msg )
87+ config = dict (orig_config )
88+ provider = config .pop ("provider" )
89+ provider_path = config .pop ("provider-path" , None )
90+ loaded_provider = load_provider (provider , provider_path )
91+ yield field , loaded_provider , config
92+
93+
94+ @dataclasses .dataclass
95+ class DynamicSettings (StrMapping ):
96+ settings : dict [str , dict [str , Any ]]
97+ project : dict [str , Any ]
98+ providers : dict [str , DMProtocols ]
99+
100+ def __getitem__ (self , key : str ) -> Any :
101+ # Try to get the settings from either the static file or dynamic metadata provider
102+ if key in self .project :
103+ return self .project [key ]
104+
105+ # Check if we are in a loop, i.e. something else is already requesting
106+ # this key while trying to get another key
107+ if key not in self .providers :
108+ dep_type = "missing" if key in self .settings else "circular"
109+ msg = f"Encountered a { dep_type } dependency at { key } "
110+ raise ValueError (msg )
111+
112+ provider = self .providers .pop (key )
113+ sig = inspect .signature (provider .dynamic_metadata )
114+ if len (sig .parameters ) < 3 :
115+ # Backcompat for dynamic_metadata without metadata dict
116+ self .project [key ] = provider .dynamic_metadata ( # type: ignore[call-arg]
117+ key , self .settings [key ]
96118 )
97- if needs > _ALL_FIELDS :
98- msg = f"Invalid dyanmic_metada_needs: { needs - _ALL_FIELDS } "
99- raise KeyError (msg )
100- yield field , loaded_provider , config , needs
101119 else :
102- yield field , None , dict (orig_config ), frozenset ()
120+ self .project [key ] = provider .dynamic_metadata (
121+ key , self .settings [key ], self .project
122+ )
123+ self .project ["dynamic" ].remove (key )
124+
125+ return self .project [key ]
126+
127+ def __iter__ (self ) -> Iterator [str ]:
128+ # Iterate over the keys of the static settings
129+ yield from self .project
130+
131+ # Iterate over the keys of the dynamic metadata providers
132+ yield from self .providers
133+
134+ def __len__ (self ) -> int :
135+ return len (self .project ) + len (self .providers )
103136
104137
105138def load_dynamic_metadata (
139+ project : Mapping [str , Any ],
106140 metadata : Mapping [str , Mapping [str , str ]],
107- ) -> list [ tuple [ str , DMProtocols | None , dict [str , str ]] ]:
108- initial = {f : (p , c , n ) for (f , p , c , n ) in _load_dynamic_metadata (metadata )}
141+ ) -> dict [str , Any ]:
142+ initial = {f : (p , c ) for (f , p , c ) in _load_dynamic_metadata (metadata )}
109143
110- dynamic_fields = initial .keys ()
111- sorter = TopologicalSorter (
112- {f : n & dynamic_fields for f , (_ , _ , n ) in initial .items ()}
144+ settings = DynamicSettings (
145+ settings = {f : c for f , (v , c ) in initial .items ()},
146+ project = dict (project ),
147+ providers = {k : v for k , (v , _ ) in initial .items ()},
113148 )
114- order = sorter . static_order ()
115- return [( f , * initial [ f ][: 2 ]) for f in order ]
149+
150+ return dict ( settings )
0 commit comments