Skip to content

Commit

Permalink
Merge pull request #584 from smarie/feature/custom_version_cls
Browse files Browse the repository at this point in the history
Support for custom and non-normalizing version classes
  • Loading branch information
RonnyPfannschmidt committed Jun 25, 2021
2 parents b680e37 + 7d07d9f commit ece87b2
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 21 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
In progress
===========

* fix #524: new parameters ``normalize`` and ``version_cls`` to customize the version normalization class.

v6.0.1
=======

Expand Down
24 changes: 24 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,30 @@ The currently supported configuration keys are:
Defaults to the value set by ``setuptools_scm.git.DEFAULT_DESCRIBE``
(see `git.py <src/setuptools_scm/git.py>`_).

:normalize:
A boolean flag indicating if the version string should be normalized.
Defaults to ``True``. Setting this to ``False`` is equivalent to setting
``version_cls`` to ``setuptools_scm.version.NonNormalizedVersion``

:version_cls:
An optional class used to parse, verify and possibly normalize the version
string. Its constructor should receive a single string argument, and its
``str`` should return the normalized version string to use.
This option can also receive a class qualified name as a string.

This defaults to ``packaging.version.Version`` if available. If
``packaging`` is not installed, ``pkg_resources.packaging.version.Version``
is used. Note that it is known to modify git release candidate schemes.

The ``setuptools_scm.NonNormalizedVersion`` convenience class is
provided to disable the normalization step done by
``packaging.version.Version``. If this is used while ``setuptools_scm``
is integrated in a setuptools packaging process, the non-normalized
version number will appear in all files (see ``write_to``) BUT note
that setuptools will still normalize it to create the final distribution,
so as to stay compliant with the python packaging standards.


To use ``setuptools_scm`` in other Python code you can use the ``get_version``
function:

