11from __future__ import annotations
22
3+ import dataclasses
34import os
45import re
56
1415
1516PRETEND_KEY = "SETUPTOOLS_SCM_PRETEND_VERSION"
1617PRETEND_KEY_NAMED = PRETEND_KEY + "_FOR_{name}"
18+ PRETEND_METADATA_KEY = "SETUPTOOLS_SCM_PRETEND_METADATA"
19+ PRETEND_METADATA_KEY_NAMED = PRETEND_METADATA_KEY + "_FOR_{name}"
1720
1821
1922def read_named_env (
@@ -30,6 +33,124 @@ def read_named_env(
3033 return os .environ .get (f"{ tool } _{ name } " )
3134
3235
36+ def _read_pretended_metadata_for (
37+ config : _config .Configuration ,
38+ ) -> dict [str , Any ] | None :
39+ """read overridden metadata from the environment
40+
41+ tries ``SETUPTOOLS_SCM_PRETEND_METADATA``
42+ and ``SETUPTOOLS_SCM_PRETEND_METADATA_FOR_$UPPERCASE_DIST_NAME``
43+
44+ Returns a dictionary with metadata field overrides like:
45+ {"node": "g1337beef", "distance": 4}
46+ """
47+ log .debug ("dist name: %s" , config .dist_name )
48+
49+ pretended = read_named_env (name = "PRETEND_METADATA" , dist_name = config .dist_name )
50+
51+ if pretended :
52+ try :
53+ metadata_overrides = load_toml_or_inline_map (pretended )
54+ # Validate that only known ScmVersion fields are provided
55+ valid_fields = {
56+ "tag" ,
57+ "distance" ,
58+ "node" ,
59+ "dirty" ,
60+ "preformatted" ,
61+ "branch" ,
62+ "node_date" ,
63+ "time" ,
64+ }
65+ invalid_fields = set (metadata_overrides .keys ()) - valid_fields
66+ if invalid_fields :
67+ log .warning (
68+ "Invalid metadata fields in pretend metadata: %s. "
69+ "Valid fields are: %s" ,
70+ invalid_fields ,
71+ valid_fields ,
72+ )
73+ # Remove invalid fields but continue processing
74+ for field in invalid_fields :
75+ metadata_overrides .pop (field )
76+
77+ return metadata_overrides or None
78+ except Exception as e :
79+ log .error ("Failed to parse pretend metadata: %s" , e )
80+ return None
81+ else :
82+ return None
83+
84+
85+ def _apply_metadata_overrides (
86+ scm_version : version .ScmVersion | None ,
87+ config : _config .Configuration ,
88+ ) -> version .ScmVersion | None :
89+ """Apply metadata overrides to a ScmVersion object.
90+
91+ This function reads pretend metadata from environment variables and applies
92+ the overrides to the given ScmVersion. TOML type coercion is used so values
93+ should be provided in their correct types (int, bool, datetime, etc.).
94+
95+ Args:
96+ scm_version: The ScmVersion to apply overrides to, or None
97+ config: Configuration object
98+
99+ Returns:
100+ Modified ScmVersion with overrides applied, or None
101+ """
102+ metadata_overrides = _read_pretended_metadata_for (config )
103+
104+ if not metadata_overrides :
105+ return scm_version
106+
107+ if scm_version is None :
108+ log .warning (
109+ "PRETEND_METADATA specified but no base version found. "
110+ "Metadata overrides cannot be applied without a base version."
111+ )
112+ return None
113+
114+ log .info ("Applying metadata overrides: %s" , metadata_overrides )
115+
116+ # Define type checks and field mappings
117+ from datetime import date
118+ from datetime import datetime
119+
120+ field_specs : dict [str , tuple [type | tuple [type , type ], str ]] = {
121+ "distance" : (int , "int" ),
122+ "dirty" : (bool , "bool" ),
123+ "preformatted" : (bool , "bool" ),
124+ "node_date" : (date , "date" ),
125+ "time" : (datetime , "datetime" ),
126+ "node" : ((str , type (None )), "str or None" ),
127+ "branch" : ((str , type (None )), "str or None" ),
128+ # tag is special - can be multiple types, handled separately
129+ }
130+
131+ # Apply each override individually using dataclasses.replace for type safety
132+ result = scm_version
133+
134+ for field , value in metadata_overrides .items ():
135+ if field in field_specs :
136+ expected_type , type_name = field_specs [field ]
137+ assert isinstance (value , expected_type ), (
138+ f"{ field } must be { type_name } , got { type (value ).__name__ } : { value !r} "
139+ )
140+ result = dataclasses .replace (result , ** {field : value })
141+ elif field == "tag" :
142+ # tag can be Version, NonNormalizedVersion, or str - we'll let the assignment handle validation
143+ result = dataclasses .replace (result , tag = value )
144+ else :
145+ # This shouldn't happen due to validation in _read_pretended_metadata_for
146+ log .warning ("Unknown field '%s' in metadata overrides" , field )
147+
148+ # Ensure config is preserved (should not be overridden)
149+ assert result .config is config , "Config must be preserved during metadata overrides"
150+
151+ return result
152+
153+
33154def _read_pretended_version_for (
34155 config : _config .Configuration ,
35156) -> version .ScmVersion | None :
0 commit comments