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 , runtime_checkable
8-
9- from graphlib import TopologicalSorter
8+ from typing import TYPE_CHECKING , Any , Protocol , Union , runtime_checkable
109
1110from .info import ALL_FIELDS
1211
13- __all__ = ["load_dynamic_metadata" , "load_provider" ]
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" ]
1421
1522
1623def __dir__ () -> list [str ]:
@@ -38,20 +45,10 @@ def dynamic_wheel(
3845 ) -> bool : ...
3946
4047
41- @runtime_checkable
42- class DynamicMetadataNeeds (DynamicMetadataProtocol , Protocol ):
43- def dynamic_metadata_needs (
44- self ,
45- field : str ,
46- settings : Mapping [str , object ] | None = None ,
47- ) -> list [str ]: ...
48-
49-
5048DMProtocols = Union [
5149 DynamicMetadataProtocol ,
5250 DynamicMetadataRequirementsProtocol ,
5351 DynamicMetadataWheelProtocol ,
54- DynamicMetadataNeeds ,
5552]
5653
5754
@@ -73,11 +70,9 @@ def load_provider(
7370 sys .path .pop (0 )
7471
7572
76- def _load_dynamic_metadata (
73+ def load_dynamic_metadata (
7774 metadata : Mapping [str , Mapping [str , str ]],
78- ) -> Generator [
79- tuple [str , DMProtocols | None , dict [str , str ], frozenset [str ]], None , None
80- ]:
75+ ) -> Generator [tuple [str , DMProtocols | None , dict [str , str ]], None , None ]:
8176 for field , orig_config in metadata .items ():
8277 if "provider" in orig_config :
8378 if field not in ALL_FIELDS :
@@ -87,27 +82,73 @@ def _load_dynamic_metadata(
8782 provider = config .pop ("provider" )
8883 provider_path = config .pop ("provider-path" , None )
8984 loaded_provider = load_provider (provider , provider_path )
90- needs = frozenset (
91- loaded_provider .dynamic_metadata_needs (field , config )
92- if isinstance (loaded_provider , DynamicMetadataNeeds )
93- else []
94- )
95- if needs > ALL_FIELDS :
96- msg = f"Invalid dyanmic_metada_needs: { needs - ALL_FIELDS } "
97- raise KeyError (msg )
98- yield field , loaded_provider , config , needs
85+ yield field , loaded_provider , config
9986 else :
100- yield field , None , dict (orig_config ), frozenset ()
87+ yield field , None , dict (orig_config )
10188
10289
103- def load_dynamic_metadata (
104- metadata : Mapping [str , Mapping [str , str ]],
105- ) -> list [tuple [str , DMProtocols | None , dict [str , str ]]]:
106- initial = {f : (p , c , n ) for (f , p , c , n ) in _load_dynamic_metadata (metadata )}
90+ @dataclasses .dataclass
91+ class DynamicPyProject (StrMapping ):
92+ settings : dict [str , dict [str , Any ]]
93+ project : dict [str , Any ]
94+ providers : dict [str , DMProtocols ]
10795
108- dynamic_fields = initial .keys ()
109- sorter = TopologicalSorter (
110- {f : n & dynamic_fields for f , (_ , _ , n ) in initial .items ()}
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+ self .project [key ] = provider .dynamic_metadata (
110+ key , self .settings [key ], self .project
111+ )
112+ self .project ["dynamic" ].remove (key )
113+
114+ return self .project [key ]
115+
116+ def __iter__ (self ) -> Iterator [str ]:
117+ # Iterate over the keys of the static settings
118+ yield from self .project
119+
120+ # Iterate over the keys of the dynamic metadata providers
121+ yield from self .providers
122+
123+ def __len__ (self ) -> int :
124+ return len (self .project ) + len (self .providers )
125+
126+ def __contains__ (self , key : object ) -> bool :
127+ return key in self .project or key in self .providers
128+
129+
130+ def process_dynamic_metadata (
131+ project : Mapping [str , Any ],
132+ metadata : Mapping [str , Mapping [str , str ]],
133+ ) -> dict [str , Any ]:
134+ """Process dynamic metadata.
135+
136+ This function loads the dynamic metadata providers and calls them to
137+ generate the dynamic metadata. It takes the original project table and
138+ returns a new project table. Empty providers are not supported; you
139+ need to implement this yourself for now if you support that.
140+ """
141+
142+ initial = {f : (p , s ) for (f , p , s ) in load_dynamic_metadata (metadata )}
143+ for f , (p , _ ) in initial .items ():
144+ if p is None :
145+ msg = f"{ f } does not have a provider"
146+ raise KeyError (msg )
147+
148+ settings = DynamicPyProject (
149+ settings = {f : s for f , (p , s ) in initial .items () if p is not None },
150+ project = dict (project ),
151+ providers = {k : p for k , (p , _ ) in initial .items () if p is not None },
111152 )
112- order = sorter . static_order ()
113- return [( f , * initial [ f ][: 2 ]) for f in order ]
153+
154+ return dict ( settings )
0 commit comments