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
1213
1314from ..metadata import _ALL_FIELDS
1415
@@ -22,7 +23,10 @@ def __dir__() -> list[str]:
2223@runtime_checkable
2324class DynamicMetadataProtocol (Protocol ):
2425 def dynamic_metadata (
25- self , fields : Iterable [str ], settings : dict [str , Any ], metadata : dict [str , Any ]
26+ self ,
27+ fields : Iterable [str ],
28+ settings : dict [str , Any ],
29+ metadata : Mapping [str , Any ],
2630 ) -> dict [str , Any ]: ...
2731
2832
@@ -40,20 +44,10 @@ def dynamic_wheel(
4044 ) -> bool : ...
4145
4246
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-
5247DMProtocols = Union [
5348 DynamicMetadataProtocol ,
5449 DynamicMetadataRequirementsProtocol ,
5550 DynamicMetadataWheelProtocol ,
56- DynamicMetadataNeeds ,
5751]
5852
5953
@@ -77,39 +71,76 @@ def load_provider(
7771
7872def _load_dynamic_metadata (
7973 metadata : Mapping [str , Mapping [str , str ]],
80- ) -> Generator [
81- tuple [str , DMProtocols | None , dict [str , str ], frozenset [str ]], None , None
82- ]:
74+ ) -> Generator [tuple [str , DMProtocols , dict [str , str ]], None , None ]:
8375 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 []
76+ if "provider" not in orig_config :
77+ msg = "Missing provider in dynamic metadata"
78+ raise KeyError (msg )
79+
80+ if field not in _ALL_FIELDS :
81+ msg = f"{ field } is not a valid field"
82+ raise KeyError (msg )
83+ config = dict (orig_config )
84+ provider = config .pop ("provider" )
85+ provider_path = config .pop ("provider-path" , None )
86+ loaded_provider = load_provider (provider , provider_path )
87+ yield field , loaded_provider , config
88+
89+
90+ @dataclasses .dataclass
91+ class DynamicSettings (Mapping [str , Any ]):
92+ settings : dict [str , dict [str , Any ]]
93+ project : dict [str , Any ]
94+ providers : dict [str , DMProtocols ]
95+
96+ def __getitem__ (self , key : str ) -> Any :
97+ # Try to get the settings from either the static file or dynamic metadata provider
98+ if key in self .project :
99+ return self .project [key ]
100+
101+ # Check if we are in a loop, i.e. something else is already requesting
102+ # this key while trying to get another key
103+ if key not in self .providers :
104+ dep_type = "missing" if key in self .settings else "circular"
105+ msg = f"Encountered a { dep_type } dependency at { key } "
106+ raise ValueError (msg )
107+
108+ provider = self .providers .pop (key )
109+ sig = inspect .signature (provider .dynamic_metadata )
110+ if len (sig .parameters ) < 3 :
111+ # Backcompat for dynamic_metadata without metadata dict
112+ self .project [key ] = provider .dynamic_metadata ( # type: ignore[call-arg]
113+ key , self .settings [key ]
96114 )
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
101115 else :
102- yield field , None , dict (orig_config ), frozenset ()
116+ self .project [key ] = provider .dynamic_metadata (
117+ key , self .settings [key ], self .project
118+ )
119+ self .project ["dynamic" ].remove (key )
120+
121+ return self .project [key ]
122+
123+ def __iter__ (self ) -> Iterator [str ]:
124+ # Iterate over the keys of the static settings
125+ yield from self .project
126+
127+ # Iterate over the keys of the dynamic metadata providers
128+ yield from self .providers
129+
130+ def __len__ (self ) -> int :
131+ return len (self .project ) + len (self .providers )
103132
104133
105134def load_dynamic_metadata (
135+ project : Mapping [str , Any ],
106136 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 )}
137+ ) -> dict [str , Any ]:
138+ initial = {f : (p , c ) for (f , p , c ) in _load_dynamic_metadata (metadata )}
109139
110- dynamic_fields = initial .keys ()
111- sorter = TopologicalSorter (
112- {f : n & dynamic_fields for f , (_ , _ , n ) in initial .items ()}
140+ settings = DynamicSettings (
141+ settings = {f : c for f , (v , c ) in initial .items ()},
142+ project = dict (project ),
143+ providers = {k : v for k , (v , _ ) in initial .items ()},
113144 )
114- order = sorter . static_order ()
115- return [( f , * initial [ f ][: 2 ]) for f in order ]
145+
146+ return dict ( settings )
0 commit comments