Repository: pypa/setuptools_scm

Issue:
write_to should also obey relative_to
Say you have e.g.
```
repo/.git
repo/java/whatever
repo/python
repo/python/setup.py
repo/python/src/yourmagicpackage
```
ie. a typical case where you do need to specify root and relative_to.
In this case it seems preferable for write_to to also obey relative_to, i.e. you (I)'d like to write `relative_to=__file__, root="..", write_to="src/yourmagicpackage/_version.py"` rather than `..., write_to="python/src/yourmagicpackage/_version.py"`.

Obviously this would be a backcompat break, but I think the proposed behavior is more sensible?

Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 9007b8c..221ffd2 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -16,6 +16,7 @@ breaking
 * move setuptools integration code to private sub-package
 * use normalized dist names for the SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${DIST_NAME}
 * drop support for python 3.7
+* introduce version_file as replacement for write_to
 
 features
 --------
diff --git a/README.rst b/README.rst
index f72d6ca..3a17d5a 100644
--- a/README.rst
+++ b/README.rst
@@ -76,7 +76,7 @@ to be supplied to ``get_version()``. For example:
 
     # pyproject.toml
     [tool.setuptools_scm]
-    write_to = "pkg/_version.py"
+    version_file = "pkg/_version.py"
 
 Where ``pkg`` is the name of your package.
 
@@ -93,72 +93,7 @@ directly in your working environment and run:
     $ python -m setuptools_scm --help
 
 
-``setup.py`` usage (deprecated)
--------------------------------
 
-.. warning::
-
-   ``setup_requires`` has been deprecated in favor of ``pyproject.toml``
-
-The following settings are considered legacy behavior and
-superseded by the ``pyproject.toml`` usage, but for maximal
-compatibility, projects may also supply the configuration in
-this older form.
-
-To use ``setuptools_scm`` just modify your project's ``setup.py`` file
-like this:
-
-* Add ``setuptools_scm`` to the ``setup_requires`` parameter.
-* Add the ``use_scm_version`` parameter and set it to ``True``.
-
-For example:
-
-.. code:: python
-
-    from setuptools import setup
-    setup(
-        ...,
-        use_scm_version=True,
-        setup_requires=['setuptools_scm'],
-        ...,
-    )
-
-Arguments to ``get_version()`` (see below) may be passed as a dictionary to
-``use_scm_version``. For example:
-
-.. code:: python
-
-    from setuptools import setup
-    setup(
-        ...,
-        use_scm_version = {
-            "root": "..",
-            "relative_to": __file__,
-            "local_scheme": "node-and-timestamp"
-        },
-        setup_requires=['setuptools_scm'],
-        ...,
-    )
-
-You can confirm the version number locally via ``setup.py``:
-
-.. code-block:: shell
-
-    $ python setup.py --version
-
-.. note::
-
-   If you see unusual version numbers for packages but ``python setup.py
-   --version`` reports the expected version number, ensure ``[egg_info]`` is
-   not defined in ``setup.cfg``.
-
-
-``setup.cfg`` usage (deprecated)
-------------------------------------
-
-as ``setup_requires`` is deprecated in favour of ``pyproject.toml``
-usage in ``setup.cfg`` is considered deprecated,
-please use ``pyproject.toml`` whenever possible.
 
 
 Programmatic usage
@@ -172,8 +107,6 @@ than the project's root, you can use:
     from setuptools_scm import get_version
     version = get_version(root='..', relative_to=__file__)
 
-See `setup.py Usage (deprecated)`_ above for how to use this within ``setup.py``.
-
 
 Retrieving package version at runtime
 -------------------------------------
@@ -193,21 +126,6 @@ or the `importlib_metadata`_ backport:
         # package is not installed
         pass
 
-Alternatively, you can use ``pkg_resources`` which is included in
-``setuptools`` (but has a significant runtime cost):
-
-.. code:: python
-
-   from pkg_resources import get_distribution, DistributionNotFound
-
-   try:
-       __version__ = get_distribution("package-name").version
-   except DistributionNotFound:
-        # package is not installed
-       pass
-
-However, this does place a runtime dependency on ``setuptools`` and can add up to
-a few 100ms overhead for the package import time.
 
 .. _PEP-0566: https://www.python.org/dev/peps/pep-0566/
 .. _importlib_metadata: https://pypi.org/project/importlib-metadata/
@@ -392,7 +310,7 @@ The currently supported configuration keys are:
     Configures how the local component of the version is constructed; either an
     entrypoint name or a callable.
 
-:write_to:
+:version_file:
     A path to a file that gets replaced with a file containing the current
     version. It is ideal for creating a ``_version.py`` file within the
     package, typically used to avoid using `pkg_resources.get_distribution`
@@ -404,10 +322,14 @@ The currently supported configuration keys are:
       templates, for other file types it is necessary to provide
       :code:`write_to_template`.
 
-:write_to_template:
+:version_file_template_template:
     A newstyle format string that is given the current version as
     the ``version`` keyword argument for formatting.
 
+:write_to:
+   (deprecated) legacy option to create a version file relative to the scm root
+   its broken for usage from a sdist and fixing it would be a fatal breaking change,
+   use ``version_file`` instead
 :relative_to:
     A file from which the root can be resolved.
     Typically called by a script or module that is not in the root of the
@@ -488,20 +410,6 @@ function:
 It optionally accepts the keys of the ``use_scm_version`` parameter as
 keyword arguments.
 
-Example configuration in ``setup.py`` format:
-
-.. code:: python
-
-    from setuptools import setup
-
-    setup(
-        use_scm_version={
-            'write_to': '_version.py',
-            'write_to_template': '__version__ = "{version}"',
-            'tag_regex': r'^(?P<prefix>v)?(?P<version>[^\+]+)(?P<suffix>.*)?$',
-        }
-    )
-
 
 Environment variables
 ---------------------
@@ -509,7 +417,13 @@ Environment variables
 :SETUPTOOLS_SCM_PRETEND_VERSION:
     when defined and not empty,
     its used as the primary source for the version number
-    in which case it will be an unparsed string
+    in which case it will be an string
+
+    .. warning::
+
+      its strongly  recommended to use use distribution name specific pretend versions
+
+
 
 :SETUPTOOLS_SCM_PRETEND_VERSION_FOR_${NORMALIZED_DIST_NAME}:
     when defined and not empty,
diff --git a/src/setuptools_scm/__init__.py b/src/setuptools_scm/__init__.py
index ecf08f8..f7c3e50 100644
--- a/src/setuptools_scm/__init__.py
+++ b/src/setuptools_scm/__init__.py
@@ -1,46 +1,19 @@
 """
-:copyright: 2010-2015 by Ronny Pfannschmidt
+:copyright: 2010-2023 by Ronny Pfannschmidt
 :license: MIT
 """
 from __future__ import annotations
 
-import os
-import re
-import warnings
 from typing import Any
 from typing import Pattern
-from typing import TYPE_CHECKING
 
+from . import _config
+from . import _types as _t
 from ._config import Configuration
