Skip to content

Commit 6321133

Browse files
Merge pull request #276 from punkadiddle/master
configurable version number extraction from tag strings
2 parents 88b10b6 + b960e29 commit 6321133

File tree

10 files changed

+265
-71
lines changed

10 files changed

+265
-71
lines changed

README.rst

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ Builtin mechanisms for obtaining version numbers
156156

157157

158158
Configuration Parameters
159-
------------------------------
159+
------------------------
160160

161161
In order to configure the way ``use_scm_version`` works you can provide
162162
a mapping with options instead of a boolean value.
@@ -202,6 +202,14 @@ The currently supported configuration keys are:
202202
Use with caution, this is a function for advanced use, and you should be
203203
familiar with the setuptools_scm internals to use it.
204204

205+
:tag_regex:
206+
A python regex string to extract the version part from any SCM tag.
207+
The regex needs to contain three named groups prefix, version and suffix,
208+
where `version` captures the actual version information.
209+
210+
defaults to the value of ``setuptools_scm.config.DEFAULT_TAG_REGEX``
211+
(see `config.py <src/setuptools_scm/config.py>`_).
212+
205213

206214
To use setuptools_scm in other Python code you can use the
207215
``get_version`` function:
@@ -214,6 +222,19 @@ To use setuptools_scm in other Python code you can use the
214222
It optionally accepts the keys of the ``use_scm_version`` parameter as
215223
keyword arguments.
216224

225+
Example configuration in `setup.py` format:
226+
227+
.. code:: python
228+
229+
from setuptools import setup
230+
231+
232+
setup(
233+
use_scm_version={
234+
'write_to': 'version.txt',
235+
'tag_regex': r'^(?P<prefix>v)?(?P<version>[^\+]+)(?P<suffix>.*)?$',
236+
}
237+
)
217238
218239
Environment Variables
219240
---------------------

src/setuptools_scm/__init__.py

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
:license: MIT
44
"""
55
import os
6-
import sys
6+
import warnings
77

8-
from .utils import trace
8+
from .config import Configuration
9+
from .utils import function_has_arg, string_types
910
from .version import format_version, meta
1011
from .discover import iter_matching_entrypoints
1112

1213
PRETEND_KEY = "SETUPTOOLS_SCM_PRETEND_VERSION"
1314

14-
1515
TEMPLATES = {
1616
".py": """\
1717
# coding: utf-8
@@ -22,17 +22,24 @@
2222
".txt": "{version}",
2323
}
2424

25-
PY3 = sys.version_info > (3,)
26-
string_types = (str,) if PY3 else (str, unicode) # noqa
27-
2825

2926
def version_from_scm(root):
27+
# TODO: Is it API?
3028
return _version_from_entrypoint(root, "setuptools_scm.parse_scm")
3129

3230

33-
def _version_from_entrypoint(root, entrypoint):
34-
for ep in iter_matching_entrypoints(root, entrypoint):
35-
version = ep.load()(root)
31+
def _call_entrypoint_fn(config, fn):
32+
if function_has_arg(fn, 'config'):
33+
return fn(config.absolute_root, config=config)
34+
else:
35+
warnings.warn("parse functions are required to provide a named argument 'config' in the future.", PendingDeprecationWarning)
36+
return fn(config.absolute_root)
37+
38+
39+
def _version_from_entrypoint(config, entrypoint):
40+
for ep in iter_matching_entrypoints(config.absolute_root, entrypoint):
41+
version = _call_entrypoint_fn(config, ep.load())
42+
3643
if version:
3744
return version
3845

@@ -55,27 +62,26 @@ def dump_version(root, version, write_to, template=None):
5562
fp.write(template.format(version=version))
5663

5764

58-
def _do_parse(root, parse):
65+
def _do_parse(config):
5966
pretended = os.environ.get(PRETEND_KEY)
6067
if pretended:
6168
# we use meta here since the pretended version
6269
# must adhere to the pep to begin with
6370
return meta(tag=pretended, preformatted=True)
6471

65-
if parse:
66-
parse_result = parse(root)
72+
if config.parse:
73+
parse_result = _call_entrypoint_fn(config, config.parse)
6774
if isinstance(parse_result, string_types):
6875
raise TypeError(
6976
"version parse result was a string\nplease return a parsed version"
7077
)
71-
version = parse_result or _version_from_entrypoint(
72-
root, "setuptools_scm.parse_scm_fallback"
73-
)
78+
version = parse_result or \
79+
_version_from_entrypoint(config, "setuptools_scm.parse_scm_fallback")
80+
7481
else:
7582
# include fallbacks after dropping them from the main entrypoint
76-
version = version_from_scm(root) or _version_from_entrypoint(
77-
root, "setuptools_scm.parse_scm_fallback"
78-
)
83+
version = _version_from_entrypoint(config, "setuptools_scm.parse_scm") or \
84+
_version_from_entrypoint(config, "setuptools_scm.parse_scm_fallback")
7985

