Handle local version specifiers in tags
Hi @jwodder ,
I've been playing around with versioningit and hatchling to maybe adapt to it and I am facing an issue with local version specs in the format process. Let's take a tag like poc-2.6.0+dt, where we explicitly track a local version spec due to some internal modifications of a public package.
However, with the initial config below I try to fetch the version incl. the local-version-spec from git to hand it over to versioningit. See the first 2 steps (vcs, tag2version).
Now, obviously on a dirty tag (and the two other states will have the same outcome) the result is not pep440 compliant due to the double + while using the formatting in the "format" step.
-
When I exclude the local version spec from the version-capture-group in the regex pattern (tag2version) and instead add the local version
+dtto the three format states, so that the versions stay pep440 compliant, I get a proper result for dirty/distance/distance-dirty versions containing the local version spec.- When I call
versioningit -nI will only get the basenext_versionwithout the +dt suffix, which makes sence in terms of templating/formating. But not nice though. - When I'm on a clean tag ref, then the current version also im missing the +dt as I excluded it from the version-capture-group. Also not nice for building releases.
- When I call
-
Now, when I instead keep the initial version-capture-group to include the local-version-spec, I can somehow overcome the initial issue in the format step with this config (see the trailing comments):
[tool.hatch.version.format] method = "basic" # Distance Example: 1.2.4.dev42+dt.ge174a1f distance = "{next_version}.dev{distance}+dt.{vcs}{rev}" # +dt is hard-coded now as part of the local version # Dirty Example: 1.2.3+dt.d20230922 dirty = "{base_version}.d{build_date:%Y%m%d}" # base_version contains +dt due to the version-capture-group # Distance-Dirty Example: 1.2.4.dev42+dt.ge174a1f.d20230922 distance-dirty = "{next_version}.dev{distance}+dt.{vcs}{rev}.d{build_date:%Y%m%d}" # +dt is hard-coded now as part of the local versionThis leads to correct dirty/distance/distance-dirty versions as also to correct versions on a clean tag ref. However, the next_version never contains the +dt, which is still not perfect. And I have to hard-code the local-version-spec in the config, which is not good if the repo would use different types of local-version-specs.
As you can see in 2. the format config is more like a workaround with limits than a good intuitive handling. I would like you to consider to implement a better handling for the local version spec as it is part of PEP440 (canonical compliance is out of scope here).
Suggestions:
When the user decides to capture the local version spec in tag2version, then:
- extract it internally from the captured version tag and save it for later use
- maybe remove it also from the base_version for the ~whole processing~ processing in "next_version" (already done?) and "format" steps, so that the 3 states in "format" become more consistent (the user has to hard-code the local version manually). Or even better: provide a template-variable like {local_version} containing the initially captured "+abcd123.abcd123" string for the "format" step.
- finally append the previously extracted/saved local version spec back to a CLEAN current version or CLEAN next version (as these cannot be templated by the user) BEFORE moving on to "template-fields", "write" or "onbuild", so that the splitting, writing or the replacement in onbuild will be consistent.
- This approach would also fit the "template_fields" step as then:
- base_version would not contain the local version spec
- there would be a new variable local_version covering this
- the final version and version_tuple would be complete as mentioned in 3.
I hope you can pick this request up and implement it soon and that I did not make any mistakes in my suggestion. :D Send me a reply if you have further questions. Cheers. :D
Initial Config:
[tool.hatch.version]
source = "versioningit"
default-version = "0.0.0+unknown"
[tool.hatch.version.vcs]
method = "git"
match = ["*[0-9].[0-9].[0-9]*"]
default-tag = "0.0.0+unknown"
[tool.hatch.version.tag2version]
method = "basic"
# [ prefix ](?P<version>[ canonical version identifier ][ local version identifier ])[ git-describe suffix ]
regex = "^([\\w-]+-)?[vV]?(?P<version>([1-9][0-9]*!)?(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\\.post(0|[1-9][0-9]*))?(\\.dev(0|[1-9][0-9]*))?(\\+[a-z0-9]+(\\.[a-z0-9]+)*)?)(\\-[0-9]+\\-g[a-f0-9]+)?$"
require-match = true
[tool.hatch.version.next-version]
method = "smallest"
[tool.hatch.version.format]
method = "basic"
# Example: 1.2.4.dev42+ge174a1f
distance = "{next_version}.dev{distance}+{vcs}{rev}"
# Example: 1.2.3+d20230922
dirty = "{base_version}+d{build_date:%Y%m%d}"
# Example: 1.2.4.dev42+ge174a1f.d20230922
distance-dirty = "{next_version}.dev{distance}+{vcs}{rev}.d{build_date:%Y%m%d}"
[tool.hatch.version.template-fields]
method = "basic"
version-tuple = {pep440 = true, double-quote = true}
Initial Result:
╰─▶ Call to `hatchling.build.build_editable` failed (exit code: 1)
[stderr]
[INFO ] versioningit: Project dir: C:\Users\A761188\Sandbox\private\pdm-vs-uv\poc
[DEBUG ] versioningit: Loading entry point 'git' in group versioningit.vcs
[DEBUG ] versioningit: Loading entry point 'basic' in group versioningit.tag2version
[DEBUG ] versioningit: Loading entry point 'smallest' in group versioningit.next_version
[DEBUG ] versioningit: Loading entry point 'basic' in group versioningit.format
[DEBUG ] versioningit: Loading entry point 'basic' in group versioningit.template_fields
[DEBUG ] versioningit: Loading entry point 'basic' in group versioningit.write
[DEBUG ] versioningit: Running: git rev-parse --is-inside-work-tree
[DEBUG ] versioningit: Running: git ls-files --error-unmatch .
[DEBUG ] versioningit: Running: git describe --long --dirty --always --tags '--match=*[0-9].[0-9].[0-9]*'
[DEBUG ] versioningit: Running: git symbolic-ref --short -q HEAD
[DEBUG ] versioningit: Running: git --no-pager show -s --format=%H%n%at%n%ct
[INFO ] versioningit: vcs returned tag poc-2.6.0+dt
[DEBUG ] versioningit: vcs state: dirty
[DEBUG ] versioningit: vcs branch: main
[DEBUG ] versioningit: vcs fields: {'distance': 0, 'rev': '9c87160', 'build_date': datetime.datetime(2025, 6, 26, 23, 56, 28, 91666, tzinfo=datetime.timezone.utc), 'vcs':
'g', 'vcs_name': 'git', 'revision': '9c87160ae9b3cc38dab86f67708173ca3b05919d', 'author_date': datetime.datetime(2025, 6, 26, 20, 21, 26, tzinfo=datetime.timezone.utc),
'committer_date': datetime.datetime(2025, 6, 26, 20, 21, 26, tzinfo=datetime.timezone.utc)}
[INFO ] versioningit: tag2version returned version 2.6.0+dt
[INFO ] versioningit: next-version returned version 2.6.1
[INFO ] versioningit: VCS state is 'dirty'; formatting version
[INFO ] versioningit: Final version: 2.6.0+dt+d20250626
[WARNING ] versioningit: Final version '2.6.0+dt+d20250626' is not PEP 440-compliant
Traceback (most recent call last):
File "<string>", line 11, in <module>
File "C:\Users\A761188\AppData\Local\uv\cache\builds-v0\.tmpjSh1sz\Lib\site-packages\hatchling\build.py", line 83, in build_editable
return os.path.basename(next(builder.build(directory=wheel_directory, versions=['editable'])))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\A761188\AppData\Local\uv\cache\builds-v0\.tmpjSh1sz\Lib\site-packages\hatchling\builders\plugin\interface.py", line 90, in build
self.metadata.validate_fields()
File "C:\Users\A761188\AppData\Local\uv\cache\builds-v0\.tmpjSh1sz\Lib\site-packages\hatchling\metadata\core.py", line 265, in validate_fields
_ = self.version
^^^^^^^^^^^^
File "C:\Users\A761188\AppData\Local\uv\cache\builds-v0\.tmpjSh1sz\Lib\site-packages\hatchling\metadata\core.py", line 149, in version
self._version = self._get_version()
^^^^^^^^^^^^^^^^^^^
File "C:\Users\A761188\AppData\Local\uv\cache\builds-v0\.tmpjSh1sz\Lib\site-packages\hatchling\metadata\core.py", line 248, in _get_version
version = self.hatch.version.cached
^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\A761188\AppData\Local\uv\cache\builds-v0\.tmpjSh1sz\Lib\site-packages\hatchling\metadata\core.py", line 1456, in cached
raise type(e)(message) from None
versioningit.errors.InvalidVersionError: Error getting the version from source `versioningit`: '2.6.0+dt+d20250626' is not a valid PEP 440 version
Unfortunately, this isn't really doable with versioningit's current architecture. Your suggested behavior involves passing extra state from one step to the next, which would require changing the arguments accepted by method functions, and since versioningit supports plugins that provide their own method functions, this would be a breaking change. I'm mulling over ways to rearchitecture the method implementations in #66 so that I'll only need to make one last breaking change to the method signatures, so if you have any suggestions on that front, I'm open to them.
Oh dear, that sounds much more complicated than expected... :/ I gonna take a look at #66, but I am not sure whether I will be able to help somehow... :(