diff --git a/CHANGELOG.md b/CHANGELOG.md index 8518113..36f2886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,12 @@ - Better support for week numbering. - Better support for optional parts. - New: Start `BUILD` parts at `1000` to avoid leading zero truncation. + - New gitlab #2: Added `grep` subcommand to find and debug patterns. - New gitlab #10: `--pin-date` to keep date parts unchanged, and only increment non-date parts. - New add `--release-num` to increment the `alphaN`/`betaN`/`a0`/`b0`/etc. release number - Fix gitlab #8: Push tags only pushed tags, not actual commit. - Fix gitlab #9: Make commit message configurable. + - Fix gitlab #11: Show regexp when `--verbose` is used. - Switch main repo from gitlab to github. diff --git a/pylint-ignore.md b/pylint-ignore.md index 493356b..e0098a1 100644 --- a/pylint-ignore.md +++ b/pylint-ignore.md @@ -63,7 +63,7 @@ The recommended approach to using `pylint-ignore` is: ``` -## File src/pycalver/config.py - Line 273 - W0511 (fixme) +## File src/pycalver/config.py - Line 275 - W0511 (fixme) - `message: TODO (mb 2020-09-18): Validate Pattern` - `author : Manuel Barkhau ` @@ -72,64 +72,64 @@ The recommended approach to using `pylint-ignore` is: ``` 251: def _parse_config(raw_cfg: RawConfig) -> Config: ... - 271: ) - 272: -> 273: # TODO (mb 2020-09-18): Validate Pattern - 274: # detect YY with WW or UU -> suggest GG with VV - 275: # detect YYMM -> suggest YY0M + 273: raise ValueError(f"Invalid week number pattern: {version_pattern}") + 274: +> 275: # TODO (mb 2020-09-18): Validate Pattern + 276: # detect YY with WW or UU -> suggest GG with VV + 277: # detect YYMM -> suggest YY0M ``` -## File src/pycalver/__main__.py - Line 300 - W0511 (fixme) +## File src/pycalver/__main__.py - Line 317 - W0511 (fixme) - `message: TODO (mb 2020-09-18): Investigate error messages` - `author : Manuel Barkhau ` - `date : 2020-09-19T16:24:10` ``` - 270: def _bump( + 287: def _bump( ... - 298: sys.exit(1) - 299: except Exception as ex: -> 300: # TODO (mb 2020-09-18): Investigate error messages - 301: logger.error(str(ex)) - 302: sys.exit(1) + 315: sys.exit(1) + 316: except Exception as ex: +> 317: # TODO (mb 2020-09-18): Investigate error messages + 318: logger.error(str(ex)) + 319: sys.exit(1) ``` -## File src/pycalver/v2version.py - Line 617 - W0511 (fixme) +## File src/pycalver/v2version.py - Line 641 - W0511 (fixme) - `message: TODO (mb 2020-09-20): New Rollover Behaviour:` - `author : Manuel Barkhau ` - `date : 2020-09-20T17:36:38` ``` - 578: def incr( + 599: def incr( ... - 615: ) - 616: -> 617: # TODO (mb 2020-09-20): New Rollover Behaviour: - 618: # Reset major, minor, patch to zero if any part to the left of it is incremented - 619: + 639: ) + 640: +> 641: # TODO (mb 2020-09-20): New Rollover Behaviour: + 642: # Reset major, minor, patch to zero if any part to the left of it is incremented + 643: ``` # W0703: broad-except -## File src/pycalver/__main__.py - Line 299 - W0703 (broad-except) +## File src/pycalver/__main__.py - Line 316 - W0703 (broad-except) - `message: Catching too general exception Exception` - `author : Manuel Barkhau ` - `date : 2020-09-05T14:30:17` ``` - 270: def _bump( + 287: def _bump( ... - 297: logger.error(str(ex)) - 298: sys.exit(1) -> 299: except Exception as ex: - 300: # TODO (mb 2020-09-18): Investigate error messages - 301: logger.error(str(ex)) + 314: logger.error(str(ex)) + 315: sys.exit(1) +> 316: except Exception as ex: + 317: # TODO (mb 2020-09-18): Investigate error messages + 318: logger.error(str(ex)) ``` diff --git a/requirements/pypi.txt b/requirements/pypi.txt index 9dcff45..c3400e8 100644 --- a/requirements/pypi.txt +++ b/requirements/pypi.txt @@ -12,3 +12,4 @@ typing; python_version < "3.5" click toml lexid +colorama>=0.4 diff --git a/src/pycalver/__main__.py b/src/pycalver/__main__.py index 3c370de..26d3bb8 100755 --- a/src/pycalver/__main__.py +++ b/src/pycalver/__main__.py @@ -9,6 +9,7 @@ __main__ module for PyCalVer. Enables use as module: $ python -m pycalver --version """ +import io import sys import typing as typ import logging @@ -16,6 +17,7 @@ import datetime as dt import subprocess as sp import click +import colorama from . import vcs from . import v1cli @@ -23,6 +25,8 @@ from . import v2cli from . import config from . import rewrite from . import version +from . import patterns +from . import regexfmt from . import v1rewrite from . import v1version from . import v2rewrite @@ -149,7 +153,7 @@ def test( ) -> None: """Increment a version number for demo purposes.""" _configure_logging(verbose=max(_VERBOSE, verbose)) - raw_pattern = pattern + raw_pattern = pattern # use internal naming convention _validate_release_tag(release) _date = _validate_date(date, pin_date) @@ -491,5 +495,108 @@ def bump( _try_bump(cfg, new_version, commit_message, allow_dirty) +def _grep_text(pattern: patterns.Pattern, text: str, color: bool) -> int: + match_count = 0 + all_lines = text.splitlines() + for match in pattern.regexp.finditer(text): + match_count += 1 + match_start, match_end = match.span() + + line_idx = text[:match_start].count("\n") + line_start = text.rfind("\n", 0, match_start) + 1 + line_end = text.find("\n", match_end, -1) + if color: + matched_line = ( + text[line_start:match_start] + + colorama.Style.BRIGHT + + text[match_start:match_end] + + colorama.Style.RESET_ALL + + text[match_end:line_end] + ) + else: + matched_line = ( + text[line_start:match_start] + + text[match_start:match_end] + + text[match_end:line_end] + ) + + lines_offset = max(0, line_idx - 1) + 1 + lines = all_lines[line_idx - 1 : line_idx + 2] + + if line_idx == 0: + lines[0] = matched_line + else: + lines[1] = matched_line + + for i, line in enumerate(lines): + print(f"{lines_offset + i:>4}: {line}") + + print() + return match_count + + +def _grep( + raw_pattern: str, + file_ios : typ.Tuple[io.TextIOWrapper], + color : bool, +) -> None: + pattern = v2patterns.compile_pattern(raw_pattern) + + match_count = 0 + for file_io in file_ios: + text = file_io.read() + + _match_count = _grep_text(pattern, text, color) + + print() + print(f"Found {_match_count} match for pattern '{raw_pattern}' in {file_io.name}") + print() + + match_count += _match_count + + if match_count == 0 or _VERBOSE: + pyexpr_regex = regexfmt.pyexpr_regex(pattern.regexp.pattern) + + print(f"# pycalver pattern: '{raw_pattern}'") + print("# " + regexfmt.regex101_url(pattern)) + print(pyexpr_regex) + print() + + if match_count == 0: + sys.exit(1) + + +@cli.command() +@click.option( + "-v", + "--verbose", + count=True, + help="Control log level. -vv for debug level.", +) +@click.argument("pattern") +@click.argument('files', nargs=-1, type=click.File('r')) +def grep( + pattern: str, + files : typ.Tuple[io.TextIOWrapper], + verbose: int = 0, +) -> None: + """Search files for a version pattern.""" + verbose = max(_VERBOSE, verbose) + _configure_logging(verbose) + + raw_pattern = pattern # use internal naming convention + + isatty = getattr(sys.stdout, 'isatty', lambda: False) + + if isatty(): + colorama.init() + try: + _grep(raw_pattern, files, color=True) + finally: + colorama.deinit() + else: + _grep(raw_pattern, files, color=False) + + if __name__ == '__main__': cli() diff --git a/src/pycalver/pysix.py b/src/pycalver/pysix.py new file mode 100644 index 0000000..36e77e9 --- /dev/null +++ b/src/pycalver/pysix.py @@ -0,0 +1,42 @@ +import sys +import typing as typ + +PY2 = sys.version < "3" + + +try: + from urllib.parse import quote as py3_stdlib_quote +except ImportError: + from urllib import quote as py2_stdlib_quote # type: ignore + + +# NOTE (mb 2016-05-23): quote in python2 expects bytes argument. + + +def quote( + string : str, + safe : str = "/", + encoding: typ.Optional[str] = None, + errors : typ.Optional[str] = None, +) -> str: + if not isinstance(string, str): + errmsg = f"Expected str/unicode but got {type(string)}" # type: ignore + raise TypeError(errmsg) + + if encoding is None: + _encoding = "utf-8" + else: + _encoding = encoding + + if errors is None: + _errors = "strict" + else: + _errors = errors + + if PY2: + data = string.encode(_encoding) + + res = py2_stdlib_quote(data, safe=safe.encode(_encoding)) + return res.decode(_encoding, errors=_errors) + else: + return py3_stdlib_quote(string, safe=safe, encoding=_encoding, errors=_errors) diff --git a/src/pycalver/regexfmt.py b/src/pycalver/regexfmt.py new file mode 100644 index 0000000..05c3ccb --- /dev/null +++ b/src/pycalver/regexfmt.py @@ -0,0 +1,68 @@ +import re +import textwrap + +from . import pysix +from . import patterns + + +def format_regex(regex: str) -> str: + r"""Format a regex pattern suitible for flags=re.VERBOSE. + + >>> regex = r"\[CalVer v(?P[1-9][0-9]{3})(?P(?:1[0-2]|0[1-9]))" + >>> print(format_regex(regex)) + \[CalVer[ ]v + (?P[1-9][0-9]{3}) + (?P + (?:1[0-2]|0[1-9]) + ) + """ + # provoke error for invalid regex + re.compile(regex) + + tmp_regex = regex.replace(" ", r"[ ]") + tmp_regex, _ = re.subn(r"([^\\])?\)(\?)?", "\\1)\\2\n", tmp_regex) + tmp_regex, _ = re.subn(r"([^\\])\(" , "\\1\n(" , tmp_regex) + tmp_regex, _ = re.subn(r"^\)\)" , ")\n)" , tmp_regex, flags=re.MULTILINE) + lines = tmp_regex.splitlines() + indented_lines = [] + level = 0 + for line in lines: + if line.strip(): + increment = line.count("(") - line.count(")") + if increment >= 0: + line = " " * level + line + level += increment + else: + level += increment + line = " " * level + line + indented_lines.append(line) + + formatted_regex = "\n".join(indented_lines) + + # provoke error if there is a bug in the formatting code + re.compile(formatted_regex) + return formatted_regex + + +def pyexpr_regex(regex: str) -> str: + try: + formatted_regex = format_regex(regex) + formatted_regex = textwrap.indent(formatted_regex.rstrip(), " ") + return 're.compile(r"""\n' + formatted_regex + '\n""", flags=re.VERBOSE)' + except re.error: + return f"re.compile({repr(regex)})" + + +def regex101_url(pattern: patterns.Pattern) -> str: + try: + regex_text = format_regex(pattern.regexp.pattern) + except re.error: + regex_text = pattern.regexp.pattern + + return "".join( + ( + "https://regex101.com/", + "?flavor=python", + "&flags=gmx" "®ex=" + pysix.quote(regex_text), + ) + ) diff --git a/test/test_cli.py b/test/test_cli.py index b7d3de4..eed8c2b 100644 --- a/test/test_cli.py +++ b/test/test_cli.py @@ -28,7 +28,7 @@ from pycalver.__main__ import cli README_TEXT_FIXTURE = """ Hello World v201701.1002-alpha ! - aka. 201701.1002a0 ! + [aka. 201701.1002a0 !] """ @@ -421,7 +421,7 @@ def test_novcs_bump(runner): with pl.Path("README.md").open() as fobj: content = fobj.read() assert calver + ".1002-alpha !\n" in content - assert calver[1:] + ".1002a0 !\n" in content + assert calver[1:] + ".1002a0 !]\n" in content result = runner.invoke(cli, ['bump', "-vv", "--release", "beta"]) assert result.exit_code == 0 @@ -429,7 +429,7 @@ def test_novcs_bump(runner): with pl.Path("README.md").open() as fobj: content = fobj.read() assert calver + ".1003-beta !\n" in content - assert calver[1:] + ".1003b0 !\n" in content + assert calver[1:] + ".1003b0 !]\n" in content def test_git_bump(runner): @@ -584,9 +584,9 @@ def test_v1_get_diff(runner): diff_lines = set(diff_str.splitlines()) assert "- Hello World v201701.1002-alpha !" in diff_lines - assert "- aka. 201701.1002a0 !" in diff_lines + assert "- [aka. 201701.1002a0 !]" in diff_lines assert "+ Hello World v202010.1003-beta !" in diff_lines - assert "+ aka. 202010.1003b0 !" in diff_lines + assert "+ [aka. 202010.1003b0 !]" in diff_lines assert '-current_version = "v202010.1001-alpha"' in diff_lines assert '+current_version = "v202010.1003-beta"' in diff_lines @@ -605,9 +605,9 @@ def test_v2_get_diff(runner): diff_lines = set(diff_str.splitlines()) assert "- Hello World v201701.1002-alpha !" in diff_lines - assert "- aka. 201701.1002a0 !" in diff_lines + assert "- [aka. 201701.1002a0 !]" in diff_lines assert "+ Hello World v202010.1003-beta !" in diff_lines - assert "+ aka. 202010.1003b0 !" in diff_lines + assert "+ [aka. 202010.1003b0 !]" in diff_lines assert '-current_version = "v202010.1001-alpha"' in diff_lines assert '+current_version = "v202010.1003-beta"' in diff_lines @@ -687,11 +687,9 @@ def test_hg_commit_message(runner, caplog): commits = shell(*shlex.split("hg log -l 2")).decode("utf-8").split("\n\n") - summary = commits[1].split("summary:")[-1] - assert ( - "bump from v201903.1001-alpha (201903.1001a0) to v201903.1002-beta (201903.1002b0)" - in summary - ) + expected = "bump from v201903.1001-alpha (201903.1001a0) to v201903.1002-beta (201903.1002b0)" + summary = commits[1].split("summary:")[-1] + assert expected in summary def test_git_commit_message(runner, caplog): @@ -716,7 +714,23 @@ def test_git_commit_message(runner, caplog): commits = shell(*shlex.split("git log -l 2")).decode("utf-8").split("\n\n") - summary = commits[1] - assert ( - "bump: v201903.1001-alpha (201903.1001a0) -> v201903.1002-beta (201903.1002b0)" in summary - ) + expected = "bump: v201903.1001-alpha (201903.1001a0) -> v201903.1002-beta (201903.1002b0)" + assert expected in commits[1] + + +def test_grep(runner): + _add_project_files("README.md") + + result = runner.invoke(cli, ['grep', r"vYYYY0M.BUILD[-RELEASE]", "README.md"]) + assert result.exit_code == 0 + assert "Found 1 match for pattern" in result.output + + search_re = r"^\s+2:\s+Hello World v201701\.1002-alpha !" + assert re.search(search_re, result.output, flags=re.MULTILINE) + + result = runner.invoke(cli, ['grep', r"\[aka. YYYY0M.BLD[PYTAGNUM] \!\]", "README.md"]) + assert result.exit_code == 0 + assert "Found 1 match for pattern" in result.output + + search_re = r"^\s+3:\s+\[aka\. 201701\.1002a0 \!\]" + assert re.search(search_re, result.output, flags=re.MULTILINE)