Expand Down
22 changes: 22 additions & 0 deletions src/setuptools_scm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
DEFAULT_VERSION_SCHEME,
DEFAULT_LOCAL_SCHEME,
DEFAULT_TAG_REGEX,
NonNormalizedVersion,
)
from .utils import function_has_arg, trace
from .version import format_version, meta
Expand Down Expand Up @@ -159,6 +160,8 @@ def get_version(
parse=None,
git_describe_command=None,
dist_name=None,
version_cls=None,
normalize=True,
):
"""
If supplied, relative_to should be a file from which root may
Expand Down Expand Up @@ -188,3 +191,22 @@ def _get_version(config):
)

return version_string


# Public API
__all__ = [
"get_version",
"dump_version",
"version_from_scm",
"Configuration",
"NonNormalizedVersion",
"DEFAULT_VERSION_SCHEME",
"DEFAULT_LOCAL_SCHEME",
"DEFAULT_TAG_REGEX",
# TODO: are the symbols below part of public API ?
"function_has_arg",
"trace",
"format_version",
"meta",
"iter_matching_entrypoints",
]
59 changes: 59 additions & 0 deletions src/setuptools_scm/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
import re
import warnings

try:
from packaging.version import Version
except ImportError:
import pkg_resources

Version = pkg_resources.packaging.version.Version


from .utils import trace

DEFAULT_TAG_REGEX = r"^(?:[\w-]+-)?(?P<version>[vV]?\d+(?:\.\d+){0,2}[^\+]*)(?:\+.*)?$"
Expand Down Expand Up @@ -65,6 +73,8 @@ def __init__(
parse=None,
git_describe_command=None,
dist_name=None,
version_cls=None,
normalize=True,
):
# TODO:
self._relative_to = relative_to
Expand All @@ -83,6 +93,30 @@ def __init__(
self.git_describe_command = git_describe_command
self.dist_name = dist_name

if not normalize:
# `normalize = False` means `version_cls = NonNormalizedVersion`
if version_cls is not None:
raise ValueError(
"Providing a custom `version_cls` is not permitted when "
"`normalize=False`"
)
self.version_cls = NonNormalizedVersion
else:
# Use `version_cls` if provided, default to packaging or pkg_resources
if version_cls is None:
version_cls = Version
elif isinstance(version_cls, str):
try:
# Not sure this will work in old python
import importlib

pkg, cls_name = version_cls.rsplit(".", 1)
version_cls_host = importlib.import_module(pkg)
version_cls = getattr(version_cls_host, cls_name)
except: # noqa
raise ValueError(f"Unable to import version_cls='{version_cls}'")
self.version_cls = version_cls

@property
def fallback_root(self):
return self._fallback_root
Expand Down Expand Up @@ -137,3 +171,28 @@ def from_file(cls, name="pyproject.toml", dist_name=None):
defn = __import__("toml").load(strm)
section = defn.get("tool", {})["setuptools_scm"]
return cls(dist_name=dist_name, **section)


class NonNormalizedVersion(Version):
"""A non-normalizing version handler.
You can use this class to preserve version verification but skip normalization.
For example you can use this to avoid git release candidate version tags
("1.0.0-rc1") to be normalized to "1.0.0rc1". Only use this if you fully
trust the version tags.
"""

def __init__(self, version):
# parse and validate using parent
super().__init__(version)

# store raw for str
self._raw_version = version

def __str__(self):
# return the non-normalized version (parent returns the normalized)
return self._raw_version

def __repr__(self):
# same pattern as parent
return f"<NonNormalizedVersion({self._raw_version!r})>"
26 changes: 12 additions & 14 deletions src/setuptools_scm/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,9 @@
import time
import os

from .config import Configuration
from .config import Configuration, Version as PkgVersion
from .utils import trace, iter_entry_points

try:
from packaging.version import Version
except ImportError:
import pkg_resources

Version = pkg_resources.packaging.version.Version


SEMVER_MINOR = 2
SEMVER_PATCH = 3
Expand Down Expand Up @@ -77,7 +70,7 @@ def tag_to_version(tag, config=None):
)
)

version = Version(version)
version = config.version_cls(version)
trace("version", repr(version))

return version
Expand Down Expand Up @@ -168,7 +161,7 @@ def format_next_version(self, guess_next, fmt="{guessed}.dev{distance}", **kw):
def _parse_tag(tag, preformatted, config):
if preformatted:
return tag
if not isinstance(tag, Version):
if not isinstance(tag, config.version_cls):
tag = tag_to_version(tag, config)
return tag

Expand Down Expand Up @@ -319,7 +312,7 @@ def date_ver_match(ver):
return match


def guess_next_date_ver(version, node_date=None, date_fmt=None):
def guess_next_date_ver(version, node_date=None, date_fmt=None, version_cls=None):
"""
same-day -> patch +1
other-day -> today
Expand Down Expand Up @@ -354,8 +347,9 @@ def guess_next_date_ver(version, node_date=None, date_fmt=None):
node_date=head_date, date_fmt=date_fmt, patch=patch
)
# rely on the Version object to ensure consistency (e.g. remove leading 0s)
# TODO: support for intentionally non-normalized date versions
next_version = str(Version(next_version))
if version_cls is None:
version_cls = PkgVersion
next_version = str(version_cls(next_version))
return next_version


Expand All @@ -370,7 +364,11 @@ def calver_by_date(version):
match = date_ver_match(ver)
if match:
return ver
return version.format_next_version(guess_next_date_ver, node_date=version.node_date)
return version.format_next_version(
guess_next_date_ver,
node_date=version.node_date,
version_cls=version.config.version_cls,
)


def _format_local_with_time(version, time_format):
Expand Down
21 changes: 21 additions & 0 deletions testing/test_basic_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,24 @@ def parse(root):

with pytest.raises(TypeError):
setuptools_scm.get_version(parse=parse)


def test_custom_version_cls():
"""Test that `normalize` and `version_cls` work as expected"""

class MyVersion:
def __init__(self, tag_str: str):
self.version = tag_str

def __repr__(self):
return f"hello,{self.version}"

# you can not use normalize=False and version_cls at the same time
with pytest.raises(ValueError):
setuptools_scm.get_version(normalize=False, version_cls=MyVersion)

