mirror of
https://github.com/kristoferssolo/School.git
synced 2025-10-21 20:10:38 +00:00
461 lines
14 KiB
Python
461 lines
14 KiB
Python
import datetime
|
|
import os
|
|
import re
|
|
import time
|
|
import warnings
|
|
|
|
from .config import Configuration
|
|
from .config import Version as PkgVersion
|
|
from .utils import iter_entry_points
|
|
from .utils import trace
|
|
|
|
|
|
SEMVER_MINOR = 2
|
|
SEMVER_PATCH = 3
|
|
SEMVER_LEN = 3
|
|
|
|
|
|
def _parse_version_tag(tag, config):
|
|
tagstring = tag if isinstance(tag, str) else str(tag)
|
|
match = config.tag_regex.match(tagstring)
|
|
|
|
result = None
|
|
if match:
|
|
if len(match.groups()) == 1:
|
|
key = 1
|
|
else:
|
|
key = "version"
|
|
|
|
result = {
|
|
"version": match.group(key),
|
|
"prefix": match.group(0)[: match.start(key)],
|
|
"suffix": match.group(0)[match.end(key) :],
|
|
}
|
|
|
|
trace(f"tag '{tag}' parsed to {result}")
|
|
return result
|
|
|
|
|
|
def callable_or_entrypoint(group, callable_or_name):
|
|
trace("ep", (group, callable_or_name))
|
|
|
|
if callable(callable_or_name):
|
|
return callable_or_name
|
|
|
|
for ep in iter_entry_points(group, callable_or_name):
|
|
trace("ep found:", ep.name)
|
|
return ep.load()
|
|
|
|
|
|
def tag_to_version(tag, config: "Configuration | None" = None):
|
|
"""
|
|
take a tag that might be prefixed with a keyword and return only the version part
|
|
:param config: optional configuration object
|
|
"""
|
|
trace("tag", tag)
|
|
|
|
if not config:
|
|
config = Configuration()
|
|
|
|
tagdict = _parse_version_tag(tag, config)
|
|
if not isinstance(tagdict, dict) or not tagdict.get("version", None):
|
|
warnings.warn(f"tag {tag!r} no version found")
|
|
return None
|
|
|
|
version = tagdict["version"]
|
|
trace("version pre parse", version)
|
|
|
|
if tagdict.get("suffix", ""):
|
|
warnings.warn(
|
|
"tag {!r} will be stripped of its suffix '{}'".format(
|
|
tag, tagdict["suffix"]
|
|
)
|
|
)
|
|
|
|
version = config.version_cls(version)
|
|
trace("version", repr(version))
|
|
|
|
return version
|
|
|
|
|
|
def tags_to_versions(tags, config=None):
|
|
"""
|
|
take tags that might be prefixed with a keyword and return only the version part
|
|
:param tags: an iterable of tags
|
|
:param config: optional configuration object
|
|
"""
|
|
result = []
|
|
for tag in tags:
|
|
tag = tag_to_version(tag, config=config)
|
|
if tag:
|
|
result.append(tag)
|
|
return result
|
|
|
|
|
|
class ScmVersion:
|
|
def __init__(
|
|
self,
|
|
tag_version,
|
|
distance=None,
|
|
node=None,
|
|
dirty=False,
|
|
preformatted=False,
|
|
branch=None,
|
|
config=None,
|
|
node_date=None,
|
|
**kw,
|
|
):
|
|
if kw:
|
|
trace("unknown args", kw)
|
|
self.tag = tag_version
|
|
if dirty and distance is None:
|
|
distance = 0
|
|
self.distance = distance
|
|
self.node = node
|
|
self.node_date = node_date
|
|
self.time = datetime.datetime.utcfromtimestamp(
|
|
int(os.environ.get("SOURCE_DATE_EPOCH", time.time()))
|
|
)
|
|
self._extra = kw
|
|
self.dirty = dirty
|
|
self.preformatted = preformatted
|
|
self.branch = branch
|
|
self.config = config
|
|
|
|
@property
|
|
def extra(self):
|
|
warnings.warn(
|
|
"ScmVersion.extra is deprecated and will be removed in future",
|
|
category=DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
return self._extra
|
|
|
|
@property
|
|
def exact(self):
|
|
return self.distance is None
|
|
|
|
def __repr__(self):
|
|
return self.format_with(
|
|
"<ScmVersion {tag} d={distance} n={node} d={dirty} b={branch}>"
|
|
)
|
|
|
|
def format_with(self, fmt, **kw):
|
|
return fmt.format(
|
|
time=self.time,
|
|
tag=self.tag,
|
|
distance=self.distance,
|
|
node=self.node,
|
|
dirty=self.dirty,
|
|
branch=self.branch,
|
|
node_date=self.node_date,
|
|
**kw,
|
|
)
|
|
|
|
def format_choice(self, clean_format, dirty_format, **kw):
|
|
return self.format_with(dirty_format if self.dirty else clean_format, **kw)
|
|
|
|
def format_next_version(self, guess_next, fmt="{guessed}.dev{distance}", **kw):
|
|
guessed = guess_next(self.tag, **kw)
|
|
return self.format_with(fmt, guessed=guessed)
|
|
|
|
|
|
def _parse_tag(tag, preformatted, config: "Configuration|None"):
|
|
if preformatted:
|
|
return tag
|
|
if config is None or not isinstance(tag, config.version_cls):
|
|
tag = tag_to_version(tag, config)
|
|
return tag
|
|
|
|
|
|
def meta(
|
|
tag,
|
|
distance: "int|None" = None,
|
|
dirty: bool = False,
|
|
node: "str|None" = None,
|
|
preformatted: bool = False,
|
|
branch: "str|None" = None,
|
|
config: "Configuration|None" = None,
|
|
**kw,
|
|
):
|
|
if not config:
|
|
warnings.warn(
|
|
"meta invoked without explicit configuration,"
|
|
" will use defaults where required."
|
|
)
|
|
parsed_version = _parse_tag(tag, preformatted, config)
|
|
trace("version", tag, "->", parsed_version)
|
|
assert parsed_version is not None, "Can't parse version %s" % tag
|
|
return ScmVersion(
|
|
parsed_version, distance, node, dirty, preformatted, branch, config, **kw
|
|
)
|
|
|
|
|
|
def guess_next_version(tag_version: ScmVersion):
|
|
version = _strip_local(str(tag_version))
|
|
return _bump_dev(version) or _bump_regex(version)
|
|
|
|
|
|
def _strip_local(version_string):
|
|
public, sep, local = version_string.partition("+")
|
|
return public
|
|
|
|
|
|
def _bump_dev(version):
|
|
if ".dev" not in version:
|
|
return
|
|
|
|
prefix, tail = version.rsplit(".dev", 1)
|
|
if tail != "0":
|
|
raise ValueError(
|
|
"choosing custom numbers for the `.devX` distance "
|
|
"is not supported.\n "
|
|
"The {version} can't be bumped\n"
|
|
"Please drop the tag or create a new supported one".format(version=version)
|
|
)
|
|
return prefix
|
|
|
|
|
|
def _bump_regex(version):
|
|
match = re.match(r"(.*?)(\d+)$", version)
|
|
if match is None:
|
|
raise ValueError(
|
|
"{version} does not end with a number to bump, "
|
|
"please correct or use a custom version scheme".format(version=version)
|
|
)
|
|
else:
|
|
prefix, tail = match.groups()
|
|
return "%s%d" % (prefix, int(tail) + 1)
|
|
|
|
|
|
def guess_next_dev_version(version):
|
|
if version.exact:
|
|
return version.format_with("{tag}")
|
|
else:
|
|
return version.format_next_version(guess_next_version)
|
|
|
|
|
|
def guess_next_simple_semver(version, retain, increment=True):
|
|
try:
|
|
parts = [int(i) for i in str(version).split(".")[:retain]]
|
|
except ValueError:
|
|
raise ValueError(f"{version} can't be parsed as numeric version")
|
|
while len(parts) < retain:
|
|
parts.append(0)
|
|
if increment:
|
|
parts[-1] += 1
|
|
while len(parts) < SEMVER_LEN:
|
|
parts.append(0)
|
|
return ".".join(str(i) for i in parts)
|
|
|
|
|
|
def simplified_semver_version(version):
|
|
if version.exact:
|
|
return guess_next_simple_semver(version.tag, retain=SEMVER_LEN, increment=False)
|
|
else:
|
|
if version.branch is not None and "feature" in version.branch:
|
|
return version.format_next_version(
|
|
guess_next_simple_semver, retain=SEMVER_MINOR
|
|
)
|
|
else:
|
|
return version.format_next_version(
|
|
guess_next_simple_semver, retain=SEMVER_PATCH
|
|
)
|
|
|
|
|
|
def release_branch_semver_version(version):
|
|
if version.exact:
|
|
return version.format_with("{tag}")
|
|
if version.branch is not None:
|
|
# Does the branch name (stripped of namespace) parse as a version?
|
|
branch_ver = _parse_version_tag(version.branch.split("/")[-1], version.config)
|
|
if branch_ver is not None:
|
|
branch_ver = branch_ver["version"]
|
|
if branch_ver[0] == "v":
|
|
# Allow branches that start with 'v', similar to Version.
|
|
branch_ver = branch_ver[1:]
|
|
# Does the branch version up to the minor part match the tag? If not it
|
|
# might be like, an issue number or something and not a version number, so
|
|
# we only want to use it if it matches.
|
|
tag_ver_up_to_minor = str(version.tag).split(".")[:SEMVER_MINOR]
|
|
branch_ver_up_to_minor = branch_ver.split(".")[:SEMVER_MINOR]
|
|
if branch_ver_up_to_minor == tag_ver_up_to_minor:
|
|
# We're in a release/maintenance branch, next is a patch/rc/beta bump:
|
|
return version.format_next_version(guess_next_version)
|
|
# We're in a development branch, next is a minor bump:
|
|
return version.format_next_version(guess_next_simple_semver, retain=SEMVER_MINOR)
|
|
|
|
|
|
def release_branch_semver(version):
|
|
warnings.warn(
|
|
"release_branch_semver is deprecated and will be removed in future. "
|
|
+ "Use release_branch_semver_version instead",
|
|
category=DeprecationWarning,
|
|
stacklevel=2,
|
|
)
|
|
return release_branch_semver_version(version)
|
|
|
|
|
|
def no_guess_dev_version(version):
|
|
if version.exact:
|
|
return version.format_with("{tag}")
|
|
else:
|
|
return version.format_with("{tag}.post1.dev{distance}")
|
|
|
|
|
|
def date_ver_match(ver):
|
|
match = re.match(
|
|
(
|
|
r"^(?P<date>(?P<year>\d{2}|\d{4})(?:\.\d{1,2}){2})"
|
|
r"(?:\.(?P<patch>\d*)){0,1}?$"
|
|
),
|
|
str(ver),
|
|
)
|
|
return match
|
|
|
|
|
|
def guess_next_date_ver(version, node_date=None, date_fmt=None, version_cls=None):
|
|
"""
|
|
same-day -> patch +1
|
|
other-day -> today
|
|
|
|
distance is always added as .devX
|
|
"""
|
|
match = date_ver_match(version)
|
|
if match is None:
|
|
warnings.warn(
|
|
f"{version} does not correspond to a valid versioning date, "
|
|
"assuming legacy version"
|
|
)
|
|
if date_fmt is None:
|
|
date_fmt = "%y.%m.%d"
|
|
|
|
# deduct date format if not provided
|
|
if date_fmt is None:
|
|
date_fmt = "%Y.%m.%d" if len(match.group("year")) == 4 else "%y.%m.%d"
|
|
head_date = node_date or datetime.date.today()
|
|
# compute patch
|
|
if match is None:
|
|
tag_date = datetime.date.today()
|
|
else:
|
|
tag_date = datetime.datetime.strptime(match.group("date"), date_fmt).date()
|
|
if tag_date == head_date:
|
|
patch = "0" if match is None else (match.group("patch") or "0")
|
|
patch = int(patch) + 1
|
|
else:
|
|
if tag_date > head_date and match is not None:
|
|
# warn on future times
|
|
warnings.warn(
|
|
"your previous tag ({}) is ahead your node date ({})".format(
|
|
tag_date, head_date
|
|
)
|
|
)
|
|
patch = 0
|
|
next_version = "{node_date:{date_fmt}}.{patch}".format(
|
|
node_date=head_date, date_fmt=date_fmt, patch=patch
|
|
)
|
|
# rely on the Version object to ensure consistency (e.g. remove leading 0s)
|
|
if version_cls is None:
|
|
version_cls = PkgVersion
|
|
next_version = str(version_cls(next_version))
|
|
return next_version
|
|
|
|
|
|
def calver_by_date(version):
|
|
if version.exact and not version.dirty:
|
|
return version.format_with("{tag}")
|
|
# TODO: move the release-X check to a new scheme
|
|
if version.branch is not None and version.branch.startswith("release-"):
|
|
branch_ver = _parse_version_tag(version.branch.split("-")[-1], version.config)
|
|
if branch_ver is not None:
|
|
ver = branch_ver["version"]
|
|
match = date_ver_match(ver)
|
|
if match:
|
|
return ver
|
|
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):
|
|
|
|
if version.exact or version.node is None:
|
|
return version.format_choice(
|
|
"", "+d{time:{time_format}}", time_format=time_format
|
|
)
|
|
else:
|
|
return version.format_choice(
|
|
"+{node}", "+{node}.d{time:{time_format}}", time_format=time_format
|
|
)
|
|
|
|
|
|
def get_local_node_and_date(version):
|
|
return _format_local_with_time(version, time_format="%Y%m%d")
|
|
|
|
|
|
def get_local_node_and_timestamp(version, fmt="%Y%m%d%H%M%S"):
|
|
return _format_local_with_time(version, time_format=fmt)
|
|
|
|
|
|
def get_local_dirty_tag(version):
|
|
return version.format_choice("", "+dirty")
|
|
|
|
|
|
def get_no_local_node(_):
|
|
return ""
|
|
|
|
|
|
def postrelease_version(version):
|
|
if version.exact:
|
|
return version.format_with("{tag}")
|
|
else:
|
|
return version.format_with("{tag}.post{distance}")
|
|
|
|
|
|
def _get_ep(group, name):
|
|
for ep in iter_entry_points(group, name):
|
|
trace("ep found:", ep.name)
|
|
return ep.load()
|
|
|
|
|
|
def _iter_version_schemes(entrypoint, scheme_value, _memo=None):
|
|
if _memo is None:
|
|
_memo = set()
|
|
if isinstance(scheme_value, str):
|
|
scheme_value = _get_ep(entrypoint, scheme_value)
|
|
|
|
if isinstance(scheme_value, (list, tuple)):
|
|
for variant in scheme_value:
|
|
if variant not in _memo:
|
|
_memo.add(variant)
|
|
yield from _iter_version_schemes(entrypoint, variant, _memo=_memo)
|
|
elif callable(scheme_value):
|
|
yield scheme_value
|
|
|
|
|
|
def _call_version_scheme(version, entypoint, given_value, default):
|
|
for scheme in _iter_version_schemes(entypoint, given_value):
|
|
result = scheme(version)
|
|
if result is not None:
|
|
return result
|
|
return default
|
|
|
|
|
|
def format_version(version, **config):
|
|
trace("scm version", version)
|
|
trace("config", config)
|
|
if version.preformatted:
|
|
return version.tag
|
|
main_version = _call_version_scheme(
|
|
version, "setuptools_scm.version_scheme", config["version_scheme"], None
|
|
)
|
|
trace("version", main_version)
|
|
assert main_version is not None
|
|
local_version = _call_version_scheme(
|
|
version, "setuptools_scm.local_scheme", config["local_scheme"], "+unknown"
|
|
)
|
|
trace("local_version", local_version)
|
|
return main_version + local_version
|