8086
if version:
8187
return version
@@ -88,7 +94,7 @@ def _do_parse(root, parse):
8894
"metadata and will not work.\n\n"
8995
"For example, if you're using pip, instead of "
9096
"https://github.com/user/proj/archive/master.zip "
91-
"use git+https://github.com/user/proj.git#egg=proj" % root
97+
"use git+https://github.com/user/proj.git#egg=proj" % config.absolute_root
9298
)
9399

94100

@@ -99,6 +105,7 @@ def get_version(
99105
write_to=None,
100106
write_to_template=None,
101107
relative_to=None,
108+
tag_regex=None,
102109
parse=None,
103110
):
104111
"""
@@ -107,12 +114,18 @@ def get_version(
107114
in the root of the repository to direct setuptools_scm to the
108115
root of the repository by supplying ``__file__``.
109116
"""
110-
if relative_to:
111-
root = os.path.join(os.path.dirname(relative_to), root)
112-
root = os.path.abspath(root)
113-
trace("root", repr(root))
114-
115-
parsed_version = _do_parse(root, parse)
117+
118+
config = Configuration()
119+
config.root = root
120+
config.version_scheme = version_scheme
121+
config.local_scheme = local_scheme
122+
config.write_to = write_to
123+
config.write_to_template = write_to_template
124+
config.relative_to = relative_to
125+
config.tag_regex = tag_regex
126+
config.parse = parse
127+
128+
parsed_version = _do_parse(config)
116129

117130
if parsed_version:
118131
version_string = format_version(

src/setuptools_scm/config.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
""" configuration """
2+
from __future__ import print_function, unicode_literals
3+
import os
4+
import re
5+
import warnings
6+
7+
from .utils import trace
8+
9+
DEFAULT_TAG_REGEX = r'^(?:\w+-)?(?P<version>v?\d+(?:\.\d+){0,2}[^\+]+)(?:\+.*)?$'
10+
DEFAULT_VERSION_SCHEME = 'version_scheme'
11+
12+
13+
def _check_tag_regex(value):
14+
if not value:
15+
value = DEFAULT_TAG_REGEX
16+
regex = re.compile(value)
17+
18+
group_names = regex.groupindex.keys()
19+
if regex.groups == 0 or (regex.groups > 1 and 'version' not in group_names):
20+
warnings.warn("Expected tag_regex to contain a single match group or a group named 'version' " +
21+
"to identify the version part of any tag.")
22+
23+
return regex
24+
25+
26+
def _check_absolute_root(root, relative_to):
27+
if relative_to:
28+
if os.path.isabs(root) and not root.startswith(relative_to):
29+
warnings.warn("absolute root path '%s' overrides relative_to '%s'" % (root, relative_to))
30+
root = os.path.join(os.path.dirname(relative_to), root)
31+
return os.path.abspath(root)
32+
33+
34+
class Configuration(object):
35+
""" Global configuration model """
36+
37+
_root = None
38+
version_scheme = None
39+
local_scheme = None
40+
write_to = None
41+
write_to_template = None
42+
_relative_to = None
43+
parse = None
44+
_tag_regex = None
45+
_absolute_root = None
46+
47+
def __init__(self,
48+
relative_to=None,
49+
root='.'):
50+
# TODO:
51+
self._relative_to = relative_to
52+
self._root = '.'
53+
54+
self.root = root
55+
self.version_scheme = DEFAULT_VERSION_SCHEME
56+
self.local_scheme = "node-and-date"
57+
self.write_to = ''
58+
self.write_to_template = None
59+
self.parse = None
60+
self.tag_regex = DEFAULT_TAG_REGEX
61+
62+
@property
63+
def absolute_root(self):
64+
return self._absolute_root
65+
66+
@property
67+
def relative_to(self):
68+
return self._relative_to
69+
70+
@relative_to.setter
71+
def relative_to(self, value):
72+
self._absolute_root = _check_absolute_root(self._root, value)
73+
self._relative_to = value
74+
trace("root", repr(self._absolute_root))
75+
76+
@property
77+
def root(self):
78+
return self._root
79+
80+
@root.setter
81+
def root(self, value):
82+
self._absolute_root = _check_absolute_root(value, self._relative_to)
83+
self._root = value
84+
trace("root", repr(self._absolute_root))
85+
86+
@property
87+
def tag_regex(self):
88+
return self._tag_regex
89+
90+
@tag_regex.setter
91+
def tag_regex(self, value):
92+
self._tag_regex = _check_tag_regex(value)

src/setuptools_scm/git.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from .config import Configuration
12
from .utils import do_ex, trace, has_command
23
from .version import meta
34

@@ -82,20 +83,23 @@ def fail_on_shallow(wd):
8283
)
8384

