Skip to content

Handle local version specifiers in tags #116

@martinpakosch

Description

@martinpakosch

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.

  1. When I exclude the local version spec from the version-capture-group in the regex pattern (tag2version) and instead add the local version +dt to 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 -n I will only get the base next_version without 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.
  2. 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 version

    This 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:

  1. extract it internally from the captured version tag and save it for later use
  2. 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.
  3. 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.
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    blockedCannot be implemented until something else happensenhancementNew feature or request thereforunder considerationDev has not yet decided whether or how to implement

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions