8
8
import warnings
9
9
10
10
from pathlib import Path
11
+ from typing import TYPE_CHECKING
11
12
from typing import Any
12
13
from typing import Pattern
13
14
from typing import Protocol
14
15
16
+ if TYPE_CHECKING :
17
+ from . import git
18
+
15
19
from . import _log
16
20
from . import _types as _t
17
21
from ._integration .pyproject_reading import PyProjectData
26
30
27
31
log = _log .log .getChild ("config" )
28
32
33
+
34
+ def _is_called_from_dataclasses () -> bool :
35
+ """Check if the current call is from the dataclasses module."""
36
+ import inspect
37
+
38
+ frame = inspect .currentframe ()
39
+ try :
40
+ # Walk up to 7 frames to check for dataclasses calls
41
+ current_frame = frame
42
+ assert current_frame is not None
43
+ for _ in range (7 ):
44
+ current_frame = current_frame .f_back
45
+ if current_frame is None :
46
+ break
47
+ if "dataclasses.py" in current_frame .f_code .co_filename :
48
+ return True
49
+ return False
50
+ finally :
51
+ del frame
52
+
53
+
54
+ class _GitDescribeCommandDescriptor :
55
+ """Data descriptor for deprecated git_describe_command field."""
56
+
57
+ def __get__ (
58
+ self , obj : Configuration | None , objtype : type [Configuration ] | None = None
59
+ ) -> _t .CMD_TYPE | None :
60
+ if obj is None :
61
+ return self # type: ignore[return-value]
62
+
63
+ # Only warn if not being called by dataclasses.replace or similar introspection
64
+ is_from_dataclasses = _is_called_from_dataclasses ()
65
+ if not is_from_dataclasses :
66
+ warnings .warn (
67
+ "Configuration field 'git_describe_command' is deprecated. "
68
+ "Use 'scm.git.describe_command' instead." ,
69
+ DeprecationWarning ,
70
+ stacklevel = 2 ,
71
+ )
72
+ return obj .scm .git .describe_command
73
+
74
+ def __set__ (self , obj : Configuration , value : _t .CMD_TYPE | None ) -> None :
75
+ warnings .warn (
76
+ "Configuration field 'git_describe_command' is deprecated. "
77
+ "Use 'scm.git.describe_command' instead." ,
78
+ DeprecationWarning ,
79
+ stacklevel = 2 ,
80
+ )
81
+ obj .scm .git .describe_command = value
82
+
83
+
29
84
DEFAULT_TAG_REGEX = re .compile (
30
85
r"^(?:[\w-]+-)?(?P<version>[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$"
31
86
)
@@ -52,6 +107,13 @@ def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]:
52
107
return regex
53
108
54
109
110
+ def _get_default_git_pre_parse () -> git .GitPreParse :
111
+ """Get the default git pre_parse enum value"""
112
+ from . import git
113
+
114
+ return git .GitPreParse .WARN_ON_SHALLOW
115
+
116
+
55
117
class ParseFunction (Protocol ):
56
118
def __call__ (
57
119
self , root : _t .PathT , * , config : Configuration
@@ -83,6 +145,54 @@ def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str:
83
145
return os .path .abspath (root )
84
146
85
147
148
+ @dataclasses .dataclass
149
+ class GitConfiguration :
150
+ """Git-specific configuration options"""
151
+
152
+ pre_parse : git .GitPreParse = dataclasses .field (
153
+ default_factory = lambda : _get_default_git_pre_parse ()
154
+ )
155
+ describe_command : _t .CMD_TYPE | None = None
156
+
157
+ @classmethod
158
+ def from_data (cls , data : dict [str , Any ]) -> GitConfiguration :
159
+ """Create GitConfiguration from configuration data, converting strings to enums"""
160
+ git_data = data .copy ()
161
+
162
+ # Convert string pre_parse values to enum instances
163
+ if "pre_parse" in git_data and isinstance (git_data ["pre_parse" ], str ):
164
+ from . import git
165
+
166
+ try :
167
+ git_data ["pre_parse" ] = git .GitPreParse (git_data ["pre_parse" ])
168
+ except ValueError as e :
169
+ valid_options = [option .value for option in git .GitPreParse ]
170
+ raise ValueError (
171
+ f"Invalid git pre_parse function '{ git_data ['pre_parse' ]} '. "
172
+ f"Valid options are: { ', ' .join (valid_options )} "
173
+ ) from e
174
+
175
+ return cls (** git_data )
176
+
177
+
178
+ @dataclasses .dataclass
179
+ class ScmConfiguration :
180
+ """SCM-specific configuration options"""
181
+
182
+ git : GitConfiguration = dataclasses .field (default_factory = GitConfiguration )
183
+
184
+ @classmethod
185
+ def from_data (cls , data : dict [str , Any ]) -> ScmConfiguration :
186
+ """Create ScmConfiguration from configuration data"""
187
+ scm_data = data .copy ()
188
+
189
+ # Handle git-specific configuration
190
+ git_data = scm_data .pop ("git" , {})
191
+ git_config = GitConfiguration .from_data (git_data )
192
+
193
+ return cls (git = git_config , ** scm_data )
194
+
195
+
86
196
@dataclasses .dataclass
87
197
class Configuration :
88
198
"""Global configuration model"""
@@ -100,16 +210,57 @@ class Configuration:
100
210
version_file : _t .PathT | None = None
101
211
version_file_template : str | None = None
102
212
parse : ParseFunction | None = None
103
- git_describe_command : _t .CMD_TYPE | None = None
213
+ git_describe_command : dataclasses .InitVar [_t .CMD_TYPE | None ] = (
214
+ _GitDescribeCommandDescriptor ()
215
+ )
216
+
104
217
dist_name : str | None = None
105
218
version_cls : type [_VersionT ] = _Version
106
219
search_parent_directories : bool = False
107
220
108
221
parent : _t .PathT | None = None
109
222
110
- def __post_init__ (self ) -> None :
223
+ # Nested SCM configurations
224
+ scm : ScmConfiguration = dataclasses .field (
225
+ default_factory = lambda : ScmConfiguration ()
226
+ )
227
+
228
+ # Deprecated fields (handled in __post_init__)
229
+
230
+ def __post_init__ (self , git_describe_command : _t .CMD_TYPE | None ) -> None :
111
231
self .tag_regex = _check_tag_regex (self .tag_regex )
112
232
233
+ # Handle deprecated git_describe_command
234
+ # Check if it's a descriptor object (happens when no value is passed)
235
+ if git_describe_command is not None and not isinstance (
236
+ git_describe_command , _GitDescribeCommandDescriptor
237
+ ):
238
+ # Check if this is being called from dataclasses
239
+ is_from_dataclasses = _is_called_from_dataclasses ()
240
+
241
+ same_value = (
242
+ self .scm .git .describe_command is not None
243
+ and self .scm .git .describe_command == git_describe_command
244
+ )
245
+
246
+ if is_from_dataclasses and same_value :
247
+ # Ignore the passed value - it's from dataclasses.replace() with same value
248
+ pass
249
+ else :
250
+ warnings .warn (
251
+ "Configuration field 'git_describe_command' is deprecated. "
252
+ "Use 'scm.git.describe_command' instead." ,
253
+ DeprecationWarning ,
254
+ stacklevel = 2 ,
255
+ )
256
+ # Check for conflicts
257
+ if self .scm .git .describe_command is not None :
258
+ raise ValueError (
259
+ "Cannot specify both 'git_describe_command' (deprecated) and "
260
+ "'scm.git.describe_command'. Please use only 'scm.git.describe_command'."
261
+ )
262
+ self .scm .git .describe_command = git_describe_command
263
+
113
264
@property
114
265
def absolute_root (self ) -> str :
115
266
return _check_absolute_root (self .root , self .relative_to )
@@ -161,8 +312,17 @@ def from_data(
161
312
version_cls = _validate_version_cls (
162
313
data .pop ("version_cls" , None ), data .pop ("normalize" , True )
163
314
)
315
+
316
+ # Handle nested SCM configuration
317
+ scm_data = data .pop ("scm" , {})
318
+
319
+ # Handle nested SCM configuration
320
+
321
+ scm_config = ScmConfiguration .from_data (scm_data )
322
+
164
323
return cls (
165
324
relative_to = relative_to ,
166
325
version_cls = version_cls ,
326
+ scm = scm_config ,
167
327
** data ,
168
328
)
0 commit comments