8485

85-
def parse(root, describe_command=DEFAULT_DESCRIBE, pre_parse=warn_on_shallow):
86+
def parse(root, config=None, describe_command=DEFAULT_DESCRIBE, pre_parse=warn_on_shallow):
8687
"""
8788
:param pre_parse: experimental pre_parse action, may change at any time
8889
"""
90+
if not config:
91+
config = Configuration(root=root)
92+
8993
if not has_command("git"):
9094
return
9195

92-
wd = GitWorkdir.from_potential_worktree(root)
96+
wd = GitWorkdir.from_potential_worktree(config.absolute_root)
9397
if wd is None:
9498
return
9599
if pre_parse:
96100
pre_parse(wd)
97101

98-
out, err, ret = wd.do_ex(describe_command)
102+
out, unused_err, ret = wd.do_ex(describe_command)
99103
if ret:
100104
# If 'git describe' failed, try to get the information otherwise.
101105
rev_node = wd.node()
@@ -116,9 +120,9 @@ def parse(root, describe_command=DEFAULT_DESCRIBE, pre_parse=warn_on_shallow):
116120

117121
branch = wd.get_branch()
118122
if number:
119-
return meta(tag, distance=number, node=node, dirty=dirty, branch=branch)
123+
return meta(tag, config=config, distance=number, node=node, dirty=dirty, branch=branch)
120124
else:
121-
return meta(tag, node=node, dirty=dirty, branch=branch)
125+
return meta(tag, config=config, node=node, dirty=dirty, branch=branch)
122126

123127

124128
def _git_parse_describe(describe_output):

src/setuptools_scm/hg.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
from .config import Configuration
23
from .utils import do, trace, data_from_mime, has_command
34
from .version import meta, tags_to_versions
45

@@ -28,10 +29,13 @@ def _hg_tagdist_normalize_tagcommit(root, tag, dist, node, branch):
2829
return meta(tag)
2930

3031

31-
def parse(root):
32+
def parse(root, config=None):
33+
if not config:
34+
config = Configuration(root=root)
35+
3236
if not has_command("hg"):
3337
return
34-
identity_data = do("hg id -i -b -t", root).split()
38+
identity_data = do("hg id -i -b -t", config.absolute_root).split()
3539
if not identity_data:
3640
return
3741
node = identity_data.pop(0)
@@ -44,16 +48,16 @@ def parse(root):
4448
return meta(tags[0], dirty=dirty, branch=branch)
4549

4650
if node.strip("+") == "0" * 12:
47-
trace("initial node", root)
48-
return meta("0.0", dirty=dirty, branch=branch)
51+
trace("initial node", config.absolute_root)
52+
return meta("0.0", config=config, dirty=dirty, branch=branch)
4953

5054
try:
51-
tag = get_latest_normalizable_tag(root)
52-
dist = get_graph_distance(root, tag)
55+
tag = get_latest_normalizable_tag(config.absolute_root)
56+
dist = get_graph_distance(config.absolute_root, tag)
5357
if tag == "null":
5458
tag = "0.0"
5559
dist = int(dist) + 1
56-
return _hg_tagdist_normalize_tagcommit(root, tag, dist, node, branch)
60+
return _hg_tagdist_normalize_tagcommit(config.absolute_root, tag, dist, node, branch)
5761
except ValueError:
5862
pass # unpacking failed, old hg
5963

src/setuptools_scm/utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
utils
33
"""
44
from __future__ import print_function, unicode_literals
5+
import inspect
56
import warnings
67
import sys
78
import shlex
@@ -14,6 +15,8 @@
1415
DEBUG = bool(os.environ.get("SETUPTOOLS_SCM_DEBUG"))
1516
IS_WINDOWS = platform.system() == "Windows"
1617
PY2 = sys.version_info < (3,)
18+
PY3 = sys.version_info > (3,)
19+
string_types = (str,) if PY3 else (str, unicode) # noqa
1720

1821

1922
def trace(*k):
@@ -91,6 +94,17 @@ def data_from_mime(path):
9194
return data
9295

9396

97+
def function_has_arg(fn, argname):
98+
assert inspect.isfunction(fn)
99+
100+
if PY2:
101+
argspec = inspect.getargspec(fn).args
102+
else:
103+
argspec = inspect.getfullargspec(fn).args
104+
105+
return argname in argspec
106+
107+
94108
def has_command(name):
95109
try:
96110
p = _popen_pipes([name, "help"], ".")

0 commit comments

Comments
 (0)