diff --git a/README.rst b/README.rst index f72d6ca5..3a17d5a8 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'^(?Pv)?(?P[^\+]+)(?P.*)?$', - } - ) - 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 9c6f0e9e..f7c3e50d 100644 --- a/src/setuptools_scm/__init__.py +++ b/src/setuptools_scm/__init__.py @@ -37,6 +37,8 @@ def get_version( 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, diff --git a/src/setuptools_scm/_config.py b/src/setuptools_scm/_config.py index 7b439ae1..c9669759 100644 --- a/src/setuptools_scm/_config.py +++ b/src/setuptools_scm/_config.py @@ -93,6 +93,8 @@ class Configuration: fallback_root: _t.PathT = "." write_to: _t.PathT | None = None write_to_template: str | 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 diff --git a/src/setuptools_scm/_get_version.py b/src/setuptools_scm/_get_version.py index 5edd3f00..a1afe2d7 100644 --- a/src/setuptools_scm/_get_version.py +++ b/src/setuptools_scm/_get_version.py @@ -2,6 +2,7 @@ import re import warnings +from pathlib import Path from typing import Any from typing import NoReturn from typing import Pattern @@ -41,25 +42,48 @@ def _do_parse(config: Configuration) -> ScmVersion | None: ) -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, - ) +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_string, - scm_version=parsed_version, + 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 @@ -83,6 +107,8 @@ def get_version( 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, @@ -106,7 +132,7 @@ def get_version( del normalize tag_regex = parse_tag_regex(tag_regex) config = Configuration(**locals()) - maybe_version = _get_version(config) + maybe_version = _get_version(config, force_write_version_files=True) if maybe_version is None: _version_missing(config) diff --git a/src/setuptools_scm/_integration/dump_version.py b/src/setuptools_scm/_integration/dump_version.py index d39dbcee..d40c6bbd 100644 --- a/src/setuptools_scm/_integration/dump_version.py +++ b/src/setuptools_scm/_integration/dump_version.py @@ -1,11 +1,16 @@ 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 @@ -25,27 +30,44 @@ def dump_version( 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) - template = template or TEMPLATES.get(target.suffix) - from .._log import log + 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) - log.debug("dump %s into %s", version, write_to) if template is None: raise ValueError( f"bad file format: {target.suffix!r} (of {target})\n" "only *.txt and *.py have a default template" ) - version_tuple = _version_as_tuple(version) + 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 = template.format( + content = final_template.format( version=version, version_tuple=version_tuple, scm_version=scm_version, ) - else: - content = template.format(version=version, version_tuple=version_tuple) + content = final_template.format(version=version, version_tuple=version_tuple) - with open(target, "w", encoding="utf-8") as fp: - fp.write(content) + target.write_text(content, encoding="utf-8") diff --git a/src/setuptools_scm/_integration/setuptools.py b/src/setuptools_scm/_integration/setuptools.py index 5afd58f5..33a78a80 100644 --- a/src/setuptools_scm/_integration/setuptools.py +++ b/src/setuptools_scm/_integration/setuptools.py @@ -50,7 +50,8 @@ def _assign_version( ) -> None: 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/testing/test_functions.py b/testing/test_functions.py index 0b01fd47..ad8b1679 100644 --- a/testing/test_functions.py +++ b/testing/test_functions.py @@ -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