diff --git a/poetry.lock b/poetry.lock index 05532bb..338cd9d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.0 and should not be changed by hand. [[package]] name = "black" @@ -370,14 +370,72 @@ type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.deve [[package]] name = "tomli" -version = "2.0.1" +version = "2.4.0" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}, + {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}, + {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}, + {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}, + {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}, + {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}, + {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}, + {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}, + {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}, + {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}, + {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}, + {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}, + {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}, + {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}, + {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}, + {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}, + {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}, + {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, +] + +[[package]] +name = "tomlkit" +version = "0.14.0" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680"}, + {file = "tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064"}, ] [[package]] @@ -417,4 +475,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "ec0f9588ecd9593af4440306cbe42d71a1b7c8ee2db342734d0b00a2b9452174" +content-hash = "1f7062de244f45d54d7e4dd1f541eafbb658e8d26a8e23dca3d478efff1c3674" diff --git a/pyproject.toml b/pyproject.toml index e0e83e3..b359f60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ sync_pyproject_version = "pyproject_version_sync.sync_pyproject_version:main" [tool.poetry.dependencies] python = "^3.10" -tomli = "~2.0.1" +tomlkit = "^0.14.0" [tool.poetry.group.dev.dependencies] diff --git a/pyproject_version_sync/sync_pyproject_version.py b/pyproject_version_sync/sync_pyproject_version.py index 39a93cd..b71cb9e 100644 --- a/pyproject_version_sync/sync_pyproject_version.py +++ b/pyproject_version_sync/sync_pyproject_version.py @@ -1,14 +1,21 @@ """This pre-commit hook ensures that the version in pyproject.toml matches the latest git tag.""" + import argparse import re import subprocess import sys from pathlib import Path +from typing import TypeVar + +import tomlkit +from tomlkit.items import Table +from tomlkit.toml_document import TOMLDocument -import tomli +PATHS = ["tool.poetry.version", "project.version"] +T = TypeVar("T") -def _execute_in_shell(cmd: str) -> subprocess.CompletedProcess: +def _execute_in_shell(cmd: str) -> subprocess.CompletedProcess[bytes]: return subprocess.run(cmd.split(), check=True, capture_output=True) # noqa: S603 @@ -44,45 +51,92 @@ def find_latest_tag() -> str: return re.findall(r"^v?(\d+\.\d+\.\d+).*", latest_tag)[0] -def find_version_in_toml(toml_file: Path) -> str: +def extract_prefix_and_tail(path: str) -> tuple[list[str], str]: """ - Find the project version in pyproject.toml. + Split a given dotted path into prefix and the last element. Args: - toml_file: Path to pyproject.toml. + path: Dotted path, like "a.b.c". Returns: - Project version. + Tuple of prefix and the last element. + """ + parts = path.split(".") + return parts[:-1], parts[-1] + + +def traverse(toml: TOMLDocument, path: str, cls: type[T]) -> T | None: """ - with Path.open(toml_file, "rb") as f: - pyproject = tomli.load(f) + Traverse given toml by a given dotted path and verify found type. - return pyproject["tool"]["poetry"]["version"] + Args: + toml: Toml documet. + path: Dotted path, like "a.b.c". + cls: Expected class. + + Returns: + Object at a given path or None if not found. + """ + prefix, tail = extract_prefix_and_tail(path) + root: Table | TOMLDocument = toml + for part in prefix: + next_root = root.get(part) + if not isinstance(next_root, Table): + return None + root = next_root -def write_new_version_to_toml(toml_file: Path, version_pyproject: str, version_git: str) -> None: + result = root.get(tail) + if not isinstance(result, cls): + return None + return result + + +def traverse_set(toml: TOMLDocument, path: str, value: object) -> bool: """ - Write the new version to pyproject.toml. + Traverse given toml by a given dotted path and set the value, overwrite if it exists already. Args: - toml_file: Path to pyproject.toml. - version_pyproject: Version in pyproject.toml. - version_git: Latest git tag. + toml: Toml documet. + path: Dotted path, like "a.b.c". + value: string or int. + + Returns: + Success status. """ - pyproject_raw = Path.open(toml_file).read() + prefix, tail = extract_prefix_and_tail(path) + + root: Table | TOMLDocument = toml + for part in prefix: + next_root = root.setdefault(part, {}) + if not isinstance(next_root, Table): + return False + root = next_root + + root[tail] = value + return True + + +def find_version_in_toml(pyproject: TOMLDocument) -> tuple[str, str]: + """ + Find the project version in pyproject.toml. - # Ignore all the stuff after the block of interest - # Avoids edge case where we may overwrite the version of something else in the file by mistake - block = re.findall( - rf"\[tool\.poetry\][^\n]*.*\nversion\s?=\s?[\"|\']{re.escape(version_pyproject)}[\"|\']\n", - pyproject_raw, - flags=re.DOTALL, - )[0] - new_block = block.replace(version_pyproject, version_git) + Args: + pyproject: Parsed pyproject.toml. - pyproject_new = pyproject_raw.replace(block, new_block) - with Path.open(toml_file, "w") as f_out: - f_out.write(pyproject_new) + Returns: + Project version. + """ + # If user has invalid values it will error out + versions: list[tuple[str, str]] = [] + for path in PATHS: + version = traverse(pyproject, path, str) + if version: + versions.append((path, version)) + + if len(versions) != 1: + sys.exit(f"Expected exactly one version in pyproject.toml, got: {versions}") + return versions[0] def parse_args() -> argparse.Namespace: @@ -109,9 +163,11 @@ def main() -> None: """Run the pre-commit hook.""" args = parse_args() fix = args.fix - toml_file = args.toml_file - version_pyproject = find_version_in_toml(toml_file) + path = Path(args.toml_file) + pyproject = tomlkit.parse(path.read_bytes()) + + version_path, version_pyproject = find_version_in_toml(pyproject) if version_pyproject != (version_git := find_latest_tag()): if not fix: @@ -120,7 +176,10 @@ def main() -> None: f"Run with the `--fix` option to automatically sync.", ) - write_new_version_to_toml(toml_file, version_pyproject, version_git) + # This path should exist + assert traverse_set(pyproject, version_path, version_git) # noqa: S101 + tomlkit.dump(pyproject, path.open("w")) + sys.exit("Syncing version in pyproject.toml to match latest git tag.") sys.exit()