Repository: pypa/setuptools_scm

Issue:
parse `SCMVersion` to dump_version
Currently, only `version`(str) and `version_tuple` are exposed in `dump_version`, therefore it is not possible to write additional informations such as `node`, `time`, etc. 

We propose to parse `version` to `dump_version` and rename the `version` to `version_str` to give more flexibility in `dump_version`. 

See also #577 

Discussion:
ZhiyuanChen: > Excellent work, thanks
> 
> Please confirm if i should squash or merge
> 
> I'll give it a last look over once I return to the computer

Thanks~ I believe it's better to squash the changes, as most commits are redundant"

Patch:
diff --git a/README.rst b/README.rst
index a48309c..f72d6ca 100644
--- a/README.rst
+++ b/README.rst
@@ -292,6 +292,15 @@ the tag.
 For Git projects, the version relies on `git describe <https://git-scm.com/docs/git-describe>`_,
 so you will see an additional ``g`` prepended to the ``{revision hash}``.
 
+.. note::
+
+    According to `PEP 440 <https://peps.python.org/pep-0440/#local-version-identifiers>`_,
+    if a version includes a local component, the package cannot be published to public
+    package indexes like PyPI or TestPyPI. The disallowed version segments may
+    be seen in auto-publishing workflows or when a configuration mistake is made.
+
+    However, some package indexes such as devpi or other alternatives allow local
+    versions. Local version identifiers must comply with `PEP 440`_.
 
 Semantic Versioning (SemVer)
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/src/setuptools_scm/__init__.py b/src/setuptools_scm/__init__.py
index 01b3da8..ecf08f8 100644
--- a/src/setuptools_scm/__init__.py
+++ b/src/setuptools_scm/__init__.py
@@ -24,6 +24,7 @@ from ._version_cls import _version_as_tuple
 from ._version_cls import NonNormalizedVersion
 from ._version_cls import Version
 from .version import format_version as _format_version
+from .version import ScmVersion
 
 if TYPE_CHECKING:
     from typing import NoReturn
@@ -46,6 +47,7 @@ def dump_version(
     version: str,
     write_to: _t.PathT,
     template: str | None = None,
+    scm_version: ScmVersion | None = None,
 ) -> None:
     assert isinstance(version, str)
     target = os.path.normpath(os.path.join(root, write_to))
@@ -62,8 +64,18 @@ def dump_version(
         )
     version_tuple = _version_as_tuple(version)
 
-    with open(target, "w") as fp:
-        fp.write(template.format(version=version, version_tuple=version_tuple))
+    if scm_version is not None:
+        with open(target, "w") as fp:
+            fp.write(
+                template.format(
+                    version=version,
+                    version_tuple=version_tuple,
+                    scm_version=scm_version,
+                )
+            )
+    else:
+        with open(target, "w") as fp:
+            fp.write(template.format(version=version, version_tuple=version_tuple))
 
 
 def _do_parse(config: Configuration) -> _t.SCMVERSION | None:
@@ -164,6 +176,7 @@ def _get_version(config: Configuration) -> str | None:
         dump_version(
             root=config.root,
             version=version_string,
+            scm_version=parsed_version,
             write_to=config.write_to,
             template=config.write_to_template,
         )


Test patch:
diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml
index 373be21..1c1ab00 100644
--- a/.github/workflows/python-tests.yml
+++ b/.github/workflows/python-tests.yml
@@ -97,6 +97,8 @@ jobs:
 
     runs-on: ubuntu-latest
     if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')
+    permissions:
+      id-token: write
     needs: [test]
     steps:
     - uses: actions/download-artifact@v3
@@ -105,5 +107,19 @@ jobs:
         path: dist
     - name: Publish package to PyPI
       uses: pypa/gh-action-pypi-publish@release/v1
+
+  test-pypi-upload:
+    runs-on: ubuntu-latest
+    needs: [test]
+    permissions:
+      id-token: write
+    steps:
+    - uses: actions/download-artifact@v3
+      with:
+        name: Packages
+        path: dist
+    - name: Publish package to PyPI
+      continue-on-error: true
+      uses: pypa/gh-action-pypi-publish@release/v1
       with:
