Skip to content

Commit ec80406

Browse files
feat: add experimental integrator workflow API for composable configuration building
Add composable helper API for integrators to build configurations with proper override priority handling. The API is marked experimental and supports starting from PyProjectData which integrators compose themselves. Key Features: - Composable helpers (substantial orchestration, not thin wrappers) - Public API only accepts tool.vcs-versioning sections - Internal API supports multiple tool names for setuptools_scm transition - Proper override priority: env vars > integrator > config file > defaults New Public API (Experimental): - PyProjectData.from_file() - load pyproject data - build_configuration_from_pyproject() - orchestrate config building workflow Implementation: - vcs_versioning/_integrator_helpers.py: substantial helper with full workflow - vcs_versioning/_pyproject_reading.py: added from_file() class method - vcs_versioning/__init__.py: exported experimental API - Simplified read_pyproject() to only use tool_names parameter Integrator Workflow: 1. Setup GlobalOverrides context (handles env vars, logging) 2. Load PyProjectData (from file or manual composition) 3. Build Configuration (orchestrates overrides with proper priority) 4. Infer version (existing API) Override Priority (highest to lowest): 1. Environment TOML overrides (users always win) 2. Integrator overrides (integrator defaults/transformations) 3. Config file (pyproject.toml) 4. Defaults Testing: - 23 comprehensive tests for integrator helpers (all passing) - Tests cover: public API, internal API, override priorities, composition - Fixed pre-existing test_internal_log_level.py to use correct API Documentation: - Updated docs/integrators.md with complete workflow examples - Added experimental API section with Hatch integration example - Documented override priorities and tool section naming setuptools_scm Integration: - Updated to use internal multi-tool API - Supports both setuptools_scm and vcs-versioning sections - Maintains backward compatibility Files Changed: - NEW: vcs-versioning/src/vcs_versioning/_integrator_helpers.py - NEW: vcs-versioning/testing_vcs/test_integrator_helpers.py - MODIFIED: vcs-versioning/src/vcs_versioning/_pyproject_reading.py - MODIFIED: vcs-versioning/src/vcs_versioning/__init__.py - MODIFIED: setuptools-scm/src/setuptools_scm/_integration/pyproject_reading.py - MODIFIED: docs/integrators.md - FIXED: vcs-versioning/testing_vcs/test_internal_log_level.py All tests passing: 321 vcs-versioning, 16 setuptools_scm integration
1 parent 13459b7 commit ec80406

File tree

7 files changed

+1104
-37
lines changed

7 files changed

+1104
-37
lines changed

docs/integrators.md

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,263 @@ def my_function():
737737

738738
All internal vcs-versioning modules automatically use the active override context, so you don't need to change their usage.
739739

