Versioning It with your Version In Git - automatic package versioning based on VCS tags
—
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.
Abstract specifications for different ways to reference methods in configuration.
class MethodSpec(ABC):
"""
Abstract base class for method specifications parsed from versioningit
configurations.
"""
@abstractmethod
def load(self, project_dir: str | Path) -> Callable:
"""
Load & return the callable specified by the MethodSpec.
Parameters:
- project_dir: Project directory for loading project-local methods
Returns:
Callable: The loaded method function
"""Load methods from Python packaging entry points registered by packages.
@dataclass
class EntryPointSpec(MethodSpec):
"""
A parsed method specification identifying a Python packaging entry point.
"""
group: str
"""The name of the group in which to look up the entry point."""
name: str
"""The name of the entry point."""
def load(self, _project_dir: str | Path) -> Callable:
"""
Loads & returns the entry point.
Returns:
Callable: The loaded entry point function
Raises:
- ConfigError: if no such entry point exists
- MethodError: if the loaded entry point is not callable
"""Load methods from local Python modules within the project.
@dataclass
class CustomMethodSpec(MethodSpec):
"""
A parsed method specification identifying a callable in a local Python
module.
"""
module: str
"""The dotted name of the module containing the callable."""
value: str
"""The name of the callable object within the module."""
module_dir: Optional[str]
"""The directory in which the module is located; defaults to project_dir."""
def load(self, project_dir: str | Path) -> Callable:
"""
Loads the module and returns the callable.
Parameters:
- project_dir: Project directory added to sys.path for import
Returns:
Callable: The loaded method function
Raises:
- MethodError: if the object is not actually callable
"""Wrap already-loaded callable objects.
@dataclass
class CallableSpec(MethodSpec):
"""
A parsed method specification identifying a callable by the callable itself.
"""
func: Callable
"""The callable."""
def load(self, _project_dir: str | Path) -> Callable:
"""Return the callable."""Loaded method with user-supplied parameters for pipeline execution.
@dataclass
class VersioningitMethod:
"""
A loaded versioningit method and the user-supplied parameters to pass to it.
"""
method: Callable
"""The loaded method."""
params: dict[str, Any]
"""User-supplied parameters obtained from the original configuration."""
def __call__(self, **kwargs: Any) -> Any:
"""
Invokes the method with the given keyword arguments and the
user-supplied parameters.
Parameters:
- **kwargs: Step-specific arguments (e.g., tag, version, description)
Returns:
Any: Return value from the method (type depends on pipeline step)
"""Versioningit uses these entry point groups for built-in method discovery:
# Entry point groups used by versioningit
ENTRY_POINT_GROUPS = {
"versioningit.vcs": "VCS querying methods",
"versioningit.tag2version": "Tag to version conversion methods",
"versioningit.next_version": "Next version calculation methods",
"versioningit.format": "Version formatting methods",
"versioningit.template_fields": "Template field generation methods",
"versioningit.write": "File writing methods",
"versioningit.onbuild": "Build-time file modification methods"
}# Using built-in entry points
config = {
"vcs": {"method": "git"},
"tag2version": {"method": "basic"},
"next-version": {"method": "minor"},
"format": {"method": "basic"},
"template-fields": {"method": "basic"},
"write": {"method": "basic"},
"onbuild": {"method": "replace-version"}
}# Custom method in project module
config = {
"tag2version": {
"method": "mypackage.versioning:custom_tag2version",
"strip-prefix": "release-"
}
}
# Method in separate directory
config = {
"format": {
"method": "scripts.version_tools:custom_format",
"module-dir": "build_scripts"
}
}# Custom VCS method
def custom_vcs_method(*, project_dir: Path, params: dict[str, Any]) -> VCSDescription:
"""Custom VCS implementation."""
# Your custom logic here
return VCSDescription(
tag="v1.0.0",
state="exact",
branch="main",
fields={"custom_field": "value"}
)
# Custom tag2version method
def custom_tag2version(*, tag: str, params: dict[str, Any]) -> str:
"""Custom tag to version conversion."""
prefix = params.get("strip-prefix", "")
if tag.startswith(prefix):
tag = tag[len(prefix):]
return tag.lstrip("v")
# Custom format method
def custom_format(
*,
description: VCSDescription,
base_version: str,
next_version: str,
params: dict[str, Any]
) -> str:
"""Custom version formatting."""
if description.state == "exact":
return base_version
elif description.state == "distance":
distance = description.fields.get("distance", 0)
return f"{next_version}.dev{distance}"
else:
return f"{next_version}.dev0+dirty"# setup.py or pyproject.toml
[project.entry-points."versioningit.tag2version"]
mymethod = "mypackage.versioning:my_tag2version"
[project.entry-points."versioningit.format"]
myformat = "mypackage.versioning:my_format"from versioningit import Versioningit
from versioningit.methods import CallableSpec, VersioningitMethod
def my_custom_method(*, tag: str, params: dict[str, Any]) -> str:
return tag.replace("release-", "")
# Create method spec
callable_spec = CallableSpec(func=my_custom_method)
method = VersioningitMethod(
method=callable_spec.load("."),
params={}
)
# Use in configuration dict format
config = {
"vcs": {"method": "git"},
"tag2version": my_custom_method, # Direct callable
"format": {"method": "basic"}
}from versioningit.methods import EntryPointSpec, CustomMethodSpec
# Load entry point method
entry_spec = EntryPointSpec(group="versioningit.tag2version", name="basic")
basic_tag2version = entry_spec.load(".")
# Load custom method
custom_spec = CustomMethodSpec(
module="mypackage.versioning",
value="custom_tag2version",
module_dir=None
)
custom_tag2version = custom_spec.load("/path/to/project")
# Execute methods
result1 = basic_tag2version(tag="v1.2.3", params={"rmprefix": "v"})
result2 = custom_tag2version(tag="release-1.2.3", params={"strip-prefix": "release-"})from versioningit.methods import EntryPointSpec
from versioningit.errors import ConfigError, MethodError
try:
spec = EntryPointSpec(group="versioningit.tag2version", name="nonexistent")
method = spec.load(".")
except ConfigError as e:
print(f"Entry point not found: {e}")
try:
spec = CustomMethodSpec(
module="nonexistent_module",
value="some_function",
module_dir=None
)
method = spec.load(".")
except (ImportError, AttributeError) as e:
print(f"Failed to load custom method: {e}")
except MethodError as e:
print(f"Loaded object is not callable: {e}")from versioningit.core import Versioningit
from versioningit.config import Config
from versioningit.methods import CallableSpec, VersioningitMethod
# Custom pipeline with mix of methods
def custom_vcs(*, project_dir: Path, params: dict) -> VCSDescription:
# Custom VCS logic
pass
def custom_formatter(*, description: VCSDescription, base_version: str,
next_version: str, params: dict) -> str:
# Custom formatting logic
pass
# Build configuration programmatically
config_dict = {
"vcs": custom_vcs, # Direct callable
"tag2version": {"method": "basic"}, # Entry point
"next-version": {"method": "mypackage.custom:next_version"}, # Custom module
"format": custom_formatter, # Direct callable
"template-fields": {"method": "basic"},
}
# Create Versioningit instance
vgit = Versioningit.from_project_dir(config=config_dict)
version = vgit.get_version()def validated_method(*, tag: str, params: dict[str, Any]) -> str:
"""Example method with parameter validation."""
# Validate required parameters
if "required_param" not in params:
raise ConfigError("required_param is mandatory")
# Validate parameter types
if not isinstance(params.get("numeric_param", 0), int):
raise ConfigError("numeric_param must be an integer")
# Validate parameter values
valid_options = ["option1", "option2", "option3"]
if params.get("choice_param") not in valid_options:
raise ConfigError(f"choice_param must be one of {valid_options}")
# Method implementation
return tag.lstrip("v")Install with Tessl CLI
npx tessl i tessl/pypi-versioningit