-        password: ${{ secrets.pypi_token }}
+        repository-url: https://test.pypi.org/legacy/
diff --git a/testing/test_basic_api.py b/testing/test_basic_api.py
index 6fd39bd..02084ae 100644
--- a/testing/test_basic_api.py
+++ b/testing/test_basic_api.py
@@ -2,6 +2,7 @@ from __future__ import annotations
 
 import os
 import sys
+from datetime import date
 from pathlib import Path
 
 import pytest
@@ -11,10 +12,20 @@ from setuptools_scm import Configuration
 from setuptools_scm import dump_version
 from setuptools_scm._run_cmd import run
 from setuptools_scm.integration import data_from_mime
+from setuptools_scm.version import meta
 from setuptools_scm.version import ScmVersion
 from testing.wd_wrapper import WorkDir
 
 
+c = Configuration()
+
+template = """\
+__version__ = version = {version!r}
+__version_tuple__ = version_tuple = {version_tuple!r}
+__sha__ = {scm_version.node!r}
+"""
+
+
 def test_run_plain(tmp_path: Path) -> None:
     run([sys.executable, "-c", "print(1)"], cwd=tmp_path)
 
@@ -155,30 +166,46 @@ def test_root_relative_to(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> No
 
 
 def test_dump_version(tmp_path: Path) -> None:
-    dump_version(tmp_path, "1.0", "first.txt")
+    version = "1.0"
+    scm_version = meta(version, config=c)
+    dump_version(tmp_path, version, "first.txt", scm_version=scm_version)
 
     def read(name: str) -> str:
         return tmp_path.joinpath(name).read_text()
 
     assert read("first.txt") == "1.0"
 
-    dump_version(tmp_path, "1.0.dev42", "first.py")
+    version = "1.0.dev42"
+    scm_version = meta("1.0", distance=42, config=c)
+    dump_version(tmp_path, version, "first.py", scm_version=scm_version)
     lines = read("first.py").splitlines()
     assert "__version__ = version = '1.0.dev42'" in lines
     assert "__version_tuple__ = version_tuple = (1, 0, 'dev42')" in lines
 
-    dump_version(tmp_path, "1.0.1+g4ac9d2c", "second.py")
+    version = "1.0.1+g4ac9d2c"
+    scm_version = meta("1.0.1", node="g4ac9d2c", config=c)
+    dump_version(
+        tmp_path, version, "second.py", scm_version=scm_version, template=template
+    )
     lines = read("second.py").splitlines()
     assert "__version__ = version = '1.0.1+g4ac9d2c'" in lines
     assert "__version_tuple__ = version_tuple = (1, 0, 1, 'g4ac9d2c')" in lines
+    assert "__sha__ = 'g4ac9d2c'" in lines
 
-    dump_version(tmp_path, "1.2.3.dev18+gb366d8b.d20210415", "third.py")
+    version = "1.2.3.dev18+gb366d8b.d20210415"
+    scm_version = meta(
+        "1.2.3", node="gb366d8b", distance=18, node_date=date(2021, 4, 15), config=c
+    )
+    dump_version(
+        tmp_path, version, "third.py", scm_version=scm_version, template=template
+    )
     lines = read("third.py").splitlines()
     assert "__version__ = version = '1.2.3.dev18+gb366d8b.d20210415'" in lines
     assert (
         "__version_tuple__ = version_tuple = (1, 2, 3, 'dev18', 'gb366d8b.d20210415')"
         in lines
     )
+    assert "__sha__ = 'gb366d8b'" in lines
 
     import ast
 
diff --git a/testing/test_functions.py b/testing/test_functions.py
index 6b0bd39..3c8486d 100644
--- a/testing/test_functions.py
+++ b/testing/test_functions.py
@@ -74,8 +74,9 @@ def test_format_version(
 def test_dump_version_doesnt_bail_on_value_error(tmp_path: Path) -> None:
     write_to = "VERSION"
     version = str(VERSIONS["exact"].tag)
+    scm_version = meta(VERSIONS["exact"].tag, config=c)
     with pytest.raises(ValueError, match="^bad file format:"):
-        dump_version(tmp_path, version, write_to)
+        dump_version(tmp_path, version, write_to, scm_version=scm_version)
 
 
 @pytest.mark.parametrize(