88
99import dataclasses
1010import logging
11+ import os
12+ import sys
1113from collections .abc import Mapping
14+ from datetime import date , datetime
1215from difflib import get_close_matches
13- from typing import Any
16+ from re import Pattern
17+ from typing import TYPE_CHECKING , Any , TypedDict , get_type_hints
1418
1519from packaging .utils import canonicalize_name
1620
1721from . import _config
22+ from . import _types as _t
1823from . import _version_schemes as version
19- from ._toml import load_toml_or_inline_map
24+ from ._version_cls import Version as _Version
25+
26+ if TYPE_CHECKING :
27+ pass
28+
29+ if sys .version_info >= (3 , 11 ):
30+ pass
31+ else :
32+ pass
2033
2134log = logging .getLogger (__name__ )
2235
36+
37+ # TypedDict schemas for TOML data validation and type hints
38+
39+
40+ class PretendMetadataDict (TypedDict , total = False ):
41+ """Schema for ScmVersion metadata fields that can be overridden via environment.
42+
43+ All fields are optional since partial overrides are allowed.
44+ """
45+
46+ tag : str | _Version
47+ distance : int
48+ node : str | None
49+ dirty : bool
50+ preformatted : bool
51+ branch : str | None
52+ node_date : date | None
53+ time : datetime
54+
55+
56+ class ConfigOverridesDict (TypedDict , total = False ):
57+ """Schema for Configuration fields that can be overridden via environment.
58+
59+ All fields are optional since partial overrides are allowed.
60+ """
61+
62+ # Configuration fields
63+ root : _t .PathT
64+ version_scheme : _t .VERSION_SCHEME
65+ local_scheme : _t .VERSION_SCHEME
66+ tag_regex : str | Pattern [str ]
67+ parentdir_prefix_version : str | None
68+ fallback_version : str | None
69+ fallback_root : _t .PathT
70+ write_to : _t .PathT | None
71+ write_to_template : str | None
72+ version_file : _t .PathT | None
73+ version_file_template : str | None
74+ parse : Any # ParseFunction - avoid circular import
75+ git_describe_command : _t .CMD_TYPE | None # deprecated but still supported
76+ dist_name : str | None
77+ version_cls : Any # type[_Version] - avoid circular import
78+ normalize : bool # Used in from_data
79+ search_parent_directories : bool
80+ parent : _t .PathT | None
81+ scm : dict [str , Any ] # Nested SCM configuration
82+
83+
2384PRETEND_KEY = "SETUPTOOLS_SCM_PRETEND_VERSION"
2485PRETEND_KEY_NAMED = PRETEND_KEY + "_FOR_{name}"
2586PRETEND_METADATA_KEY = "SETUPTOOLS_SCM_PRETEND_METADATA"
@@ -90,7 +151,7 @@ def _find_close_env_var_matches(
90151
91152def _read_pretended_metadata_for (
92153 config : _config .Configuration ,
93- ) -> dict [ str , Any ] | None :
154+ ) -> PretendMetadataDict | None :
94155 """read overridden metadata from the environment
95156
96157 tries ``SETUPTOOLS_SCM_PRETEND_METADATA``
@@ -99,8 +160,6 @@ def _read_pretended_metadata_for(
99160 Returns a dictionary with metadata field overrides like:
100161 {"node": "g1337beef", "distance": 4}
101162 """
102- import os
103-
104163 from .overrides import EnvReader
105164
106165 log .debug ("dist name: %s" , config .dist_name )
@@ -110,39 +169,15 @@ def _read_pretended_metadata_for(
110169 env = os .environ ,
111170 dist_name = config .dist_name ,
112171 )
113- pretended = reader .read ("PRETEND_METADATA" )
114172
115- if pretended :
116- try :
117- metadata_overrides = load_toml_or_inline_map (pretended )
118- # Validate that only known ScmVersion fields are provided
119- valid_fields = {
120- "tag" ,
121- "distance" ,
122- "node" ,
123- "dirty" ,
124- "preformatted" ,
125- "branch" ,
126- "node_date" ,
127- "time" ,
128- }
129- invalid_fields = set (metadata_overrides .keys ()) - valid_fields
130- if invalid_fields :
131- log .warning (
132- "Invalid metadata fields in pretend metadata: %s. "
133- "Valid fields are: %s" ,
134- invalid_fields ,
135- valid_fields ,
136- )
137- # Remove invalid fields but continue processing
138- for field in invalid_fields :
139- metadata_overrides .pop (field )
140-
141- return metadata_overrides or None
142- except Exception as e :
143- log .error ("Failed to parse pretend metadata: %s" , e )
144- return None
145- else :
173+ try :
174+ # Use schema validation during TOML parsing
175+ metadata_overrides = reader .read_toml (
176+ "PRETEND_METADATA" , schema = PretendMetadataDict
177+ )
178+ return metadata_overrides or None
179+ except Exception as e :
180+ log .error ("Failed to parse pretend metadata: %s" , e )
146181 return None
147182
148183
@@ -177,36 +212,41 @@ def _apply_metadata_overrides(
177212
178213 log .info ("Applying metadata overrides: %s" , metadata_overrides )
179214
180- # Define type checks and field mappings
181- from datetime import date , datetime
182-
183- field_specs : dict [str , tuple [type | tuple [type , type ], str ]] = {
184- "distance" : (int , "int" ),
185- "dirty" : (bool , "bool" ),
186- "preformatted" : (bool , "bool" ),
187- "node_date" : (date , "date" ),
188- "time" : (datetime , "datetime" ),
189- "node" : ((str , type (None )), "str or None" ),
190- "branch" : ((str , type (None )), "str or None" ),
191- # tag is special - can be multiple types, handled separately
192- }
193-
194- # Apply each override individually using dataclasses.replace for type safety
215+ # Get type hints from PretendMetadataDict for validation
216+ field_types = get_type_hints (PretendMetadataDict )
217+
218+ # Apply each override individually using dataclasses.replace
195219 result = scm_version
196220
197221 for field , value in metadata_overrides .items ():
198- if field in field_specs :
199- expected_type , type_name = field_specs [field ]
200- assert isinstance (value , expected_type ), (
201- f"{ field } must be { type_name } , got { type (value ).__name__ } : { value !r} "
202- )
203- result = dataclasses .replace (result , ** {field : value })
204- elif field == "tag" :
205- # tag can be Version, NonNormalizedVersion, or str - we'll let the assignment handle validation
206- result = dataclasses .replace (result , tag = value )
207- else :
208- # This shouldn't happen due to validation in _read_pretended_metadata_for
209- log .warning ("Unknown field '%s' in metadata overrides" , field )
222+ # Validate field types using the TypedDict annotations
223+ if field in field_types :
224+ expected_type = field_types [field ]
225+ # Handle Optional/Union types (e.g., str | None)
226+ if hasattr (expected_type , "__args__" ):
227+ # Union type - check if value is instance of any of the types
228+ valid = any (
229+ isinstance (value , t ) if t is not type (None ) else value is None
230+ for t in expected_type .__args__
231+ )
232+ if not valid :
233+ type_names = " | " .join (
234+ t .__name__ if t is not type (None ) else "None"
235+ for t in expected_type .__args__
236+ )
237+ raise TypeError (
238+ f"Field '{ field } ' must be { type_names } , "
239+ f"got { type (value ).__name__ } : { value !r} "
240+ )
241+ else :
242+ # Simple type
243+ if not isinstance (value , expected_type ):
244+ raise TypeError (
245+ f"Field '{ field } ' must be { expected_type .__name__ } , "
246+ f"got { type (value ).__name__ } : { value !r} "
247+ )
248+
249+ result = dataclasses .replace (result , ** {field : value }) # type: ignore[arg-type]
210250
211251 # Ensure config is preserved (should not be overridden)
212252 assert result .config is config , "Config must be preserved during metadata overrides"
@@ -222,8 +262,6 @@ def _read_pretended_version_for(
222262 tries ``SETUPTOOLS_SCM_PRETEND_VERSION``
223263 and ``SETUPTOOLS_SCM_PRETEND_VERSION_FOR_$UPPERCASE_DIST_NAME``
224264 """
225- import os
226-
227265 from .overrides import EnvReader
228266
229267 log .debug ("dist name: %s" , config .dist_name )
@@ -241,15 +279,16 @@ def _read_pretended_version_for(
241279 return None
242280
243281
244- def read_toml_overrides (dist_name : str | None ) -> dict [str , Any ]:
245- """Read TOML overrides from environment."""
246- import os
282+ def read_toml_overrides (dist_name : str | None ) -> ConfigOverridesDict :
283+ """Read TOML overrides from environment.
247284
285+ Validates that only known Configuration fields are provided.
286+ """
248287 from .overrides import EnvReader
249288
250289 reader = EnvReader (
251290 tools_names = ("SETUPTOOLS_SCM" , "VCS_VERSIONING" ),
252291 env = os .environ ,
253292 dist_name = dist_name ,
254293 )
255- return reader .read_toml ("OVERRIDES" )
294+ return reader .read_toml ("OVERRIDES" , schema = ConfigOverridesDict )
0 commit comments