1
1
from __future__ import annotations
2
2
3
+ import dataclasses
3
4
import os
4
5
import re
5
6
14
15
15
16
PRETEND_KEY = "SETUPTOOLS_SCM_PRETEND_VERSION"
16
17
PRETEND_KEY_NAMED = PRETEND_KEY + "_FOR_{name}"
18
+ PRETEND_METADATA_KEY = "SETUPTOOLS_SCM_PRETEND_METADATA"
19
+ PRETEND_METADATA_KEY_NAMED = PRETEND_METADATA_KEY + "_FOR_{name}"
17
20
18
21
19
22
def read_named_env (
@@ -30,6 +33,124 @@ def read_named_env(
30
33
return os .environ .get (f"{ tool } _{ name } " )
31
34
32
35
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
+
33
154
def _read_pretended_version_for (
34
155
config : _config .Configuration ,
35
156
) -> version .ScmVersion | None :
0 commit comments