bumpver/src/pycalver/parse.py

109 lines
3.2 KiB
Python
Raw Normal View History

2018-09-02 21:48:12 +02:00
# This file is part of the pycalver project
# https://github.com/mbarkhau/pycalver
#
2018-11-06 21:45:33 +01:00
# Copyright (c) 2018 Manuel Barkhau (@mbarkhau) - MIT License
2018-09-02 21:48:12 +02:00
# SPDX-License-Identifier: MIT
2018-12-09 14:49:13 +01:00
"""Parse PyCalVer strings from files."""
2018-09-02 21:48:12 +02:00
import re
import logging
import typing as typ
log = logging.getLogger("pycalver.parse")
VALID_RELEASE_VALUES = ("alpha", "beta", "dev", "rc", "post", "final")
2018-09-02 21:48:12 +02:00
2018-11-06 21:45:33 +01:00
PATTERN_ESCAPES = [
("\u005c", "\u005c\u005c"),
("-" , "\u005c-"),
("." , "\u005c."),
("+" , "\u005c+"),
("*" , "\u005c*"),
2018-12-20 15:28:11 +01:00
("{" , "\u005c{{"),
("}" , "\u005c}}"),
2018-11-06 21:45:33 +01:00
("[" , "\u005c["),
2018-12-20 15:28:11 +01:00
("]" , "\u005c]"),
2018-11-06 21:45:33 +01:00
("(" , "\u005c("),
2018-12-20 15:28:11 +01:00
(")" , "\u005c)"),
2018-11-06 21:45:33 +01:00
]
2018-09-02 21:48:12 +02:00
2018-09-03 09:19:27 +02:00
# NOTE (mb 2018-09-03): These are matchers for parts, which are
# used in the patterns, they're not for validation. This means
# that they may find strings, which are not valid pycalver
# strings, when parsed in their full context. For such cases,
# the patterns should be expanded.
2018-09-02 21:48:12 +02:00
RE_PATTERN_PARTS = {
2018-11-06 21:45:33 +01:00
'pep440_version': r"\d{6}\.[1-9]\d*(a|b|dev|rc|post)?\d*",
'version' : r"v\d{6}\.\d{4,}(\-(alpha|beta|dev|rc|post|final))?",
2018-11-06 21:45:33 +01:00
'calver' : r"v\d{6}",
'year' : r"\d{4}",
'month' : r"\d{2}",
2018-11-06 21:45:33 +01:00
'build' : r"\.\d{4,}",
'build_no' : r"\d{4,}",
'release' : r"(\-(alpha|beta|dev|rc|post|final))?",
'release_tag' : r"(alpha|beta|dev|rc|post|final)?",
2018-09-02 21:48:12 +02:00
}
class PatternMatch(typ.NamedTuple):
2018-11-15 22:16:16 +01:00
"""Container to mark a version string in a file."""
2018-09-02 21:48:12 +02:00
2018-11-06 21:45:33 +01:00
lineno : int # zero based
line : str
pattern: str
span : typ.Tuple[int, int]
match : str
2018-09-02 21:48:12 +02:00
PatternMatches = typ.Iterable[PatternMatch]
2018-12-21 19:17:58 +01:00
def compile_pattern(pattern: str) -> typ.Pattern[str]:
pattern_tmpl = pattern
for char, escaped in PATTERN_ESCAPES:
pattern_tmpl = pattern_tmpl.replace(char, escaped)
2018-12-20 15:28:11 +01:00
# undo escaping only for valid part names
for part_name in RE_PATTERN_PARTS.keys():
2018-12-21 19:17:58 +01:00
pattern_tmpl = pattern_tmpl.replace(
"\u005c{{" + part_name + "\u005c}}", "{" + part_name + "}"
)
2018-12-20 15:28:11 +01:00
pattern_str = pattern_tmpl.format(**RE_PATTERN_PARTS)
2018-12-20 15:28:11 +01:00
return re.compile(pattern_str)
def _iter_for_pattern(lines: typ.List[str], pattern: str) -> PatternMatches:
# The pattern is escaped, so that everything besides the format
# string variables is treated literally.
pattern_re = compile_pattern(pattern)
for lineno, line in enumerate(lines):
match = pattern_re.search(line)
if match:
yield PatternMatch(lineno, line, pattern, match.span(), match.group(0))
def iter_matches(lines: typ.List[str], patterns: typ.List[str]) -> PatternMatches:
"""Iterate over all matches of any pattern on any line.
>>> lines = ["__version__ = 'v201712.0002-alpha'"]
>>> patterns = ["{version}", "{pep440_version}"]
>>> matches = list(iter_matches(lines, patterns))
>>> assert matches[0] == PatternMatch(
... lineno = 0,
... line = "__version__ = 'v201712.0002-alpha'",
... pattern= "{version}",
... span = (15, 33),
... match = "v201712.0002-alpha",
... )
"""
for pattern in patterns:
for match in _iter_for_pattern(lines, pattern):
yield match