740+
## Experimental Integrator API
741+
742+
!!! warning "Experimental"
743+
This API is marked as experimental and may change in future versions.
744+
Use with caution in production code.
745+
746+
vcs-versioning provides helper functions for integrators to build configurations with proper override priority handling.
747+
748+
### Overview
749+
750+
The experimental API provides:
751+
- `PyProjectData`: Public class for composing pyproject.toml data
752+
- `build_configuration_from_pyproject()`: Substantial orchestration helper for building Configuration
753+
754+
### Priority Order
755+
756+
When building configurations, overrides are applied in this priority order (highest to lowest):
757+
758+
1. **Environment TOML overrides** - `TOOL_OVERRIDES_FOR_DIST`, `TOOL_OVERRIDES`
759+
2. **Integrator overrides** - Python arguments passed by the integrator
760+
3. **Config file** - `pyproject.toml` `[tool.vcs-versioning]` section
761+
4. **Defaults** - vcs-versioning defaults
762+
763+
This ensures that:
764+
- Users can always override via environment variables
765+
- Integrators can provide their own defaults/transformations
766+
- Config file settings are respected
767+
- Sensible defaults are always available
768+
769+
### Basic Workflow
770+
771+
```python
772+
from vcs_versioning import (
773+
PyProjectData,
774+
build_configuration_from_pyproject,
775+
infer_version_string,
776+
)
777+
from vcs_versioning.overrides import GlobalOverrides
778+
779+
def get_version_for_my_tool(pyproject_path="pyproject.toml", dist_name=None):
780+
"""Complete integrator workflow."""
781+
# 1. Setup global overrides context (handles env vars, logging, etc.)
782+
with GlobalOverrides.from_env("MY_TOOL", dist_name=dist_name):
783+
784+
# 2. Load pyproject data
785+
pyproject = PyProjectData.from_file(pyproject_path)
786+
787+
# 3. Build configuration with proper override priority
788+
config = build_configuration_from_pyproject(
789+
pyproject_data=pyproject,
790+
dist_name=dist_name,
791+
# Optional: integrator overrides (override config file, not env)
792+
# local_scheme="no-local-version",
793+
)
794+
795+
# 4. Infer version
796+
version = infer_version_string(
797+
dist_name=dist_name or pyproject.project_name,
798+
pyproject_data=pyproject,
799+
)
800+
801+
return version
802+
```
803+
804+
### PyProjectData Composition
805+
806+
Integrators can create `PyProjectData` in two ways:
807+
808+
#### 1. From File (Recommended)
809+
810+
```python
811+
from vcs_versioning import PyProjectData
812+
813+
# Load from pyproject.toml (reads tool.vcs-versioning section)
814+
pyproject = PyProjectData.from_file("pyproject.toml")
815+
```
816+
817+
#### 2. Manual Composition
818+
819+
If your tool already has its own TOML reading logic:
820+
821+
```python
822+
from pathlib import Path
823+
from vcs_versioning import PyProjectData
824+
825+
pyproject = PyProjectData(
826+
path=Path("pyproject.toml"),
827+
tool_name="vcs-versioning",
828+
project={"name": "my-pkg", "dynamic": ["version"]},
829+
section={"local_scheme": "no-local-version"},
830+
is_required=True,
831+
section_present=True,
832+
project_present=True,
833+
build_requires=["vcs-versioning"],
834+
)
835+
```
836+
837+
### Building Configuration with Overrides
838+
839+
The `build_configuration_from_pyproject()` function orchestrates the complete configuration workflow:
840+
841+
```python
842+
from vcs_versioning import build_configuration_from_pyproject
843+
844+
config = build_configuration_from_pyproject(
845+
pyproject_data=pyproject,
846+
dist_name="my-package", # Optional: override project.name
847+
# Integrator overrides (middle priority):
848+
version_scheme="release-branch-semver",
849+
local_scheme="no-local-version",
850+
)
851+
```
852+
853+
**What it does:**
854+
1. Extracts config from `pyproject_data.section`
855+
2. Determines `dist_name` (argument > config > project.name)
856+
3. Merges integrator overrides (kwargs)
857+
4. Reads and applies environment TOML overrides
858+
5. Builds and validates `Configuration` instance
859+
860+
### Environment TOML Overrides
861+
862+
Users can override configuration via environment variables:
863+
864+
```bash
865+
# Inline TOML format
866+
export MY_TOOL_OVERRIDES='{local_scheme = "no-local-version"}'
867+
868+
# Distribution-specific
869+
export MY_TOOL_OVERRIDES_FOR_MY_PACKAGE='{version_scheme = "guess-next-dev"}'
870+
```
871+
872+
These always have the highest priority, even over integrator overrides.
873+
874+
### Complete Example: Hatch Integration
875+
876+
```python
877+
# In your hatch plugin
878+
from pathlib import Path
879+
from vcs_versioning import (
880+
PyProjectData,
881+
build_configuration_from_pyproject,
882+
infer_version_string,
883+
)
884+
from vcs_versioning.overrides import GlobalOverrides
885+
886+
887+
class HatchVCSVersion:
888+
"""Hatch version source plugin using vcs-versioning."""
889+
890+
def get_version_data(self):
891+
"""Get version from VCS."""
892+
# Setup global context with HATCH_VCS prefix
893+
with GlobalOverrides.from_env("HATCH_VCS", dist_name=self.config["dist-name"]):
894+
895+
# Load pyproject data
896+
pyproject_path = Path(self.root) / "pyproject.toml"
897+
pyproject = PyProjectData.from_file(pyproject_path)
898+
899+
# Build configuration
900+
# Hatch-specific transformations can go here as kwargs
901+
config = build_configuration_from_pyproject(
902+
pyproject_data=pyproject,
903+
dist_name=self.config["dist-name"],
904+
root=self.root, # Hatch provides the root
905+
)
906+
907+
# Get version
908+
version = infer_version_string(
909+
dist_name=self.config["dist-name"],
910+
pyproject_data=pyproject,
911+
)
912+
913+
return {"version": version}
914+
```
915+
916+
### Tool Section Naming
917+
918+
**Important:** The public experimental API only accepts `tool.vcs-versioning` sections.
919+
920+
```toml
921+
# ✅ Correct - use tool.vcs-versioning
922+
[tool.vcs-versioning]
923+
version_scheme = "guess-next-dev"
924+
local_scheme = "no-local-version"
925+
926+
# ❌ Wrong - tool.setuptools_scm not supported in public API
927+
[tool.setuptools_scm]
928+
version_scheme = "guess-next-dev"
929+
```
930+
931+
Only `setuptools_scm` should use `tool.setuptools_scm` (for backward compatibility during transition).
932+
933+
### API Reference
934+
935+
#### `PyProjectData.from_file()`
936+
937+
```python
938+
@classmethod
939+
def from_file(
940+
cls,
941+
path: str | os.PathLike = "pyproject.toml",
942+
*,
943+
_tool_names: list[str] | None = None,
944+
) -> PyProjectData:
945+
"""Load PyProjectData from pyproject.toml.
946+
947+
Public API reads tool.vcs-versioning section.
948+
Internal: pass _tool_names for multi-tool support.
949+
"""
950+
```
951+
952+
#### `build_configuration_from_pyproject()`
953+
954+
```python
955+
def build_configuration_from_pyproject(
956+
pyproject_data: PyProjectData,
957+
*,
958+
dist_name: str | None = None,
959+
**integrator_overrides: Any,
960+
) -> Configuration:
961+
"""Build Configuration with full workflow orchestration.
962+
963+
Priority order:
964+
1. Environment TOML overrides (highest)
965+
2. Integrator **integrator_overrides
966+
3. pyproject_data.section configuration
967+
4. Configuration defaults (lowest)
968+
"""
969+
```
970+
971+
### Migration from Direct API Usage
972+
973+
If you were previously using internal APIs directly:
974+
975+
**Before:**
976+
```python
977+
from vcs_versioning._config import Configuration
978+
979+
config = Configuration.from_file("pyproject.toml", dist_name="my-pkg")
980+
```
981+
982+
**After (Experimental API):**
983+
```python
984+
from vcs_versioning import PyProjectData, build_configuration_from_pyproject
985+
from vcs_versioning.overrides import GlobalOverrides
986+
987+
with GlobalOverrides.from_env("MY_TOOL", dist_name="my-pkg"):
988+
pyproject = PyProjectData.from_file("pyproject.toml")
989+
config = build_configuration_from_pyproject(
990+
pyproject_data=pyproject,
991+
dist_name="my-pkg",
992+
)
993+
```
994+
995+
The experimental API provides better separation of concerns and proper override priority handling.
996+
740997
## See Also
741998