-from ._config import DEFAULT_LOCAL_SCHEME
-from ._config import DEFAULT_TAG_REGEX
-from ._config import DEFAULT_VERSION_SCHEME
-from ._entrypoints import _version_from_entrypoints
-from ._overrides import _read_pretended_version_for
-from ._overrides import PRETEND_KEY
-from ._overrides import PRETEND_KEY_NAMED
-from ._version_cls import _validate_version_cls
-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
-    from . import _types as _t
-
-
-TEMPLATES = {
-    ".py": """\
-# file generated by setuptools_scm
-# don't change, don't track in version control
-__version__ = version = {version!r}
-__version_tuple__ = version_tuple = {version_tuple!r}
-""",
-    ".txt": "{version}",
-}
-
 
 def dump_version(
     root: _t.PathT,
@@ -49,84 +22,25 @@ def dump_version(
     template: str | None = None,
     scm_version: ScmVersion | None = None,
 ) -> None:
-    assert isinstance(version, str)
-    target = os.path.normpath(os.path.join(root, write_to))
-    ext = os.path.splitext(target)[1]
-    template = template or TEMPLATES.get(ext)
-    from ._log import log
-
-    log.debug("dump %s into %s", version, write_to)
-    if template is None:
-        raise ValueError(
-            "bad file format: '{}' (of {}) \nonly *.txt and *.py are supported".format(
-                os.path.splitext(target)[1], target
-            )
-        )
-    version_tuple = _version_as_tuple(version)
-
-    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:
-    from .version import ScmVersion
-
-    pretended = _read_pretended_version_for(config)
-    if pretended is not None:
-        return pretended
-    parsed_version: ScmVersion | None
-    if config.parse:
-        parse_result = config.parse(config.absolute_root, config=config)
-        if isinstance(parse_result, str):
-            raise TypeError(
-                f"version parse result was {str!r}\nplease return a parsed version"
-            )
-
-        if parse_result:
-            assert isinstance(parse_result, ScmVersion)
-            parsed_version = parse_result
-        else:
-            parsed_version = _version_from_entrypoints(config, fallback=True)
-    else:
-        # include fallbacks after dropping them from the main entrypoint
-        parsed_version = _version_from_entrypoints(config) or _version_from_entrypoints(
-            config, fallback=True
-        )
-
-    return parsed_version
+    """soft deprecated helper to write the version file in the cwd
 
+    still used by hatch-vcs, will be removed after upstream uses the modern patterns
+    """
+    from ._integration.dump_version import dump_version as real
 
-def _version_missing(config: Configuration) -> NoReturn:
-    raise LookupError(
-        f"setuptools-scm was unable to detect version for {config.absolute_root}.\n\n"
-        "Make sure you're either building from a fully intact git repository "
-        "or PyPI tarballs. Most other sources (such as GitHub's tarballs, a "
-        "git checkout without the .git folder) don't contain the necessary "
-        "metadata and will not work.\n\n"
-        "For example, if you're using pip, instead of "
-        "https://github.com/user/proj/archive/master.zip "
-        "use git+https://github.com/user/proj.git#egg=proj"
-    )
+    return real(root, version, write_to, template, scm_version)
 
 
 def get_version(
     root: _t.PathT = ".",
-    version_scheme: _t.VERSION_SCHEME = DEFAULT_VERSION_SCHEME,
-    local_scheme: _t.VERSION_SCHEME = DEFAULT_LOCAL_SCHEME,
+    version_scheme: _t.VERSION_SCHEME = _config.DEFAULT_VERSION_SCHEME,
+    local_scheme: _t.VERSION_SCHEME = _config.DEFAULT_LOCAL_SCHEME,
     write_to: _t.PathT | None = None,
     write_to_template: str | None = None,
-    relative_to: str | None = None,
-    tag_regex: str | Pattern[str] = DEFAULT_TAG_REGEX,
+    version_file: _t.PathT | None = None,
+    version_file_template: str | None = None,
+    relative_to: _t.PathT | None = None,
+    tag_regex: str | Pattern[str] = _config.DEFAULT_TAG_REGEX,
     parentdir_prefix_version: str | None = None,
     fallback_version: str | None = None,
     fallback_root: _t.PathT = ".",
@@ -138,50 +52,16 @@ def get_version(
     search_parent_directories: bool = False,
 ) -> str:
     """
-    If supplied, relative_to should be a file from which root may
-    be resolved. Typically called by a script or module that is not
-    in the root of the repository to direct setuptools_scm to the
-    root of the repository by supplying ``__file__``.
-    """
-    version_cls = _validate_version_cls(version_cls, normalize)
-    del normalize
-    if isinstance(tag_regex, str):
-        if tag_regex == "":
-            warnings.warn(
-                DeprecationWarning(
-                    "empty regex for tag regex is invalid, using default"
-                )
-            )
-            tag_regex = DEFAULT_TAG_REGEX
-        else:
-            tag_regex = re.compile(tag_regex)
-    config = Configuration(**locals())
-    maybe_version = _get_version(config)
-
-    if maybe_version is None:
-        _version_missing(config)
-    return maybe_version
+    soft deprecated helper to obtain the vcs version
+    its misused at runtime in numerous projects and has to stay for now
 
+    a replacement supporting editable installation in hatch/setuptools is planned
+    """
 
-def _get_version(config: Configuration) -> str | None:
-    parsed_version = _do_parse(config)
-    if parsed_version is None:
-        return None
-    version_string = _format_version(
-        parsed_version,
-        version_scheme=config.version_scheme,
-        local_scheme=config.local_scheme,
-    )
-    if config.write_to is not None:
-        dump_version(
-            root=config.root,
-            version=version_string,
-            scm_version=parsed_version,
-            write_to=config.write_to,
-            template=config.write_to_template,
-        )
+    params = {**locals()}
+    from ._get_version import get_version
 
-    return version_string
+    return get_version(**params)
 
 
 # Public API
@@ -189,11 +69,6 @@ __all__ = [
     "get_version",
     "dump_version",
     "Configuration",
-    "DEFAULT_VERSION_SCHEME",
-    "DEFAULT_LOCAL_SCHEME",
-    "DEFAULT_TAG_REGEX",
-    "PRETEND_KEY",
-    "PRETEND_KEY_NAMED",
     "Version",
     "NonNormalizedVersion",
 ]
diff --git a/src/setuptools_scm/_cli.py b/src/setuptools_scm/_cli.py
index 37bbc9a..4786ec2 100644
--- a/src/setuptools_scm/_cli.py
+++ b/src/setuptools_scm/_cli.py
@@ -4,9 +4,9 @@ import argparse
 import os
 import sys
 
-from setuptools_scm import _get_version
 from setuptools_scm import Configuration
 from setuptools_scm._file_finders import find_files
+from setuptools_scm._get_version import _get_version
 from setuptools_scm.discover import walk_potential_roots
 
 
diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py
index 108c86e..c966975 100644
--- a/src/setuptools_scm/_config.py
+++ b/src/setuptools_scm/_config.py
@@ -8,6 +8,7 @@ import warnings
 from typing import Any
 from typing import Callable
 from typing import Pattern
+from typing import Protocol
 
 from . import _log
 from . import _types as _t
@@ -45,6 +46,13 @@ def _check_tag_regex(value: str | Pattern[str] | None) -> Pattern[str]:
     return regex
 
 
+class ParseFunction(Protocol):
+    def __call__(
+        self, root: _t.PathT, *, config: Configuration
+    ) -> _t.SCMVERSION | None:
+        ...
+
+
 def _check_absolute_root(root: _t.PathT, relative_to: _t.PathT | None) -> str:
     log.debug("check absolute root=%s relative_to=%s", root, relative_to)
     if relative_to:
@@ -85,7 +93,9 @@ class Configuration:
     fallback_root: _t.PathT = "."
     write_to: _t.PathT | None = None
     write_to_template: str | None = None
-    parse: Any | None = None
+    version_file: _t.PathT | None = None
+    version_file_template: str | None = None
+    parse: ParseFunction | None = None
     git_describe_command: _t.CMD_TYPE | None = None
     dist_name: str | None = None
     version_cls: type[_VersionT] = _Version
diff --git a/src/setuptools_scm/_get_version.py b/src/setuptools_scm/_get_version.py
new file mode 100644
index 0000000..a1afe2d
--- /dev/null
+++ b/src/setuptools_scm/_get_version.py
@@ -0,0 +1,154 @@
+from __future__ import annotations
+
+import re
+import warnings
+from pathlib import Path
+from typing import Any
+from typing import NoReturn
+from typing import Pattern
+
+from . import _config
+from . import _types as _t
+from ._config import Configuration
+from ._entrypoints import _version_from_entrypoints
+from ._overrides import _read_pretended_version_for
+from ._version_cls import _validate_version_cls
+from .version import format_version as _format_version
+from .version import ScmVersion
+
+
+def parse_scm_version(config: Configuration) -> ScmVersion | None:
+    if config.parse is not None:
+        parse_result = config.parse(config.absolute_root, config=config)
+        if parse_result is not None and not isinstance(parse_result, ScmVersion):
+            raise TypeError(
+                f"version parse result was {str!r}\n"
+                "please return a parsed version (ScmVersion)"
+            )
+        return parse_result
+    else:
+        return _version_from_entrypoints(config)
+
+
+def parse_fallback_version(config: Configuration) -> ScmVersion | None:
+    return _version_from_entrypoints(config, fallback=True)
+
+
+def _do_parse(config: Configuration) -> ScmVersion | None:
+    return (
+        _read_pretended_version_for(config)
+        or parse_scm_version(config)
+        or parse_fallback_version(config)
+    )
+
+
+def write_version_files(
+    config: Configuration, version: str, scm_version: ScmVersion
+) -> None:
+    if config.write_to is not None:
+        from ._integration.dump_version import dump_version
+
+        dump_version(
+            root=config.root,
+            version=version,
+            scm_version=scm_version,
+            write_to=config.write_to,
+            template=config.write_to_template,
+        )
+    if config.version_file:
+        from ._integration.dump_version import write_version_to_path
+
+        version_file = Path(config.version_file)
+        assert not version_file.is_absolute(), f"{version_file=}"
+        # todo: use a better name than fallback root
+        assert config.relative_to is not None
+        target = Path(config.relative_to).parent.joinpath(version_file)
+        write_version_to_path(
+            target,
+            template=config.version_file_template,
+            version=version,
+            scm_version=scm_version,
+        )
+
+
+def _get_version(
+    config: Configuration, force_write_version_files: bool = False
+) -> str | None:
+    parsed_version = _do_parse(config)
+    if parsed_version is None:
+        return None
+    version_string = _format_version(
+        parsed_version,
+        version_scheme=config.version_scheme,
+        local_scheme=config.local_scheme,
+    )
+    if force_write_version_files:
+        write_version_files(config, version=version_string, scm_version=parsed_version)
+
+    return version_string
+
+
+def _version_missing(config: Configuration) -> NoReturn:
+    raise LookupError(
+        f"setuptools-scm was unable to detect version for {config.absolute_root}.\n\n"
+        "Make sure you're either building from a fully intact git repository "
+        "or PyPI tarballs. Most other sources (such as GitHub's tarballs, a "
+        "git checkout without the .git folder) don't contain the necessary "
+        "metadata and will not work.\n\n"
+        "For example, if you're using pip, instead of "
+        "https://github.com/user/proj/archive/master.zip "
+        "use git+https://github.com/user/proj.git#egg=proj"
+    )
+
+
+def get_version(
+    root: _t.PathT = ".",
+    version_scheme: _t.VERSION_SCHEME = _config.DEFAULT_VERSION_SCHEME,
+    local_scheme: _t.VERSION_SCHEME = _config.DEFAULT_LOCAL_SCHEME,
+    write_to: _t.PathT | None = None,
+    write_to_template: str | None = None,
+    version_file: _t.PathT | None = None,
+    version_file_template: str | None = None,
+    relative_to: _t.PathT | None = None,
+    tag_regex: str | Pattern[str] = _config.DEFAULT_TAG_REGEX,
+    parentdir_prefix_version: str | None = None,
+    fallback_version: str | None = None,
+    fallback_root: _t.PathT = ".",
+    parse: Any | None = None,
+    git_describe_command: _t.CMD_TYPE | None = None,
+    dist_name: str | None = None,
+    version_cls: Any | None = None,
+    normalize: bool = True,
+    search_parent_directories: bool = False,
+) -> str:
+    """
+    If supplied, relative_to should be a file from which root may
+    be resolved. Typically called by a script or module that is not
+    in the root of the repository to direct setuptools_scm to the
+    root of the repository by supplying ``__file__``.
+    """
+
+    version_cls = _validate_version_cls(version_cls, normalize)
+    del normalize
+    tag_regex = parse_tag_regex(tag_regex)
+    config = Configuration(**locals())
+    maybe_version = _get_version(config, force_write_version_files=True)
+
+    if maybe_version is None:
+        _version_missing(config)
+    return maybe_version
+
+
+def parse_tag_regex(tag_regex: str | Pattern[str]) -> Pattern[str]:
+    if isinstance(tag_regex, str):
+        if tag_regex == "":
+            warnings.warn(
+                DeprecationWarning(
+                    "empty regex for tag regex is invalid, using default"
+                )
+            )
+            return _config.DEFAULT_TAG_REGEX
+        else:
+            return re.compile(tag_regex)
+    else:
+        return tag_regex
diff --git a/src/setuptools_scm/_integration/dump_version.py b/src/setuptools_scm/_integration/dump_version.py
new file mode 100644
index 0000000..d40c6bb
--- /dev/null
+++ b/src/setuptools_scm/_integration/dump_version.py
@@ -0,0 +1,73 @@
+from __future__ import annotations
+
+import warnings
+from pathlib import Path
+
+from .. import _types as _t
+from .._log import log as parent_log
+from .._version_cls import _version_as_tuple
+from ..version import ScmVersion
+
+
+log = parent_log.getChild("dump_version")
+
+TEMPLATES = {
+    ".py": """\
+# file generated by setuptools_scm
+# don't change, don't track in version control
+__version__ = version = {version!r}
+__version_tuple__ = version_tuple = {version_tuple!r}
+""",
+    ".txt": "{version}",
+}
+
+
+def dump_version(
+    root: _t.PathT,
+    version: str,
+    write_to: _t.PathT,
+    template: str | None = None,
+    scm_version: ScmVersion | None = None,
+) -> None:
+    assert isinstance(version, str)
+    # todo: assert write_to doesnt escape
+    write_to = Path(write_to)
+    assert not write_to.is_absolute(), f"{write_to=}"
+    target = Path(root).joinpath(write_to)
+    write_version_to_path(
+        target, template=template, version=version, scm_version=scm_version
+    )
+
+
+def _validate_template(target: Path, template: str | None) -> str:
+    if template == "":
+        warnings.warn(f"{template=} looks like a error, using default instead")
+        template = None
+    if template is None:
+        template = TEMPLATES.get(target.suffix)
+
+    if template is None:
+        raise ValueError(
+            f"bad file format: {target.suffix!r} (of {target})\n"
+            "only *.txt and *.py have a default template"
+        )
+    else:
+        return template
+
+
+def write_version_to_path(
+    target: Path, template: str | None, version: str, scm_version: ScmVersion | None
+) -> None:
+    final_template = _validate_template(target, template)
+    log.debug("dump %s into %s", version, target)
+    version_tuple = _version_as_tuple(version)
+    if scm_version is not None:
+        content = final_template.format(
+            version=version,
+            version_tuple=version_tuple,
+            scm_version=scm_version,
+        )
+    else:
+        content = final_template.format(version=version, version_tuple=version_tuple)
+
+    target.write_text(content, encoding="utf-8")
diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py
index 94981d7..33a78a8 100644
--- a/src/setuptools_scm/_integration/setuptools.py
+++ b/src/setuptools_scm/_integration/setuptools.py
@@ -48,9 +48,10 @@ Suggested workaround if applicable:
 def _assign_version(
     dist: setuptools.Distribution, config: _config.Configuration
 ) -> None:
-    from .. import _get_version, _version_missing
+    from .._get_version import _get_version, _version_missing
 
-    maybe_version = _get_version(config)
+    # todo: build time plugin
+    maybe_version = _get_version(config, force_write_version_files=True)
 
     if maybe_version is None:
         _version_missing(config)
diff --git a/tox.ini b/tox.ini
index 480d0ad..d52660a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist=py{37,38,39,310,311},check_readme,check-dist
+envlist=py{38,39,310,311},check_readme,check-dist
 
 [pytest]
 testpaths=testing
@@ -27,6 +27,7 @@ deps=
     pytest
     setuptools >= 45
     virtualenv>20
+    rich
 commands=
     pytest []
 
@@ -40,6 +41,7 @@ deps=
     pygments
     typing_extensions; python_version<'3.8'
     hatchling
+    rich
 commands=
     rst2html.py README.rst {envlogdir}/README.html --strict []
     check-manifest --no-build-isolation


Test patch:
diff --git a/testing/test_basic_api.py b/testing/test_basic_api.py
index 02084ae..76770de 100644
--- a/testing/test_basic_api.py
+++ b/testing/test_basic_api.py
@@ -56,7 +56,7 @@ def assert_root(monkeypatch: pytest.MonkeyPatch, expected_root: str) -> None:
         assert config.absolute_root == expected_root
         return ScmVersion("1.0", config=config)
 
-    monkeypatch.setattr(setuptools_scm, "_do_parse", assertion)
+    monkeypatch.setattr(setuptools_scm._get_version, "_do_parse", assertion)
 
 
 def test_root_parameter_creation(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -143,7 +143,7 @@ def test_get_version_blank_tag_regex() -> None:
     "version", ["1.0", "1.2.3.dev1+ge871260", "1.2.3.dev15+ge871260.d20180625", "2345"]
 )
 def test_pretended(version: str, monkeypatch: pytest.MonkeyPatch) -> None:
-    monkeypatch.setenv(setuptools_scm.PRETEND_KEY, version)
+    monkeypatch.setenv(setuptools_scm._overrides.PRETEND_KEY, version)
     assert setuptools_scm.get_version() == version
 
 
diff --git a/testing/test_functions.py b/testing/test_functions.py
index 3c8486d..ad8b167 100644
--- a/testing/test_functions.py
+++ b/testing/test_functions.py
@@ -7,7 +7,7 @@ import pytest
 from setuptools_scm import Configuration
 from setuptools_scm import dump_version
 from setuptools_scm import get_version
-from setuptools_scm import PRETEND_KEY
+from setuptools_scm._overrides import PRETEND_KEY
 from setuptools_scm._run_cmd import has_command
 from setuptools_scm.version import format_version
 from setuptools_scm.version import guess_next_version
@@ -86,8 +86,22 @@ def test_dump_version_works_with_pretend(
     version: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
 ) -> None:
     monkeypatch.setenv(PRETEND_KEY, version)
-    target = tmp_path.joinpath("VERSION.txt")
-    get_version(write_to=target)
+    name = "VERSION.txt"
+    target = tmp_path.joinpath(name)
+    get_version(root=tmp_path, write_to=name)
+    assert target.read_text() == version
+
+
+def test_dump_version_modern(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
+    version = "1.2.3"
+    monkeypatch.setenv(PRETEND_KEY, version)
+    name = "VERSION.txt"
+
+    project = tmp_path.joinpath("project")
+    target = project.joinpath(name)
+    project.mkdir()
+
+    get_version(root="..", relative_to=target, version_file=name)
     assert target.read_text() == version
 
 
diff --git a/testing/test_integration.py b/testing/test_integration.py
index 18d37c4..014039c 100644
--- a/testing/test_integration.py
+++ b/testing/test_integration.py
@@ -8,9 +8,9 @@ import pytest
 
 import setuptools_scm._integration.setuptools
 from .wd_wrapper import WorkDir
-from setuptools_scm import PRETEND_KEY
-from setuptools_scm import PRETEND_KEY_NAMED
 from setuptools_scm._integration.setuptools import _warn_on_old_setuptools
+from setuptools_scm._overrides import PRETEND_KEY
+from setuptools_scm._overrides import PRETEND_KEY_NAMED
 from setuptools_scm._run_cmd import run