wip: add v2 module placeholders

This commit is contained in:
Manuel Barkhau 2020-09-06 20:20:36 +00:00
parent 7962a7cb9f
commit 669e8944e9
27 changed files with 1361 additions and 393 deletions

View file

@ -1,7 +1,7 @@
# This file is part of the pycalver project
# https://gitlab.com/mbarkhau/pycalver
# https://github.com/mbarkhau/pycalver
#
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
"""PyCalVer: CalVer for Python Packages."""

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python
# This file is part of the pycalver project
# https://gitlab.com/mbarkhau/pycalver
# https://github.com/mbarkhau/pycalver
#
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
"""
__main__ module for PyCalVer.

View file

@ -1,8 +1,8 @@
#!/usr/bin/env python
# This file is part of the pycalver project
# https://gitlab.com/mbarkhau/pycalver
# https://github.com/mbarkhau/pycalver
#
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
"""
CLI module for PyCalVer.

View file

@ -234,6 +234,8 @@ def _parse_config(raw_cfg: RawConfig) -> Config:
version_str: str = raw_cfg['current_version']
version_str = raw_cfg['current_version'] = version_str.strip("'\" ")
# TODO (mb 2020-09-06): new style pattern by default
# version_pattern: str = raw_cfg.get('version_pattern', "vYYYY0M.BUILD[-TAG]")
version_pattern: str = raw_cfg.get('version_pattern', "{pycalver}")
version_pattern = raw_cfg['version_pattern'] = version_pattern.strip("'\" ")

View file

@ -1,169 +0,0 @@
# This file is part of the pycalver project
# https://gitlab.com/mbarkhau/pycalver
#
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
"""A scheme for lexically ordered numerical ids.
Throughout the sequence this expression remains true, whether you
are dealing with integers or strings:
older_id < newer_id
The left most character/digit is only used to maintain lexical
order, so that the position in the sequence is maintained in the
remaining digits.
sequence_pos = int(idval[1:], 10)
lexical sequence_pos
0 0
11 1
12 2
...
19 9
220 20
221 21
...
298 98
299 99
3300 300
3301 301
...
3998 998
3999 999
44000 4000
44001 4001
...
899999998 99999998
899999999 99999999
9900000000 900000000
9900000001 900000001
...
9999999998 999999998
9999999999 999999999 # maximum value
You can add leading zeros to delay the expansion and/or increase
the maximum possible value.
lexical sequence_pos
0001 1
0002 2
0003 3
...
0999 999
11000 1000
11001 1001
11002 1002
...
19998 9998
19999 9999
220000 20000
220001 20001
...
899999999998 99999999998
899999999999 99999999999
9900000000000 900000000000
9900000000001 900000000001
...
9999999999998 999999999998
9999999999999 999999999999 # maximum value
This scheme is useful when you just want an ordered sequence of
numbers, but the numbers don't have any particular meaning or
arithmetical relation. The only relation they have to each other
is that numbers generated later in the sequence are greater than
ones generated earlier.
"""
MINIMUM_ID = "0"
def next_id(prev_id: str) -> str:
"""Generate next lexical id.
Increments by one and adds padding if required.
>>> next_id("0098")
'0099'
>>> next_id("0099")
'0100'
>>> next_id("0999")
'11000'
>>> next_id("11000")
'11001'
"""
num_digits = len(prev_id)
if prev_id.count("9") == num_digits:
raise OverflowError("max lexical version reached: " + prev_id)
_prev_id_val = int(prev_id, 10)
_maybe_next_id_val = int(_prev_id_val) + 1
_maybe_next_id_str = f"{_maybe_next_id_val:0{num_digits}}"
_is_padding_ok = prev_id[0] == _maybe_next_id_str[0]
_next_id_str: str
if _is_padding_ok:
_next_id_str = _maybe_next_id_str
else:
_next_id_str = str(_maybe_next_id_val * 11)
return _next_id_str
def ord_val(lex_id: str) -> int:
"""Parse the ordinal value of a lexical id.
The ordinal value is the position in the sequence,
from repeated calls to next_id.
>>> ord_val("0098")
98
>>> ord_val("0099")
99
>>> ord_val("0100")
100
>>> ord_val("11000")
1000
>>> ord_val("11001")
1001
"""
if len(lex_id) == 1:
return int(lex_id, 10)
else:
return int(lex_id[1:], 10)
def _main() -> None:
_curr_id = "01"
print(f"{'lexical':<13} {'numerical':>12}")
while True:
print(f"{_curr_id:<13} {ord_val(_curr_id):>12}")
_next_id = next_id(_curr_id)
if _next_id.count("9") == len(_next_id):
# all nines, we're done
print(f"{_next_id:<13} {ord_val(_next_id):>12}")
break
if _next_id[0] != _curr_id[0] and len(_curr_id) > 1:
print(f"{_next_id:<13} {ord_val(_next_id):>12}")
_next_id = next_id(_next_id)
print(f"{_next_id:<13} {ord_val(_next_id):>12}")
_next_id = next_id(_next_id)
print("...")
# skip ahead
_next_id = _next_id[:1] + "9" * (len(_next_id) - 2) + "8"
_curr_id = _next_id
if __name__ == '__main__':
_main()

View file

@ -1,7 +1,7 @@
# This file is part of the pycalver project
# https://gitlab.com/mbarkhau/pycalver
# https://github.com/mbarkhau/pycalver
#
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
"""Parse PyCalVer strings from files."""
@ -22,6 +22,8 @@ class PatternMatch(typ.NamedTuple):
PatternMatches = typ.Iterable[PatternMatch]
RegexpPatterns = typ.List[typ.Pattern[str]]
def _iter_for_pattern(lines: typ.List[str], pattern: str) -> PatternMatches:
# The pattern is escaped, so that everything besides the format

View file

@ -1,7 +1,7 @@
# This file is part of the pycalver project
# https://gitlab.com/mbarkhau/pycalver
# https://github.com/mbarkhau/pycalver
#
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
"""Compose Regular Expressions from Patterns.

View file

@ -1,7 +1,7 @@
# This file is part of the pycalver project
# https://gitlab.com/mbarkhau/pycalver
# https://github.com/mbarkhau/pycalver
#
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
"""Rewrite files, updating occurences of version strings."""
@ -13,8 +13,9 @@ import logging
import pathlib2 as pl
from . import parse
from . import config
from pycalver import parse
from pycalver import config
from . import version
from . import patterns
@ -53,6 +54,28 @@ class NoPatternMatch(Exception):
"""
class RewrittenFileData(typ.NamedTuple):
"""Container for line-wise content of rewritten files."""
path : str
line_sep : str
old_lines: typ.List[str]
new_lines: typ.List[str]
def iter_file_paths(
file_patterns: config.PatternsByGlob,
) -> typ.Iterable[typ.Tuple[pl.Path, config.Patterns]]:
for globstr, pattern_strs in file_patterns.items():
file_paths = glob.glob(globstr)
if not any(file_paths):
errmsg = f"No files found for path/glob '{globstr}'"
raise IOError(errmsg)
for file_path_str in file_paths:
file_path = pl.Path(file_path_str)
yield (file_path, pattern_strs)
def rewrite_lines(
pattern_strs: typ.List[str], new_vinfo: version.VersionInfo, old_lines: typ.List[str]
) -> typ.List[str]:
@ -88,15 +111,6 @@ def rewrite_lines(
return new_lines
class RewrittenFileData(typ.NamedTuple):
"""Container for line-wise content of rewritten files."""
path : str
line_sep : str
old_lines: typ.List[str]
new_lines: typ.List[str]
def rfd_from_content(
pattern_strs: typ.List[str], new_vinfo: version.VersionInfo, content: str
) -> RewrittenFileData:
@ -122,19 +136,6 @@ def rfd_from_content(
return RewrittenFileData("<path>", line_sep, old_lines, new_lines)
def _iter_file_paths(
file_patterns: config.PatternsByGlob,
) -> typ.Iterable[typ.Tuple[pl.Path, config.Patterns]]:
for globstr, pattern_strs in file_patterns.items():
file_paths = glob.glob(globstr)
if not any(file_paths):
errmsg = f"No files found for path/glob '{globstr}'"
raise IOError(errmsg)
for file_path_str in file_paths:
file_path = pl.Path(file_path_str)
yield (file_path, pattern_strs)
def iter_rewritten(
file_patterns: config.PatternsByGlob, new_vinfo: version.VersionInfo
) -> typ.Iterable[RewrittenFileData]:
@ -144,23 +145,23 @@ def iter_rewritten(
>>> new_vinfo = version.parse_version_info("v201809.0123")
>>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo)
>>> rfd = list(rewritten_datas)[0]
>>> assert rfd.new_lines == [
>>> expected = [
... '# This file is part of the pycalver project',
... '# https://gitlab.com/mbarkhau/pycalver',
... '# https://github.com/mbarkhau/pycalver',
... '#',
... '# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License',
... '# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License',
... '# SPDX-License-Identifier: MIT',
... '"""PyCalVer: CalVer for Python Packages."""',
... '',
... '__version__ = "v201809.0123"',
... '',
... ]
>>>
>>> assert rfd.new_lines[:len(expected)] == expected
'''
fobj: typ.IO[str]
for file_path, pattern_strs in _iter_file_paths(file_patterns):
for file_path, pattern_strs in iter_file_paths(file_patterns):
with file_path.open(mode="rt", encoding="utf-8") as fobj:
content = fobj.read()
@ -204,7 +205,7 @@ def diff(new_vinfo: version.VersionInfo, file_patterns: config.PatternsByGlob) -
full_diff = ""
fobj: typ.IO[str]
for file_path, pattern_strs in sorted(_iter_file_paths(file_patterns)):
for file_path, pattern_strs in sorted(iter_file_paths(file_patterns)):
with file_path.open(mode="rt", encoding="utf-8") as fobj:
content = fobj.read()

View file

@ -1,7 +1,7 @@
# This file is part of the pycalver project
# https://gitlab.com/mbarkhau/pycalver
# https://github.com/mbarkhau/pycalver
#
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
#
# pycalver/vcs.py (this file) is based on code from the
@ -15,11 +15,14 @@ mercurial, then the git terms are used. For example "fetch"
"""
import os
import sys
import typing as typ
import logging
import tempfile
import subprocess as sp
from pycalver import config
logger = logging.getLogger("pycalver.vcs")
@ -179,3 +182,57 @@ def get_vcs_api() -> VCSAPI:
return vcs_api
raise OSError("No such directory .git/ or .hg/ ")
# cli helper methods
def assert_not_dirty(vcs_api: VCSAPI, filepaths: typ.Set[str], allow_dirty: bool) -> None:
dirty_files = vcs_api.status(required_files=filepaths)
if dirty_files:
logger.warning(f"{vcs_api.name} working directory is not clean. Uncomitted file(s):")
for dirty_file in dirty_files:
logger.warning(" " + dirty_file)
if not allow_dirty and dirty_files:
sys.exit(1)
dirty_pattern_files = set(dirty_files) & filepaths
if dirty_pattern_files:
logger.error("Not commiting when pattern files are dirty:")
for dirty_file in dirty_pattern_files:
logger.warning(" " + dirty_file)
sys.exit(1)
def commit(
cfg : config.Config,
vcs_api : VCSAPI,
filepaths : typ.Set[str],
new_version : str,
commit_message: str,
) -> None:
for filepath in filepaths:
vcs_api.add(filepath)
vcs_api.commit(commit_message)
if cfg.commit and cfg.tag:
vcs_api.tag(new_version)
if cfg.commit and cfg.tag and cfg.push:
vcs_api.push(new_version)
def get_tags(fetch: bool) -> typ.List[str]:
try:
vcs_api = get_vcs_api()
logger.debug(f"vcs found: {vcs_api.name}")
if fetch:
logger.info("fetching tags from remote (to turn off use: -n / --no-fetch)")
vcs_api.fetch()
return vcs_api.ls_tags()
except OSError:
logger.debug("No vcs found")
return []

View file

@ -1,7 +1,7 @@
# This file is part of the pycalver project
# https://gitlab.com/mbarkhau/pycalver
# https://github.com/mbarkhau/pycalver
#
# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
"""Functions related to version string manipulation."""
@ -9,9 +9,9 @@ import typing as typ
import logging
import datetime as dt
import lexid
import pkg_resources
from . import lex_id
from . import patterns
logger = logging.getLogger("pycalver.version")
@ -482,7 +482,7 @@ def incr(
else:
logger.warning(f"Version appears to be from the future '{old_version}'")
cur_vinfo = cur_vinfo._replace(bid=lex_id.next_id(cur_vinfo.bid))
cur_vinfo = cur_vinfo._replace(bid=lexid.next_id(cur_vinfo.bid))
if major:
cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0)

View file

@ -0,0 +1,8 @@
# This file is part of the pycalver project
# https://github.com/mbarkhau/pycalver
#
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
"""PyCalVer: CalVer for Python Packages."""
__version__ = "v202007.1036"

53
src/pycalver2/cli.py Normal file
View file

@ -0,0 +1,53 @@
#!/usr/bin/env python
# This file is part of the pycalver project
# https://github.com/mbarkhau/pycalver
#
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
"""
CLI module for PyCalVer.
Provided subcommands: show, test, init, bump
"""
import typing as typ
import logging
import pycalver2.rewrite as v2rewrite
import pycalver2.version as v2version
from pycalver import config
logger = logging.getLogger("pycalver2.cli")
def update_cfg_from_vcs(cfg: config.Config, all_tags: typ.List[str]) -> config.Config:
version_tags = [tag for tag in all_tags if v2version.is_valid(tag, cfg.version_pattern)]
if not version_tags:
logger.debug("no vcs tags found")
return cfg
version_tags.sort(reverse=True)
logger.debug(f"found {len(version_tags)} tags: {version_tags[:2]}")
latest_version_tag = version_tags[0]
latest_version_pep440 = v2version.to_pep440(latest_version_tag)
if latest_version_tag <= cfg.current_version:
return cfg
logger.info(f"Working dir version : {cfg.current_version}")
logger.info(f"Latest version from VCS tag: {latest_version_tag}")
return cfg._replace(
current_version=latest_version_tag,
pep440_version=latest_version_pep440,
)
def rewrite(
cfg : config.Config,
new_version: str,
) -> None:
new_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern)
v2rewrite.rewrite(cfg.file_patterns, new_vinfo)
def get_diff(cfg: config.Config, new_version: str) -> str:
new_vinfo = v2version.parse_version_info(new_version, cfg.version_pattern)
return v2rewrite.diff(new_vinfo, cfg.file_patterns)

205
src/pycalver2/patterns.py Normal file
View file

@ -0,0 +1,205 @@
# This file is part of the pycalver project
# https://github.com/mbarkhau/pycalver
#
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
"""Compose Regular Expressions from Patterns.
>>> version_info = PYCALVER_RE.match("v201712.0123-alpha").groupdict()
>>> assert version_info == {
... "pycalver" : "v201712.0123-alpha",
... "vYYYYMM" : "v201712",
... "year" : "2017",
... "month" : "12",
... "build" : ".0123",
... "build_no" : "0123",
... "release" : "-alpha",
... "release_tag" : "alpha",
... }
>>>
>>> version_info = PYCALVER_RE.match("v201712.0033").groupdict()
>>> assert version_info == {
... "pycalver" : "v201712.0033",
... "vYYYYMM" : "v201712",
... "year" : "2017",
... "month" : "12",
... "build" : ".0033",
... "build_no" : "0033",
... "release" : None,
... "release_tag": None,
... }
"""
import re
import typing as typ
# https://regex101.com/r/fnj60p/10
PYCALVER_PATTERN = r"""
\b
(?P<pycalver>
(?P<vYYYYMM>
v # "v" version prefix
(?P<year>\d{4})
(?P<month>\d{2})
)
(?P<build>
\. # "." build nr prefix
(?P<build_no>\d{4,})
)
(?P<release>
\- # "-" release prefix
(?P<release_tag>alpha|beta|dev|rc|post)
)?
)(?:\s|$)
"""
PYCALVER_RE: typ.Pattern[str] = re.compile(PYCALVER_PATTERN, flags=re.VERBOSE)
PATTERN_ESCAPES = [
("\u005c", "\u005c\u005c"),
("-" , "\u005c-"),
("." , "\u005c."),
("+" , "\u005c+"),
("*" , "\u005c*"),
("?" , "\u005c?"),
("{" , "\u005c{"),
("}" , "\u005c}"),
("[" , "\u005c["),
("]" , "\u005c]"),
("(" , "\u005c("),
(")" , "\u005c)"),
]
# NOTE (mb 2020-09-04): These are depricated in favour of explicit patterns
COMPOSITE_PART_PATTERNS = {
'pep440_pycalver': r"{year}{month}\.{BID}(?:{pep440_tag})?",
'pycalver' : r"v{year}{month}\.{bid}(?:-{tag})?",
'calver' : r"v{year}{month}",
'semver' : r"{MAJOR}\.{MINOR}\.{PATCH}",
'release_tag' : r"{tag}",
'build' : r"\.{bid}",
'release' : r"(?:-{tag})?",
# depricated
'pep440_version': r"{year}{month}\.{BID}(?:{pep440_tag})?",
}
PART_PATTERNS = {
# recommended (based on calver.org)
'YYYY': r"[1-9]\d{3}",
'YY' : r"\d{1,2}",
'0Y' : r"\d{2}",
'Q' : r"[1-4]",
'MM' : r"(?:[1-9]|1[0-2])",
'0M' : r"(?:0[1-9]|1[0-2])",
'DD' : r"([1-9]|[1-2][0-9]|3[0-1])",
'0D' : r"(0[1-9]|[1-2][0-9]|3[0-1])",
'JJJ' : r"(?:[1-9]\d|[1-9]|[1-2]\d\d|3[0-5][0-9]|36[0-6])",
'00J' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])",
'WW' : r"(?:[1-9]|[1-4]\d|5[0-2])",
'0W' : r"(?:[0-4]\d|5[0-2])",
'UU' : r"(?:[1-9]|[0-4]\d|5[0-2])",
'0U' : r"(?:[0-4]\d|5[0-2])",
'VV' : r"(?:[1-9]|[1-4]\d|5[0-3])",
'0V' : r"(?:[0-4]\d|5[0-3])",
'GGGG': r"[1-9]\d{3}",
'GG' : r"\d{1,2}",
'0G' : r"\d{2}",
# non calver parts
'MAJOR': r"\d+",
'MINOR': r"\d+",
'PATCH': r"\d+",
'MICRO': r"\d+",
'BUILD': r"\d+",
'TAG' : r"(?:alpha|beta|dev|rc|post|final)",
'PYTAG': r"(?:a|b|dev|rc|post)?\d*",
# supported (but legacy)
'year' : r"\d{4}",
'month' : r"(?:0[0-9]|1[0-2])",
'month_short': r"(?:1[0-2]|[1-9])",
'build_no' : r"\d{4,}",
'pep440_tag' : r"(?:a|b|dev|rc|post)?\d*",
'tag' : r"(?:alpha|beta|dev|rc|post|final)",
'yy' : r"\d{2}",
'yyyy' : r"\d{4}",
'quarter' : r"[1-4]",
'iso_week' : r"(?:[0-4]\d|5[0-3])",
'us_week' : r"(?:[0-4]\d|5[0-3])",
'dom' : r"(0[1-9]|[1-2][0-9]|3[0-1])",
'dom_short' : r"([1-9]|[1-2][0-9]|3[0-1])",
'doy' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])",
'doy_short' : r"(?:[0-2]\d\d|3[0-5][0-9]|36[0-6])",
'bid' : r"\d{4,}",
# dropped support (never documented)
# 'BID' : r"[1-9]\d*",
# 'MM' : r"\d{2,}",
# 'MMM' : r"\d{3,}",
# 'MMMM' : r"\d{4,}",
# 'MMMMM' : r"\d{5,}",
# 'PP' : r"\d{2,}",
# 'PPP' : r"\d{3,}",
# 'PPPP' : r"\d{4,}",
# 'PPPPP' : r"\d{5,}",
# 'BB' : r"[1-9]\d{1,}",
# 'BBB' : r"[1-9]\d{2,}",
# 'BBBB' : r"[1-9]\d{3,}",
# 'BBBBB' : r"[1-9]\d{4,}",
# 'BBBBBB' : r"[1-9]\d{5,}",
# 'BBBBBBB' : r"[1-9]\d{6,}",
}
FULL_PART_FORMATS = {
'pep440_pycalver': "{year}{month:02}.{BID}{pep440_tag}",
'pycalver' : "v{year}{month:02}.{bid}{release}",
'calver' : "v{year}{month:02}",
'semver' : "{MAJOR}.{MINOR}.{PATCH}",
'release_tag' : "{tag}",
'build' : ".{bid}",
# NOTE (mb 2019-01-04): since release is optional, it
# is treated specially in version.format
# 'release' : "-{tag}",
'month' : "{month:02}",
'month_short': "{month}",
'build_no' : "{bid}",
'iso_week' : "{iso_week:02}",
'us_week' : "{us_week:02}",
'dom' : "{dom:02}",
'doy' : "{doy:03}",
'dom_short' : "{dom}",
'doy_short' : "{doy}",
# depricated
'pep440_version': "{year}{month:02}.{BID}{pep440_tag}",
'version' : "v{year}{month:02}.{bid}{release}",
}
def _replace_pattern_parts(pattern: str) -> str:
for part_name, part_pattern in PART_PATTERNS.items():
named_part_pattern = f"(?P<{part_name}>{part_pattern})"
placeholder = "\u005c{" + part_name + "\u005c}"
pattern = pattern.replace(placeholder, named_part_pattern)
return pattern
def compile_pattern_str(pattern: str) -> str:
for char, escaped in PATTERN_ESCAPES:
pattern = pattern.replace(char, escaped)
return _replace_pattern_parts(pattern)
def compile_pattern(pattern: str) -> typ.Pattern[str]:
pattern_str = compile_pattern_str(pattern)
return re.compile(pattern_str)
def _init_composite_patterns() -> None:
for part_name, part_pattern in COMPOSITE_PART_PATTERNS.items():
part_pattern = part_pattern.replace("{", "\u005c{").replace("}", "\u005c}")
pattern_str = _replace_pattern_parts(part_pattern)
PART_PATTERNS[part_name] = pattern_str
_init_composite_patterns()

175
src/pycalver2/rewrite.py Normal file
View file

@ -0,0 +1,175 @@
# This file is part of the pycalver project
# https://github.com/mbarkhau/pycalver
#
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
"""Rewrite files, updating occurences of version strings."""
import io
import typing as typ
import logging
from pycalver import parse
from pycalver import config
from pycalver import rewrite as v1rewrite
from pycalver2 import version
from pycalver2 import patterns
logger = logging.getLogger("pycalver2.rewrite")
def rewrite_lines(
pattern_strs: typ.List[str], new_vinfo: version.VersionInfo, old_lines: typ.List[str]
) -> typ.List[str]:
"""TODO reenable doctest"""
pass
"""Replace occurances of pattern_strs in old_lines with new_vinfo.
>>> new_vinfo = version.parse_version_info("v201811.0123-beta")
>>> pattern_strs = ['__version__ = "{pycalver}"']
>>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "v201809.0002-beta"'])
['__version__ = "v201811.0123-beta"']
>>> pattern_strs = ['__version__ = "{pep440_version}"']
>>> rewrite_lines(pattern_strs, new_vinfo, ['__version__ = "201809.2b0"'])
['__version__ = "201811.123b0"']
"""
new_lines = old_lines[:]
found_patterns = set()
re_patterns = [patterns.compile_pattern(p) for p in pattern_strs]
for match in parse.iter_matches(old_lines, re_patterns):
found_patterns.add(match.pattern)
replacement = version.format_version(new_vinfo, match.pattern)
span_l, span_r = match.span
new_line = match.line[:span_l] + replacement + match.line[span_r:]
new_lines[match.lineno] = new_line
non_matched_patterns = set(pattern_strs) - found_patterns
if non_matched_patterns:
for non_matched_pattern in non_matched_patterns:
logger.error(f"No match for pattern '{non_matched_pattern}'")
compiled_pattern_str = patterns.compile_pattern_str(non_matched_pattern)
logger.error(f"Pattern compiles to regex '{compiled_pattern_str}'")
raise v1rewrite.NoPatternMatch("Invalid pattern(s)")
else:
return new_lines
def rfd_from_content(
pattern_strs: typ.List[str], new_vinfo: version.VersionInfo, content: str
) -> v1rewrite.RewrittenFileData:
"""TODO reenable doctest"""
pass
r"""Rewrite pattern occurrences with version string.
>>> new_vinfo = version.parse_version_info("v201809.0123")
>>> pattern_strs = ['__version__ = "{pycalver}"']
>>> content = '__version__ = "v201809.0001-alpha"'
>>> rfd = rfd_from_content(pattern_strs, new_vinfo, content)
>>> rfd.new_lines
['__version__ = "v201809.0123"']
>>>
>>> new_vinfo = version.parse_version_info("v1.2.3", "v{semver}")
>>> pattern_strs = ['__version__ = "v{semver}"']
>>> content = '__version__ = "v1.2.2"'
>>> rfd = rfd_from_content(pattern_strs, new_vinfo, content)
>>> rfd.new_lines
['__version__ = "v1.2.3"']
"""
line_sep = v1rewrite.detect_line_sep(content)
old_lines = content.split(line_sep)
new_lines = rewrite_lines(pattern_strs, new_vinfo, old_lines)
return v1rewrite.RewrittenFileData("<path>", line_sep, old_lines, new_lines)
def iter_rewritten(
file_patterns: config.PatternsByGlob, new_vinfo: version.VersionInfo
) -> typ.Iterable[v1rewrite.RewrittenFileData]:
"""TODO reenable doctest"""
pass
r'''Iterate over files with version string replaced.
>>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']}
>>> new_vinfo = version.parse_version_info("v201809.0123")
>>> rewritten_datas = iter_rewritten(file_patterns, new_vinfo)
>>> rfd = list(rewritten_datas)[0]
>>> assert rfd.new_lines == [
... '# This file is part of the pycalver project',
... '# https://gitlab.com/mbarkhau/pycalver',
... '#',
... '# Copyright (c) 2019 Manuel Barkhau (mbarkhau@gmail.com) - MIT License',
... '# SPDX-License-Identifier: MIT',
... '"""PyCalVer: CalVer for Python Packages."""',
... '',
... '__version__ = "v201809.0123"',
... '',
... ]
>>>
'''
fobj: typ.IO[str]
for file_path, pattern_strs in v1rewrite.iter_file_paths(file_patterns):
with file_path.open(mode="rt", encoding="utf-8") as fobj:
content = fobj.read()
rfd = rfd_from_content(pattern_strs, new_vinfo, content)
yield rfd._replace(path=str(file_path))
def diff(new_vinfo: version.VersionInfo, file_patterns: config.PatternsByGlob) -> str:
"""TODO reenable doctest"""
pass
r"""Generate diffs of rewritten files.
>>> new_vinfo = version.parse_version_info("v201809.0123")
>>> file_patterns = {"src/pycalver/__init__.py": ['__version__ = "{pycalver}"']}
>>> diff_str = diff(new_vinfo, file_patterns)
>>> lines = diff_str.split("\n")
>>> lines[:2]
['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py']
>>> assert lines[6].startswith('-__version__ = "v2')
>>> assert not lines[6].startswith('-__version__ = "v201809.0123"')
>>> lines[7]
'+__version__ = "v201809.0123"'
"""
full_diff = ""
fobj: typ.IO[str]
for file_path, pattern_strs in sorted(v1rewrite.iter_file_paths(file_patterns)):
with file_path.open(mode="rt", encoding="utf-8") as fobj:
content = fobj.read()
try:
rfd = rfd_from_content(pattern_strs, new_vinfo, content)
except v1rewrite.NoPatternMatch:
# pylint:disable=raise-missing-from ; we support py2, so not an option
errmsg = f"No patterns matched for '{file_path}'"
raise v1rewrite.NoPatternMatch(errmsg)
rfd = rfd._replace(path=str(file_path))
lines = v1rewrite.diff_lines(rfd)
if len(lines) == 0:
errmsg = f"No patterns matched for '{file_path}'"
raise v1rewrite.NoPatternMatch(errmsg)
full_diff += "\n".join(lines) + "\n"
full_diff = full_diff.rstrip("\n")
return full_diff
def rewrite(file_patterns: config.PatternsByGlob, new_vinfo: version.VersionInfo) -> None:
"""Rewrite project files, updating each with the new version."""
fobj: typ.IO[str]
for file_data in iter_rewritten(file_patterns, new_vinfo):
new_content = file_data.line_sep.join(file_data.new_lines)
with io.open(file_data.path, mode="wt", encoding="utf-8") as fobj:
fobj.write(new_content)

590
src/pycalver2/version.py Normal file
View file

@ -0,0 +1,590 @@
# This file is part of the pycalver project
# https://github.com/mbarkhau/pycalver
#
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
# SPDX-License-Identifier: MIT
"""Functions related to version string manipulation."""
import typing as typ
import logging
import datetime as dt
import lexid
import pkg_resources
from . import patterns
logger = logging.getLogger("pycalver.version")
# The test suite may replace this.
TODAY = dt.datetime.utcnow().date()
PATTERN_PART_FIELDS = {
'YYYY' : 'year_y',
'YY' : 'year_y',
'0Y' : 'year_y',
'Q' : 'quarter',
'MM' : 'month',
'0M' : 'month',
'DD' : 'dom',
'0D' : 'dom',
'JJJ' : 'doy',
'00J' : 'doy',
'MAJOR': 'major',
'MINOR': 'minor',
'PATCH': 'patch',
'MICRO': 'patch',
'BUILD': 'bid',
'TAG' : 'tag',
'PYTAG': 'pytag',
'WW' : 'week_w',
'0W' : 'week_w',
'UU' : 'week_u',
'0U' : 'week_u',
'VV' : 'week_v',
'0V' : 'week_v',
'GGGG' : 'year_g',
'GG' : 'year_g',
'0G' : 'year_g',
}
ID_FIELDS_BY_PART = {
'MAJOR': 'major',
'MINOR': 'minor',
'PATCH': 'patch',
'MICRO': 'patch',
}
ZERO_VALUES = {
'major': "0",
'minor': "0",
'patch': "0",
'TAG' : "final",
'PYTAG': "",
}
class CalendarInfo(typ.NamedTuple):
"""Container for calendar components of version strings."""
year_y : int
year_g : int
quarter: int
month : int
dom : int
doy : int
week_w : int
week_u : int
week_v : int
def _date_from_doy(year: int, doy: int) -> dt.date:
"""Parse date from year and day of year (1 indexed).
>>> cases = [
... (2016, 1), (2016, 31), (2016, 31 + 1), (2016, 31 + 29), (2016, 31 + 30),
... (2017, 1), (2017, 31), (2017, 31 + 1), (2017, 31 + 28), (2017, 31 + 29),
... ]
>>> dates = [_date_from_doy(year, month) for year, month in cases]
>>> assert [(d.month, d.day) for d in dates] == [
... (1, 1), (1, 31), (2, 1), (2, 29), (3, 1),
... (1, 1), (1, 31), (2, 1), (2, 28), (3, 1),
... ]
"""
return dt.date(year, 1, 1) + dt.timedelta(days=doy - 1)
def _quarter_from_month(month: int) -> int:
"""Calculate quarter (1 indexed) from month (1 indexed).
>>> [_quarter_from_month(month) for month in range(1, 13)]
[1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4]
"""
return ((month - 1) // 3) + 1
def cal_info(date: dt.date = None) -> CalendarInfo:
"""TODO reenable doctest"""
pass
"""Generate calendar components for current date.
>>> from datetime import date
>>> c = cal_info(date(2019, 1, 5))
>>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
(2019, 1, 1, 5, 5, 0, 0)
>>> c = cal_info(date(2019, 1, 6))
>>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
(2019, 1, 1, 6, 6, 0, 1)
>>> c = cal_info(date(2019, 1, 7))
>>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
(2019, 1, 1, 7, 7, 1, 1)
>>> c = cal_info(date(2019, 4, 7))
>>> (c.year_y, c.quarter, c.month, c.dom, c.doy, c.iso_week, c.us_week)
(2019, 2, 4, 7, 97, 13, 14)
"""
if date is None:
date = TODAY
kwargs = {
'year_y' : date.year,
'year_g' : int(date.strftime("%G"), base=10),
'quarter': _quarter_from_month(date.month),
'month' : date.month,
'dom' : date.day,
'doy' : int(date.strftime("%j"), base=10),
'week_w' : int(date.strftime("%W"), base=10),
'week_u' : int(date.strftime("%U"), base=10),
'week_v' : int(date.strftime("%V"), base=10),
}
return CalendarInfo(**kwargs)
class VersionInfo(typ.NamedTuple):
"""Container for parsed version string."""
year_y : typ.Optional[int]
year_g : typ.Optional[int]
quarter: typ.Optional[int]
month : typ.Optional[int]
dom : typ.Optional[int]
doy : typ.Optional[int]
week_w : typ.Optional[int]
week_u : typ.Optional[int]
week_v : typ.Optional[int]
major : int
minor : int
patch : int
bid : str
tag : str
pytag : str
FieldKey = str
MatchGroupKey = str
MatchGroupStr = str
PatternGroups = typ.Dict[MatchGroupKey, MatchGroupStr]
FieldValues = typ.Dict[FieldKey , MatchGroupStr]
def _parse_field_values(field_values: FieldValues) -> VersionInfo:
fvals = field_values
tag = fvals.get('tag')
if tag is None:
tag = "final"
tag = TAG_ALIASES.get(tag, tag)
assert tag is not None
# TODO (mb 2020-09-06): parts of course
pytag = "TODO"
bid = fvals['bid'] if 'bid' in fvals else "1001"
year_y = int(fvals['year_y']) if 'year_y' in fvals else None
year_g = int(fvals['year_g']) if 'year_g' in fvals else None
doy = int(fvals['doy' ]) if 'doy' in fvals else None
date: typ.Optional[dt.date] = None
month: typ.Optional[int] = None
dom : typ.Optional[int] = None
week_w: typ.Optional[int] = None
week_u: typ.Optional[int] = None
week_v: typ.Optional[int] = None
if year_y and doy:
date = _date_from_doy(year_y, doy)
month = date.month
dom = date.day
else:
month = int(fvals['month']) if 'month' in fvals else None
dom = int(fvals['dom' ]) if 'dom' in fvals else None
quarter = int(fvals['quarter']) if 'quarter' in fvals else None
if quarter is None and month:
quarter = _quarter_from_month(month)
if year_y and month and dom:
date = dt.date(year_y, month, dom)
if date:
# derive all fields from other previous values
doy = int(date.strftime("%j"), base=10)
week_w = int(date.strftime("%W"), base=10)
week_u = int(date.strftime("%U"), base=10)
week_v = int(date.strftime("%V"), base=10)
year_g = int(date.strftime("%G"), base=10)
major = int(fvals['major']) if 'major' in fvals else 0
minor = int(fvals['minor']) if 'minor' in fvals else 0
patch = int(fvals['patch']) if 'patch' in fvals else 0
return VersionInfo(
year_y=year_y,
year_g=year_g,
quarter=quarter,
month=month,
dom=dom,
doy=doy,
week_w=week_w,
week_u=week_u,
week_v=week_v,
major=major,
minor=minor,
patch=patch,
bid=bid,
tag=tag,
pytag=pytag,
)
def _is_calver(nfo: typ.Union[CalendarInfo, VersionInfo]) -> bool:
"""TODO reenable doctest"""
pass
"""Check pattern for any calendar based parts.
>>> _is_calver(cal_info())
True
>>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0018"})
>>> _is_calver(vnfo)
True
>>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "023", 'PATCH': "45"})
>>> _is_calver(vnfo)
False
"""
for field in CalendarInfo._fields:
maybe_val: typ.Any = getattr(nfo, field, None)
if isinstance(maybe_val, int):
return True
return False
TAG_ALIASES: typ.Dict[str, str] = {
'a' : "alpha",
'b' : "beta",
'pre': "rc",
}
PEP440_TAGS: typ.Dict[str, str] = {
'alpha': "a",
'beta' : "b",
'final': "",
'rc' : "rc",
'dev' : "dev",
'post' : "post",
}
VersionInfoKW = typ.Dict[str, typ.Union[str, int, None]]
class PatternError(Exception):
pass
def _parse_pattern_groups(pattern_groups: PatternGroups) -> FieldValues:
for part_name in pattern_groups.keys():
is_valid_part_name = (
part_name in patterns.COMPOSITE_PART_PATTERNS or part_name in PATTERN_PART_FIELDS
)
if not is_valid_part_name:
err_msg = f"Invalid part '{part_name}'"
raise PatternError(err_msg)
field_value_items = [
(field_name, pattern_groups[part_name])
for part_name, field_name in PATTERN_PART_FIELDS.items()
if part_name in pattern_groups.keys()
]
all_fields = [field_name for field_name, _ in field_value_items]
unique_fields = set(all_fields)
duplicate_fields = [f for f in unique_fields if all_fields.count(f) > 1]
if any(duplicate_fields):
err_msg = f"Multiple parts for same field {duplicate_fields}."
raise PatternError(err_msg)
return dict(field_value_items)
def _parse_version_info(pattern_groups: PatternGroups) -> VersionInfo:
"""TODO reenable doctest"""
pass
"""Parse normalized VersionInfo from groups of a matched pattern.
>>> vnfo = _parse_version_info({'year': "2018", 'month': "11", 'bid': "0099"})
>>> (vnfo.year_y, vnfo.month, vnfo.quarter, vnfo.bid, vnfo.tag)
(2018, 11, 4, '0099', 'final')
>>> vnfo = _parse_version_info({'year': "2018", 'doy': "11", 'bid': "099", 'tag': "b"})
>>> (vnfo.year_y, vnfo.month, vnfo.dom, vnfo.bid, vnfo.tag)
(2018, 1, 11, '099', 'beta')
>>> vnfo = _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "45"})
>>> (vnfo.major, vnfo.minor, vnfo.patch)
(1, 23, 45)
>>> vnfo = _parse_version_info({'MAJOR': "1", 'MMM': "023", 'PPPP': "0045"})
>>> (vnfo.major, vnfo.minor, vnfo.patch)
(1, 23, 45)
"""
field_values = _parse_pattern_groups(pattern_groups)
return _parse_field_values(field_values)
def parse_version_info(version_str: str, pattern: str = "{pycalver}") -> VersionInfo:
"""TODO reenable doctest"""
pass
"""Parse normalized VersionInfo.
>>> vnfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}")
>>> assert vnfo == _parse_version_info({'year': 2017, 'month': 12, 'bid': "0033", 'tag': "beta"})
>>> vnfo = parse_version_info("1.23.456", pattern="{semver}")
>>> assert vnfo == _parse_version_info({'MAJOR': "1", 'MINOR': "23", 'PATCH': "456"})
"""
regex = patterns.compile_pattern(pattern)
match = regex.match(version_str)
if match is None:
err_msg = (
f"Invalid version string '{version_str}' for pattern '{pattern}'/'{regex.pattern}'"
)
raise PatternError(err_msg)
return _parse_version_info(match.groupdict())
def is_valid(version_str: str, pattern: str = "{pycalver}") -> bool:
"""TODO reenable doctest"""
pass
"""Check if a version matches a pattern.
>>> is_valid("v201712.0033-beta", pattern="{pycalver}")
True
>>> is_valid("v201712.0033-beta", pattern="{semver}")
False
>>> is_valid("1.2.3", pattern="{semver}")
True
>>> is_valid("v201712.0033-beta", pattern="{semver}")
False
"""
try:
parse_version_info(version_str, pattern)
return True
except PatternError:
return False
TemplateKwargs = typ.Dict[str, typ.Union[str, int, None]]
def _derive_template_kwargs(vinfo: VersionInfo) -> TemplateKwargs:
"""Generate kwargs for template from minimal VersionInfo.
The VersionInfo Tuple only has the minimal representation
of a parsed version, not the values suitable for formatting.
It may for example have month=9, but not the formatted
representation '09' for '0M'.
"""
kwargs: TemplateKwargs = vinfo._asdict()
tag = vinfo.tag
kwargs['TAG'] = tag
if tag == 'final':
kwargs['PYTAG'] = ""
else:
kwargs['PYTAG'] = PEP440_TAGS[tag] + "0"
year_y = vinfo.year_y
if year_y:
kwargs['0Y' ] = str(year_y)[-2:]
kwargs['YY' ] = int(str(year_y)[-2:])
kwargs['YYYY'] = year_y
year_g = vinfo.year_g
if year_g:
kwargs['0G' ] = str(year_g)[-2:]
kwargs['GG' ] = int(str(year_g)[-2:])
kwargs['GGGG'] = year_g
kwargs['BUILD'] = int(vinfo.bid, 10)
for part_name, field in ID_FIELDS_BY_PART.items():
val = kwargs[field]
if part_name.lower() == field.lower():
if isinstance(val, str):
kwargs[part_name] = int(val, base=10)
else:
kwargs[part_name] = val
else:
assert len(set(part_name)) == 1
padded_len = len(part_name)
kwargs[part_name] = str(val).zfill(padded_len)
return kwargs
def _compile_format_template(pattern: str, kwargs: TemplateKwargs) -> str:
# NOTE (mb 2020-09-04): Some parts are optional, we need the kwargs to
# determine if part is set to its zero value
format_tmpl = pattern
for part_name, full_part_format in patterns.FULL_PART_FORMATS.items():
format_tmpl = format_tmpl.replace("{" + part_name + "}", full_part_format)
return format_tmpl
def format_version(vinfo: VersionInfo, pattern: str) -> str:
"""TODO reenable doctest"""
pass
"""Generate version string.
>>> import datetime as dt
>>> vinfo = parse_version_info("v201712.0033-beta", pattern="{pycalver}")
>>> vinfo_a = vinfo._replace(**cal_info(date=dt.date(2017, 1, 1))._asdict())
>>> vinfo_b = vinfo._replace(**cal_info(date=dt.date(2017, 12, 31))._asdict())
>>> format_version(vinfo_a, pattern="v{yy}.{BID}{release}")
'v17.33-beta'
>>> format_version(vinfo_a, pattern="vYY.BUILD[-TAG]")
'v17.33-beta'
>>> format_version(vinfo_a, pattern="YYYY0M.BUILD[PYTAG]")
'201701.33b0'
>>> format_version(vinfo_a, pattern="{pycalver}")
'v201701.0033-beta'
>>> format_version(vinfo_b, pattern="{pycalver}")
'v201712.0033-beta'
>>> format_version(vinfo_a, pattern="v{year}w{iso_week}.{BID}{release}")
'v2017w00.33-beta'
>>> format_version(vinfo_a, pattern="vYYYYwWW.BUILD[-TAG]")
'v2017w00.33-beta'
>>> format_version(vinfo_b, pattern="v{year}w{iso_week}.{BID}{release}")
'v2017w52.33-beta'
>>> format_version(vinfo_b, pattern="vYYYYwWW.BUILD[-TAG]")
'v2017w52.33-beta'
>>> format_version(vinfo_a, pattern="v{year}d{doy}.{bid}{release}")
'v2017d001.0033-beta'
>>> format_version(vinfo_b, pattern="v{year}d{doy}.{bid}{release}")
'v2017d365.0033-beta'
>>> format_version(vinfo_a, pattern="vYYYYdJJJ.BUILD[-TAG]")
'v2017d001.0033-beta'
>>> format_version(vinfo_b, pattern="vYYYYdJJJ.BUILD[-TAG]")
'v2017d365.0033-beta'
>>> format_version(vinfo_a, pattern="vGGGGwVV.BUILD[-TAG]")
'v2016w52.0033-beta'
>>> vinfo_c = vinfo_b._replace(major=1, minor=2, patch=34, tag='final')
>>> format_version(vinfo_c, pattern="v{year}w{iso_week}.{BID}-{tag}")
'v2017w52.33-final'
>>> format_version(vinfo_c, pattern="v{year}w{iso_week}.{BID}{release}")
'v2017w52.33'
>>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD-TAG")
'v2017w52.33-final'
>>> format_version(vinfo_c, pattern="vYYYYwWW.BUILD[-TAG]")
'v2017w52.33'
>>> format_version(vinfo_c, pattern="v{MAJOR}.{MINOR}.{PATCH}")
'v1.2.34'
>>> format_version(vinfo_c, pattern="vMAJOR.MINOR.PATCH")
'v1.2.34'
>>> vinfo_d = vinfo_b._replace(major=1, minor=0, patch=0, tag='final')
>>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH-TAG")
'v1.0.0-final'
>>> format_version(vinfo_d, pattern="vMAJOR.MINOR.PATCH[-TAG]")
'v1.0.0'
>>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.PATCH[-TAG]]")
'v1.0'
>>> format_version(vinfo_d, pattern="vMAJOR.MINOR[.MICRO[-TAG]]")
'v1.0'
>>> format_version(vinfo_d, pattern="vMAJOR[.MINOR[.PATCH[-TAG]]]")
'v1'
"""
kwargs = _derive_template_kwargs(vinfo)
format_tmpl = _compile_format_template(pattern, kwargs)
return format_tmpl.format(**kwargs)
def incr(
old_version: str,
pattern : str = "{pycalver}",
*,
release: str = None,
major : bool = False,
minor : bool = False,
patch : bool = False,
) -> typ.Optional[str]:
"""Increment version string.
'old_version' is assumed to be a string that matches 'pattern'
"""
try:
old_vinfo = parse_version_info(old_version, pattern)
except PatternError as ex:
logger.error(str(ex))
return None
cur_vinfo = old_vinfo
cur_cal_nfo = cal_info()
old_date = (old_vinfo.year_y or 0, old_vinfo.month or 0, old_vinfo.dom or 0)
cur_date = (cur_cal_nfo.year_y , cur_cal_nfo.month , cur_cal_nfo.dom)
if old_date <= cur_date:
cur_vinfo = cur_vinfo._replace(**cur_cal_nfo._asdict())
else:
logger.warning(f"Version appears to be from the future '{old_version}'")
cur_vinfo = cur_vinfo._replace(bid=lexid.incr(cur_vinfo.bid))
if major:
cur_vinfo = cur_vinfo._replace(major=cur_vinfo.major + 1, minor=0, patch=0)
if minor:
cur_vinfo = cur_vinfo._replace(minor=cur_vinfo.minor + 1, patch=0)
if patch:
cur_vinfo = cur_vinfo._replace(patch=cur_vinfo.patch + 1)
if release:
cur_vinfo = cur_vinfo._replace(tag=release)
new_version = format_version(cur_vinfo, pattern)
if new_version == old_version:
logger.error("Invalid arguments or pattern, version did not change.")
return None
else:
return new_version
def to_pep440(version: str) -> str:
"""Derive pep440 compliant version string from PyCalVer version string.
>>> to_pep440("v201811.0007-beta")
'201811.7b0'
"""
return str(pkg_resources.parse_version(version))