0
# Method System
1
2
Framework for loading and executing custom methods via entry points, module imports, or direct callables. This system enables complete customization of versioningit's pipeline steps while maintaining a consistent interface.
3
4
## Capabilities
5
6
### Method Specifications
7
8
Abstract specifications for different ways to reference methods in configuration.
9
10
```python { .api }
11
class MethodSpec(ABC):
12
"""
13
Abstract base class for method specifications parsed from versioningit
14
configurations.
15
"""
16
17
@abstractmethod
18
def load(self, project_dir: str | Path) -> Callable:
19
"""
20
Load & return the callable specified by the MethodSpec.
21
22
Parameters:
23
- project_dir: Project directory for loading project-local methods
24
25
Returns:
26
Callable: The loaded method function
27
"""
28
```
29
30
### Entry Point Methods
31
32
Load methods from Python packaging entry points registered by packages.
33
34
```python { .api }
35
@dataclass
36
class EntryPointSpec(MethodSpec):
37
"""
38
A parsed method specification identifying a Python packaging entry point.
39
"""
40
41
group: str
42
"""The name of the group in which to look up the entry point."""
43
44
name: str
45
"""The name of the entry point."""
46
47
def load(self, _project_dir: str | Path) -> Callable:
48
"""
49
Loads & returns the entry point.
50
51
Returns:
52
Callable: The loaded entry point function
53
54
Raises:
55
- ConfigError: if no such entry point exists
56
- MethodError: if the loaded entry point is not callable
57
"""
58
```
59
60
### Custom Module Methods
61
62
Load methods from local Python modules within the project.
63
64
```python { .api }
65
@dataclass
66
class CustomMethodSpec(MethodSpec):
67
"""
68
A parsed method specification identifying a callable in a local Python
69
module.
70
"""
71
72
module: str
73
"""The dotted name of the module containing the callable."""
74
75
value: str
76
"""The name of the callable object within the module."""
77
78
module_dir: Optional[str]
79
"""The directory in which the module is located; defaults to project_dir."""
80
81
def load(self, project_dir: str | Path) -> Callable:
82
"""
83
Loads the module and returns the callable.
84
85
Parameters:
86
- project_dir: Project directory added to sys.path for import
87
88
Returns:
89
Callable: The loaded method function
90
91
Raises:
92
- MethodError: if the object is not actually callable
93
"""
94
```
95
96
### Direct Callable Methods
97
98
Wrap already-loaded callable objects.
99
100
```python { .api }
101
@dataclass
102
class CallableSpec(MethodSpec):
103
"""
104
A parsed method specification identifying a callable by the callable itself.
105
"""
106
107
func: Callable
108
"""The callable."""
109
110
def load(self, _project_dir: str | Path) -> Callable:
111
"""Return the callable."""
112
```
113
114
### Method Execution
115
116
Loaded method with user-supplied parameters for pipeline execution.
117
118
```python { .api }
119
@dataclass
120
class VersioningitMethod:
121
"""
122
A loaded versioningit method and the user-supplied parameters to pass to it.
123
"""
124
125
method: Callable
126
"""The loaded method."""
127
128
params: dict[str, Any]
129
"""User-supplied parameters obtained from the original configuration."""
130
131
def __call__(self, **kwargs: Any) -> Any:
132
"""
133
Invokes the method with the given keyword arguments and the
134
user-supplied parameters.
135
136
Parameters:
137
- **kwargs: Step-specific arguments (e.g., tag, version, description)
138
139
Returns:
140
Any: Return value from the method (type depends on pipeline step)
141
"""
142
```
143
144
## Entry Point Groups
145
146
Versioningit uses these entry point groups for built-in method discovery:
147
148
```python { .api }
149
# Entry point groups used by versioningit
150
ENTRY_POINT_GROUPS = {
151
"versioningit.vcs": "VCS querying methods",
152
"versioningit.tag2version": "Tag to version conversion methods",
153
"versioningit.next_version": "Next version calculation methods",
154
"versioningit.format": "Version formatting methods",
155
"versioningit.template_fields": "Template field generation methods",
156
"versioningit.write": "File writing methods",
157
"versioningit.onbuild": "Build-time file modification methods"
158
}
159
```
160
161
## Usage Examples
162
163
### Entry Point Method Configuration
164
165
```python
166
# Using built-in entry points
167
config = {
168
"vcs": {"method": "git"},
169
"tag2version": {"method": "basic"},
170
"next-version": {"method": "minor"},
171
"format": {"method": "basic"},
172
"template-fields": {"method": "basic"},
173
"write": {"method": "basic"},
174
"onbuild": {"method": "replace-version"}
175
}
176
```
177
178
### Custom Module Method Configuration
179
180
```python
181
# Custom method in project module
182
config = {
183
"tag2version": {
184
"method": "mypackage.versioning:custom_tag2version",
185
"strip-prefix": "release-"
186
}
187
}
188
189
# Method in separate directory
190
config = {
191
"format": {
192
"method": "scripts.version_tools:custom_format",
193
"module-dir": "build_scripts"
194
}
195
}
196
```
197
198
### Implementing Custom Methods
199
200
```python
201
# Custom VCS method
202
def custom_vcs_method(*, project_dir: Path, params: dict[str, Any]) -> VCSDescription:
203
"""Custom VCS implementation."""
204
# Your custom logic here
205
return VCSDescription(
206
tag="v1.0.0",
207
state="exact",
208
branch="main",
209
fields={"custom_field": "value"}
210
)
211
212
# Custom tag2version method
213
def custom_tag2version(*, tag: str, params: dict[str, Any]) -> str:
214
"""Custom tag to version conversion."""
215
prefix = params.get("strip-prefix", "")
216
if tag.startswith(prefix):
217
tag = tag[len(prefix):]
218
return tag.lstrip("v")
219
220
# Custom format method
221
def custom_format(
222
*,
223
description: VCSDescription,
224
base_version: str,
225
next_version: str,
226
params: dict[str, Any]
227
) -> str:
228
"""Custom version formatting."""
229
if description.state == "exact":
230
return base_version
231
elif description.state == "distance":
232
distance = description.fields.get("distance", 0)
233
return f"{next_version}.dev{distance}"
234
else:
235
return f"{next_version}.dev0+dirty"
236
```
237
238
### Creating Entry Point Methods
239
240
```python
241
# setup.py or pyproject.toml
242
[project.entry-points."versioningit.tag2version"]
243
mymethod = "mypackage.versioning:my_tag2version"
244
245
[project.entry-points."versioningit.format"]
246
myformat = "mypackage.versioning:my_format"
247
```
248
249
### Direct Callable Configuration
250
251
```python
252
from versioningit import Versioningit
253
from versioningit.methods import CallableSpec, VersioningitMethod
254
255
def my_custom_method(*, tag: str, params: dict[str, Any]) -> str:
256
return tag.replace("release-", "")
257
258
# Create method spec
259
callable_spec = CallableSpec(func=my_custom_method)
260
method = VersioningitMethod(
261
method=callable_spec.load("."),
262
params={}
263
)
264
265
# Use in configuration dict format
266
config = {
267
"vcs": {"method": "git"},
268
"tag2version": my_custom_method, # Direct callable
269
"format": {"method": "basic"}
270
}
271
```
272
273
### Method Loading and Execution
274
275
```python
276
from versioningit.methods import EntryPointSpec, CustomMethodSpec
277
278
# Load entry point method
279
entry_spec = EntryPointSpec(group="versioningit.tag2version", name="basic")
280
basic_tag2version = entry_spec.load(".")
281
282
# Load custom method
283
custom_spec = CustomMethodSpec(
284
module="mypackage.versioning",
285
value="custom_tag2version",
286
module_dir=None
287
)
288
custom_tag2version = custom_spec.load("/path/to/project")
289
290
# Execute methods
291
result1 = basic_tag2version(tag="v1.2.3", params={"rmprefix": "v"})
292
result2 = custom_tag2version(tag="release-1.2.3", params={"strip-prefix": "release-"})
293
```
294
295
### Error Handling in Method System
296
297
```python
298
from versioningit.methods import EntryPointSpec
299
from versioningit.errors import ConfigError, MethodError
300
301
try:
302
spec = EntryPointSpec(group="versioningit.tag2version", name="nonexistent")
303
method = spec.load(".")
304
except ConfigError as e:
305
print(f"Entry point not found: {e}")
306
307
try:
308
spec = CustomMethodSpec(
309
module="nonexistent_module",
310
value="some_function",
311
module_dir=None
312
)
313
method = spec.load(".")
314
except (ImportError, AttributeError) as e:
315
print(f"Failed to load custom method: {e}")
316
except MethodError as e:
317
print(f"Loaded object is not callable: {e}")
318
```
319
320
### Advanced Method System Usage
321
322
```python
323
from versioningit.core import Versioningit
324
from versioningit.config import Config
325
from versioningit.methods import CallableSpec, VersioningitMethod
326
327
# Custom pipeline with mix of methods
328
def custom_vcs(*, project_dir: Path, params: dict) -> VCSDescription:
329
# Custom VCS logic
330
pass
331
332
def custom_formatter(*, description: VCSDescription, base_version: str,
333
next_version: str, params: dict) -> str:
334
# Custom formatting logic
335
pass
336
337
# Build configuration programmatically
338
config_dict = {
339
"vcs": custom_vcs, # Direct callable
340
"tag2version": {"method": "basic"}, # Entry point
341
"next-version": {"method": "mypackage.custom:next_version"}, # Custom module
342
"format": custom_formatter, # Direct callable
343
"template-fields": {"method": "basic"},
344
}
345
346
# Create Versioningit instance
347
vgit = Versioningit.from_project_dir(config=config_dict)
348
version = vgit.get_version()
349
```
350
351
### Method Parameter Validation
352
353
```python
354
def validated_method(*, tag: str, params: dict[str, Any]) -> str:
355
"""Example method with parameter validation."""
356
357
# Validate required parameters
358
if "required_param" not in params:
359
raise ConfigError("required_param is mandatory")
360
361
# Validate parameter types
362
if not isinstance(params.get("numeric_param", 0), int):
363
raise ConfigError("numeric_param must be an integer")
364
365
# Validate parameter values
366
valid_options = ["option1", "option2", "option3"]
367
if params.get("choice_param") not in valid_options:
368
raise ConfigError(f"choice_param must be one of {valid_options}")
369
370
# Method implementation
371
return tag.lstrip("v")
372
```