88import warnings
99
1010from pathlib import Path
11+ from typing import TYPE_CHECKING
1112from typing import Any
1213from typing import Pattern
1314from typing import Protocol
1415
16+ if TYPE_CHECKING :
17+ from . import git
18+
1519from . import _log
1620from . import _types as _t
1721from ._integration .pyproject_reading import PyProjectData
2630
2731log = _log .log .getChild ("config" )
2832
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+
2984DEFAULT_TAG_REGEX = re .compile (
3085 r"^(?:[\w-]+-)?(?P<version>[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$"
3186)
@@ -52,6 +107,13 @@ def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]:
52107 return regex
53108
54109
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+
55117class ParseFunction (Protocol ):
56118 def __call__ (
57119 self , root : _t .PathT , * , config : Configuration
@@ -83,6 +145,54 @@ def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str:
83145 return os .path .abspath (root )
84146
85147
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+
86196@dataclasses .dataclass
87197class Configuration :
88198 """Global configuration model"""
@@ -100,16 +210,57 @@ class Configuration:
100210 version_file : _t .PathT | None = None
101211 version_file_template : str | None = None
102212 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+
104217 dist_name : str | None = None
105218 version_cls : type [_VersionT ] = _Version
106219 search_parent_directories : bool = False
107220
108221 parent : _t .PathT | None = None
109222
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 :
111231 self .tag_regex = _check_tag_regex (self .tag_regex )
112232
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+
113264 @property
114265 def absolute_root (self ) -> str :
115266 return _check_absolute_root (self .root , self .relative_to )
@@ -161,8 +312,17 @@ def from_data(
161312 version_cls = _validate_version_cls (
162313 data .pop ("version_cls" , None ), data .pop ("normalize" , True )
163314 )
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+
164323 return cls (
165324 relative_to = relative_to ,
166325 version_cls = version_cls ,
326+ scm = scm_config ,
167327 ** data ,
168328 )
0 commit comments