742999
- [Overrides Documentation](overrides.md) - User-facing documentation for setuptools-scm

setuptools-scm/src/setuptools_scm/_integration/pyproject_reading.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,19 @@ def read_pyproject(
117117
"""Read and parse pyproject configuration with setuptools-specific extensions.
118118
119119
This wraps vcs_versioning's read_pyproject and adds setuptools-specific behavior.
120+
Uses internal multi-tool support to read both setuptools_scm and vcs-versioning sections.
120121
"""
121-
# Use vcs_versioning's reader
122+
# Use vcs_versioning's reader with multi-tool support (internal API)
123+
# This allows setuptools_scm to transition to vcs-versioning section
122124
vcs_data = _vcs_read_pyproject(
123-
path, tool_name, canonical_build_package_name, _given_result, _given_definition
125+
path,
126+
canonical_build_package_name=canonical_build_package_name,
127+
_given_result=_given_result,
128+
_given_definition=_given_definition,
129+
tool_names=[
130+
"setuptools_scm",
131+
"vcs-versioning",
132+
], # Try both, setuptools_scm first
124133
)
125134

126135
# Check for conflicting tool.setuptools.dynamic configuration

vcs-versioning/src/vcs_versioning/__init__.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,109 @@
55

66
from __future__ import annotations
77

8+
from typing import TYPE_CHECKING, Any
9+
810
# Public API exports
911
from ._config import DEFAULT_LOCAL_SCHEME, DEFAULT_VERSION_SCHEME, Configuration
12+
from ._pyproject_reading import PyProjectData
1013
from ._version_cls import NonNormalizedVersion, Version
1114
from ._version_inference import infer_version_string
1215
from ._version_schemes import ScmVersion
1316

17+
if TYPE_CHECKING:
18+
pass
19+
20+
21+
def build_configuration_from_pyproject(
22+
pyproject_data: PyProjectData,
23+
*,
24+
dist_name: str | None = None,
25+
**integrator_overrides: Any,
26+
) -> Configuration:
27+
"""Build Configuration from PyProjectData with full workflow.
28+
29+
EXPERIMENTAL API for integrators.
30+
31+
This helper orchestrates the complete configuration building workflow:
32+
1. Extract config from pyproject_data.section
33+
2. Determine dist_name (argument > pyproject.project_name)
34+
3. Apply integrator overrides (override config file)
35+
4. Apply environment TOML overrides (highest priority)
36+
5. Create and validate Configuration instance
37+
38+
Integrators create PyProjectData themselves:
39+
40+
Example 1 - From file:
41+
>>> from vcs_versioning import PyProjectData, build_configuration_from_pyproject
42+
>>> from vcs_versioning.overrides import GlobalOverrides
43+
>>>
44+
>>> with GlobalOverrides.from_env("HATCH_VCS", dist_name="my-pkg"):
45+
... pyproject = PyProjectData.from_file("pyproject.toml")
46+
... config = build_configuration_from_pyproject(
47+
... pyproject_data=pyproject,
48+
... dist_name="my-pkg",
49+
... )
50+
51+
Example 2 - Manual composition:
52+
>>> from pathlib import Path
53+
>>> from vcs_versioning import PyProjectData, build_configuration_from_pyproject
54+
>>>
55+
>>> pyproject = PyProjectData(
56+
... path=Path("pyproject.toml"),
57+
... tool_name="vcs-versioning",
58+
... project={"name": "my-pkg"},
59+
... section={"local_scheme": "no-local-version"},
60+
... is_required=True,
61+
... section_present=True,
62+
... project_present=True,
63+
... build_requires=[],
64+
... )
65+
>>> config = build_configuration_from_pyproject(
66+
... pyproject_data=pyproject,
67+
... version_scheme="release-branch-semver", # Integrator override
68+
... )
69+
70+
Args:
71+
pyproject_data: Parsed pyproject data (integrator creates this)
72+
dist_name: Distribution name (overrides pyproject_data.project_name)
73+
**integrator_overrides: Integrator-provided config overrides
74+
(override config file, but overridden by env)
75+
76+
Returns:
77+
Configured Configuration instance ready for version inference
78+
79+
Priority order (highest to lowest):
80+
1. Environment TOML overrides (TOOL_OVERRIDES_FOR_DIST, TOOL_OVERRIDES)
81+
2. Integrator **overrides arguments
82+
3. pyproject_data.section configuration
83+
4. Configuration defaults
84+
85+
This allows integrators to provide their own transformations
86+
while still respecting user environment variable overrides.
87+
"""
88+
from ._integrator_helpers import build_configuration_from_pyproject_internal
89+
90+
return build_configuration_from_pyproject_internal(
91+
pyproject_data=pyproject_data,
92+
dist_name=dist_name,
93+
**integrator_overrides,
94+
)
95+
96+
1497
__all__ = [
1598
"DEFAULT_LOCAL_SCHEME",
1699
"DEFAULT_VERSION_SCHEME",
17100
"Configuration",
18101
"NonNormalizedVersion",
102+
"PyProjectData",
19103
"ScmVersion",
20104
"Version",
105+
"build_configuration_from_pyproject",
21106
"infer_version_string",
22107
]
108+
109+
# Experimental API markers for documentation
110+
__experimental__ = [
111+
"PyProjectData",
112+
"build_configuration_from_pyproject",
113+
]

0 commit comments

Comments
 (0)