# TODO unfortunately with PRETEND_KEY the preformatted flag becomes True
# which bypasses our class. which other mechanism would be ok to use here
# to create a test?
# monkeypatch.setenv(setuptools_scm.PRETEND_KEY, "1.0.1")
# assert setuptools_scm.get_version(version_cls=MyVersion) == "1"
78 changes: 71 additions & 7 deletions testing/test_git.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import sys
import os
from setuptools_scm import integration
from setuptools_scm.utils import do, has_command
from setuptools_scm import git
import pytest
from datetime import datetime
from os.path import join as opj
from setuptools_scm.file_finder_git import git_find_files
from datetime import date
import pytest
from datetime import datetime, date
from unittest.mock import patch, Mock

from setuptools_scm import integration, git, NonNormalizedVersion
from setuptools_scm.utils import do, has_command
from setuptools_scm.file_finder_git import git_find_files


pytestmark = pytest.mark.skipif(
not has_command("git", warn=False), reason="git executable not found"
)
Expand Down Expand Up @@ -104,6 +104,70 @@ def test_version_from_git(wd):
wd("git tag 17.33.0-rc")
assert wd.version == "17.33.0rc0"

# custom normalization
assert wd.get_version(normalize=False) == "17.33.0-rc"
assert wd.get_version(version_cls=NonNormalizedVersion) == "17.33.0-rc"
assert (
wd.get_version(version_cls="setuptools_scm.NonNormalizedVersion")
== "17.33.0-rc"
)


@pytest.mark.parametrize("with_class", [False, type, str])
def test_git_version_unnormalized_setuptools(with_class, tmpdir, wd, monkeypatch):
"""
Test that when integrating with setuptools without normalization,
the version is not normalized in write_to files,
but still normalized by setuptools for the final dist metadata.
"""
monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG")
p = wd.cwd

# create a setup.py
dest_file = str(tmpdir.join("VERSION.txt")).replace("\\", "/")
if with_class is False:
# try normalize = False
setup_py = """
from setuptools import setup
setup(use_scm_version={'normalize': False, 'write_to': '%s'})
"""
elif with_class is type:
# custom non-normalizing class
setup_py = """
from setuptools import setup
class MyVersion:
def __init__(self, tag_str: str):
self.version = tag_str
def __repr__(self):
return self.version
setup(use_scm_version={'version_cls': MyVersion, 'write_to': '%s'})
"""
elif with_class is str:
# non-normalizing class referenced by name
setup_py = """from setuptools import setup
setup(use_scm_version={
'version_cls': 'setuptools_scm.NonNormalizedVersion',
'write_to': '%s'
})
"""

# finally write the setup.py file
p.joinpath("setup.py").write_text(setup_py % dest_file)

# do git operations and tag
wd.commit_testfile()
wd("git tag 17.33.0-rc1")

# setuptools still normalizes using packaging.Version (removing the dash)
res = do((sys.executable, "setup.py", "--version"), p)
assert res == "17.33.0rc1"

# but the version tag in the file is non-normalized (with the dash)
assert tmpdir.join("VERSION.txt").read() == "17.33.0-rc1"


@pytest.mark.issue(179)
def test_unicode_version_scheme(wd):
Expand Down
16 changes: 16 additions & 0 deletions testing/test_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,19 @@ def test_calver_by_date_semver(version, expected_next):
def test_calver_by_date_future_warning():
with pytest.warns(UserWarning, match="your previous tag*"):
calver_by_date(meta(date_to_str(days_offset=-2), config=c, distance=2))


def test_custom_version_cls():
"""Test that we can pass our own version class instead of pkg_resources"""

class MyVersion:
def __init__(self, tag_str: str):
self.tag = tag_str

def __repr__(self):
return "Custom %s" % self.tag

scm_version = meta("1.0.0-foo", config=Configuration(version_cls=MyVersion))

assert isinstance(scm_version.tag, MyVersion)
assert repr(scm_version.tag) == "Custom 1.0.0-foo"

0 comments on commit ece87b2

Please sign in to comment.