bumpver/src/pycalver/rewrite.py

244 lines
8 KiB
Python
Raw Normal View History

2018-09-02 21:48:12 +02:00
# This file is part of the pycalver project
2020-09-06 20:20:36 +00:00
# https://github.com/mbarkhau/pycalver
2018-09-02 21:48:12 +02:00
#
2020-09-06 20:20:36 +00:00
# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License
2018-09-02 21:48:12 +02:00
# SPDX-License-Identifier: MIT
2018-11-15 22:16:16 +01:00
"""Rewrite files, updating occurences of version strings."""
2018-09-02 21:48:12 +02:00
2018-09-03 22:23:51 +02:00
import io
2019-02-21 15:41:06 +01:00
import glob
2020-05-25 07:46:30 +00:00
import typing as typ
2018-09-02 21:48:12 +02:00
import difflib
2018-09-03 22:23:51 +02:00
import logging
2020-05-25 07:46:30 +00:00
2019-02-21 15:41:06 +01:00
import pathlib2 as pl
2018-09-03 22:23:51 +02:00
2020-09-06 20:20:36 +00:00
from pycalver import parse
from pycalver import config
2018-12-09 14:49:13 +01:00
from . import version
from . import patterns
2018-09-02 21:48:12 +02:00
2020-07-19 13:18:42 +00:00
logger = logging.getLogger("pycalver.rewrite")
2018-09-02 21:48:12 +02:00
2018-11-15 22:16:16 +01:00
def detect_line_sep(content: str) -> str:
r"""Parse line separator from content.
>>> detect_line_sep('\r\n')
'\r\n'
>>> detect_line_sep('\r')
'\r'
>>> detect_line_sep('\n')
'\n'
>>> detect_line_sep('')
'\n'
"""
2018-11-11 15:40:16 +01:00
if "\r\n" in content:
return "\r\n"
elif "\r" in content:
return "\r"
else:
return "\n"
class NoPatternMatch(Exception):
"""Pattern not found in content.
2020-07-19 13:18:42 +00:00
logger.error is used to show error info about the patterns so
that users can debug what is wrong with them. The class
itself doesn't capture that info. This approach is used so
that all patter issues can be shown, rather than bubbling
all the way up the stack on the very first pattern with no
matches.
"""
2020-09-06 20:20:36 +00:00
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)
2018-11-04 21:11:42 +01:00
def rewrite_lines(
2020-09-06 21:44:23 +00:00
pattern_strs: typ.List[str],
new_vinfo : version.VersionInfo,
old_lines : typ.List[str],
2018-11-04 21:11:42 +01:00
) -> typ.List[str]:
"""Replace occurances of pattern_strs in old_lines with new_vinfo.
2018-11-15 22:16:16 +01:00
>>> 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"']
2018-11-15 22:16:16 +01:00
"""
new_lines = old_lines[:]
found_patterns = set()
2018-11-04 21:11:42 +01:00
2020-07-19 14:38:57 +00:00
for match in parse.iter_matches(old_lines, pattern_strs):
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
2018-11-04 21:11:42 +01:00
non_matched_patterns = set(pattern_strs) - found_patterns
if non_matched_patterns:
for non_matched_pattern in non_matched_patterns:
2020-07-19 13:18:42 +00:00
logger.error(f"No match for pattern '{non_matched_pattern}'")
2020-07-19 19:07:30 +00:00
compiled_pattern_str = patterns.compile_pattern_str(non_matched_pattern)
logger.error(f"Pattern compiles to regex '{compiled_pattern_str}'")
raise NoPatternMatch("Invalid pattern(s)")
else:
return new_lines
2018-11-04 21:11:42 +01:00
def rfd_from_content(
2020-09-06 21:44:23 +00:00
pattern_strs: typ.List[str],
new_vinfo : version.VersionInfo,
content : str,
) -> RewrittenFileData:
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"']
2019-03-29 21:25:13 +01:00
>>>
>>> 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 = detect_line_sep(content)
old_lines = content.split(line_sep)
new_lines = rewrite_lines(pattern_strs, new_vinfo, old_lines)
return RewrittenFileData("<path>", line_sep, old_lines, new_lines)
def iter_rewritten(
2020-09-06 21:44:23 +00:00
file_patterns: config.PatternsByGlob,
new_vinfo : version.VersionInfo,
) -> typ.Iterable[RewrittenFileData]:
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]
2020-09-06 20:20:36 +00:00
>>> expected = [
... '# This file is part of the pycalver project',
2020-09-06 20:20:36 +00:00
... '# https://github.com/mbarkhau/pycalver',
... '#',
2020-09-06 20:20:36 +00:00
... '# Copyright (c) 2018-2020 Manuel Barkhau (mbarkhau@gmail.com) - MIT License',
... '# SPDX-License-Identifier: MIT',
2019-02-15 00:17:46 +01:00
... '"""PyCalVer: CalVer for Python Packages."""',
... '',
... '__version__ = "v201809.0123"',
... '',
... ]
2020-09-06 20:20:36 +00:00
>>> assert rfd.new_lines[:len(expected)] == expected
'''
2020-07-19 14:38:57 +00:00
fobj: typ.IO[str]
2019-01-07 17:30:02 +01:00
2020-09-06 20:20:36 +00:00
for file_path, pattern_strs in iter_file_paths(file_patterns):
2020-07-19 14:38:57 +00:00
with file_path.open(mode="rt", encoding="utf-8") as fobj:
content = fobj.read()
rfd = rfd_from_content(pattern_strs, new_vinfo, content)
2019-02-21 15:41:06 +01:00
yield rfd._replace(path=str(file_path))
def diff_lines(rfd: RewrittenFileData) -> typ.List[str]:
r"""Generate unified diff.
>>> rfd = RewrittenFileData(
... path = "<path>",
... line_sep = "\n",
... old_lines = ["foo"],
... new_lines = ["bar"],
... )
>>> diff_lines(rfd)
['--- <path>', '+++ <path>', '@@ -1 +1 @@', '-foo', '+bar']
"""
lines = difflib.unified_diff(
a=rfd.old_lines, b=rfd.new_lines, lineterm="", fromfile=rfd.path, tofile=rfd.path
)
return list(lines)
2018-11-15 22:16:16 +01:00
def diff(new_vinfo: version.VersionInfo, file_patterns: config.PatternsByGlob) -> str:
2018-11-15 22:16:16 +01:00
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]
2018-11-15 22:16:16 +01:00
['--- src/pycalver/__init__.py', '+++ src/pycalver/__init__.py']
>>> assert lines[6].startswith('-__version__ = "v2')
>>> assert not lines[6].startswith('-__version__ = "v201809.0123"')
>>> lines[7]
2018-11-15 22:16:16 +01:00
'+__version__ = "v201809.0123"'
"""
full_diff = ""
2020-07-19 14:38:57 +00:00
fobj: typ.IO[str]
2019-01-07 17:30:02 +01:00
2020-09-06 20:20:36 +00:00
for file_path, pattern_strs in sorted(iter_file_paths(file_patterns)):
2020-07-19 14:38:57 +00:00
with file_path.open(mode="rt", encoding="utf-8") as fobj:
content = fobj.read()
2019-02-22 11:04:53 +01:00
try:
rfd = rfd_from_content(pattern_strs, new_vinfo, content)
except NoPatternMatch:
2020-08-26 22:26:30 +00:00
# pylint:disable=raise-missing-from ; we support py2, so not an option
2019-02-22 11:04:53 +01:00
errmsg = f"No patterns matched for '{file_path}'"
raise NoPatternMatch(errmsg)
2019-02-22 11:04:53 +01:00
rfd = rfd._replace(path=str(file_path))
2019-02-21 16:30:27 +01:00
lines = diff_lines(rfd)
if len(lines) == 0:
errmsg = f"No patterns matched for '{file_path}'"
raise NoPatternMatch(errmsg)
2019-02-21 16:30:27 +01:00
full_diff += "\n".join(lines) + "\n"
2018-11-15 22:16:16 +01:00
full_diff = full_diff.rstrip("\n")
return full_diff
2018-11-15 22:16:16 +01:00
2019-07-25 10:55:31 +02:00
def rewrite(file_patterns: config.PatternsByGlob, new_vinfo: version.VersionInfo) -> None:
2018-11-15 22:16:16 +01:00
"""Rewrite project files, updating each with the new version."""
2020-07-19 14:38:57 +00:00
fobj: typ.IO[str]
2018-11-15 22:16:16 +01:00
for file_data in iter_rewritten(file_patterns, new_vinfo):
2018-11-15 22:16:16 +01:00
new_content = file_data.line_sep.join(file_data.new_lines)
2020-07-19 14:38:57 +00:00
with io.open(file_data.path, mode="wt", encoding="utf-8") as fobj:
fobj.write(new_content)