From 41b6bf233c52f08f344e09107bdda253e6d8b57a Mon Sep 17 00:00:00 2001 From: real-yfprojects Date: Thu, 8 Jun 2023 14:53:00 +0200 Subject: [PATCH] Implements `--allow-frozen` cmd option that enables support for frozen revisions. This new mode will trust `frozen: xxx` comments and use those to check frozen revisions. If the comment specifies the same revision as the lock file nothing will be changed. Otherwise the revision is replaced with expected revision tag. Documents `--allow-frozen` config option in README. * sync_with_poetry/swp.py * README.md --- README.md | 10 ++++ sync_with_poetry/swp.py | 43 ++++++++++++---- tests/test_frozen.py | 107 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+), 9 deletions(-) create mode 100644 tests/test_frozen.py diff --git a/README.md b/README.md index 92c8843..686db51 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ Excerpt from a `.pre-commit-config.yaml` using an example of this hook: --skip [SKIP ...] Packages to skip --config CONFIG Path to a custom .pre-commit-config.yaml file --db PACKAGE_LIST Path to a custom package list (json) + --allow-frozen Trust `frozen: xxx` comments for frozen revisions. ``` Usually this hook uses only dev packages to sync the hooks. Pass `--all`, if you @@ -96,6 +97,15 @@ defaults to `.pre-commit-config.yaml`). Pass `--db ` to point to an alternative package list (json). Such a file overrides the mapping in [`db.py`](sync_with_poetry/db.py). +Pass `--allow-frozen` if you want to use frozen revisions in your config. +Without this option _SWP_ will replace frozen revisions with the tag name taken +from `poetry.lock` even if the frozen revision specifies the same commit as the +tag. This options relies on `frozen: xxx` comments appended to the line of the +frozen revision where `xxx` will be the tag name corresponding to the commit +hash used. If the comment specifies the same revision as the lock file nothing +is changed. Otherwise the revision is replaced with the expected revision tag +and the `frozen: xxx` comment is removed. + ## Supported packages Supported packages out-of-the-box are listed in diff --git a/sync_with_poetry/swp.py b/sync_with_poetry/swp.py index ad690f0..516d46f 100644 --- a/sync_with_poetry/swp.py +++ b/sync_with_poetry/swp.py @@ -11,7 +11,10 @@ from sync_with_poetry.db import DEPENDENCY_MAPPING YAML_FILE = ".pre-commit-config.yaml" -REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$') +REV_LINE_RE = re.compile( + r'^(\s+)rev:(\s*)(?P[\'"]?)(?P[^\s#]+)(?P=quotes)(\s*)(# frozen: (?P\S+)\b)?(?P.*?)(?P\r?\n)$' +) +FROZEN_REV_RE = re.compile(r"[a-f\d]{40}") class PoetryItems(object): @@ -35,7 +38,6 @@ def __init__( self._poetry_lock = {} for package in poetry_list: - # skip if package["name"] in skip: continue @@ -68,8 +70,8 @@ def sync_repos( skip: List[str] = [], config: str = YAML_FILE, db: Dict[str, Dict[str, str]] = DEPENDENCY_MAPPING, + frozen: bool = False, ) -> int: - retv = 0 toml = TOMLFile(filename) @@ -93,7 +95,6 @@ def sync_repos( idxs = [i for i, line in enumerate(lines) if REV_LINE_RE.match(line)] for idx, pre_commit_repo in zip(idxs, repo_pattern): - if pre_commit_repo is None: continue @@ -101,13 +102,27 @@ def sync_repos( assert match is not None - if pre_commit_repo["rev"] == match[4].replace('"', "").replace("'", ""): + lock_rev = pre_commit_repo["rev"] + config_rev = match["rev"].replace('"', "").replace("'", "") + + if frozen and FROZEN_REV_RE.fullmatch(config_rev) and match["comment"]: + config_rev = match["comment"] + + if lock_rev == config_rev: continue - new_rev_s = yaml.dump({"rev": pre_commit_repo["rev"]}, default_style=match[3]) + new_rev_s = yaml.dump({"rev": lock_rev}, default_style=match["quotes"]) new_rev = new_rev_s.split(":", 1)[1].strip() - lines[idx] = f"{match[1]}rev:{match[2]}{new_rev}{match[5]}{match[6]}" - print(f"[{pre_commit_repo['name']}] -> rev: {pre_commit_repo['rev']}") + + rest = "" + if match["rest"]: + rest = match[5] or "" + if match["comment"]: + rest += "#" + rest += match["rest"] + + lines[idx] = f"{match[1]}rev:{match[2]}{new_rev}{rest}{match['eol']}" + retv |= 1 with open(config, "w", newline="") as f: @@ -131,6 +146,14 @@ def main(argv: Optional[Sequence[str]] = None) -> int: default=YAML_FILE, help="Path to the .pre-commit-config.yaml file", ) + parser.add_argument( + "--allow-frozen", + action="store_true", + dest="frozen", + help="Trust `frozen: xxx` comments for frozen revisions. " + "If the comment specifies the same revision as the lock file the check passes. " + "Otherwise the revision is replaced with expected revision tag.", + ) parser.add_argument( "--db", type=str, @@ -144,7 +167,9 @@ def main(argv: Optional[Sequence[str]] = None) -> int: mapping = json.load(f) retv = 0 for filename in args.filenames: - retv |= sync_repos(filename, args.skip, args.config, mapping) + retv |= sync_repos( + filename, args.skip, args.config, mapping, frozen=args.frozen + ) return retv diff --git a/tests/test_frozen.py b/tests/test_frozen.py new file mode 100644 index 0000000..bfb995c --- /dev/null +++ b/tests/test_frozen.py @@ -0,0 +1,107 @@ +import pytest +from py._path.local import LocalPath + +from sync_with_poetry import swp + +# test cases for frozen revisions +# all these packages have version 1.0.0 in poetry.lock +TEST_REVS = [ + " rev: 1.0.0\n", + " rev: 1.0.0 # frozen\n", + " rev: 1.0.0 # frozen: 2.0.0\n", + " rev: 6fd1ced85fc139abd7f5ab4f3d78dab37592cd5e # frozen: 2.0.0\n", + " rev: 6fd1ced85fc139abd7f5ab4f3d78dab37592cd5e # frozen: 1.0.0\n", + " rev: 6fd1ced85fc139abd7f5ab4f3d78dab37592cd5e # frozen\n", + " rev: 6fd1ced85fc139abd7f5ab4f3d78dab37592cd5e # frozen: 1.0.0 fav version\n", + " rev: 6fd1ced85fc139abd7f5ab4f3d78dab37592cd5e # frozen: 2.0.0 fav version\n", + " rev: 6fd1ced85fc139abd7f5ab4f3d78dab37592cd5e # fav version\n", + " rev: 6fd1ced85fc139abd7f5ab4f3d78dab37592cd5e\n", +] + + +TEST_REVS_UNFROZEN = [ + " rev: 1.0.0\n", + " rev: 1.0.0 # frozen\n", + " rev: 1.0.0 # frozen: 2.0.0\n", + " rev: 1.0.0\n", + " rev: 1.0.0\n", + " rev: 1.0.0 # frozen\n", + " rev: 1.0.0 # fav version\n", + " rev: 1.0.0 # fav version\n", + " rev: 1.0.0 # fav version\n", + " rev: 1.0.0\n", +] + +TEST_REVS_FROZEN = [ + " rev: 1.0.0\n", + " rev: 1.0.0 # frozen\n", + " rev: 1.0.0 # frozen: 2.0.0\n", + " rev: 1.0.0\n", + " rev: 6fd1ced85fc139abd7f5ab4f3d78dab37592cd5e # frozen: 1.0.0\n", + " rev: 1.0.0 # frozen\n", + " rev: 6fd1ced85fc139abd7f5ab4f3d78dab37592cd5e # frozen: 1.0.0 fav version\n", + " rev: 1.0.0 # fav version\n", + " rev: 1.0.0 # fav version\n", + " rev: 1.0.0\n", +] + +assert len(TEST_REVS) == len(TEST_REVS_UNFROZEN) == len(TEST_REVS_FROZEN) + + +def config_content(rev_line: str) -> str: + return ( + "repos:\n" " - repo: test\n" + rev_line + " hooks:\n" " - id: test\n" + ) + + +LOCK_CONTENT = ( + "[[package]]\n" + 'name = "test"\n' + 'version = "1.0.0"\n' + 'description = "a dummy package"\n' + "optional = false\n" + 'python-versions = ">=3.6"\n' +) + + +DEPENDENCY_MAPPING = { + "test": { + "repo": "test", + "rev": "${rev}", + } +} + + +def run_and_check( + tmpdir: LocalPath, rev_line: str, expected: str, frozen: bool +) -> None: + lock_file = tmpdir.join("poetry.lock") + lock_file.write(LOCK_CONTENT) + config_file = tmpdir.join(".pre-commit-yaml") + config = config_content(rev_line) + config_file.write(config) + + retv = swp.sync_repos( + lock_file.strpath, + frozen=frozen, + db=DEPENDENCY_MAPPING, + config=config_file.strpath, + ) + + fixed_lines = open(config_file.strpath).readlines() + fixed_rev_line = fixed_lines[2] + + assert fixed_rev_line == expected + + assert len(config.splitlines()) == len(fixed_lines) + assert retv == int(expected != rev_line) + + +@pytest.mark.parametrize("rev_line,expected", zip(TEST_REVS, TEST_REVS_UNFROZEN)) +def test_frozen_disabled(tmpdir: LocalPath, rev_line: str, expected: str) -> None: + run_and_check(tmpdir, rev_line, expected, frozen=False) + + +@pytest.mark.parametrize("rev_line,expected", zip(TEST_REVS, TEST_REVS_FROZEN)) +def test_frozen_enabled(tmpdir: LocalPath, rev_line: str, expected: str) -> None: + run_and_check(tmpdir, rev_line, expected, frozen=True)