dycw-pre-commit-hooks 0.11.3__py3-none-any.whl → 0.12.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of dycw-pre-commit-hooks might be problematic. Click here for more details.

@@ -1,14 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dycw-pre-commit-hooks
3
- Version: 0.11.3
3
+ Version: 0.12.1
4
4
  Author-email: Derek Wan <d.wan@icloud.com>
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: click<8.3,>=8.2.1
7
- Requires-Dist: dycw-utilities<0.151,>=0.150.11
7
+ Requires-Dist: dycw-utilities<0.167,>=0.166.5
8
+ Requires-Dist: gitpython<3.2,>=3.1.45
8
9
  Requires-Dist: libcst<1.9,>=1.8.2
9
10
  Requires-Dist: loguru<0.8,>=0.7.3
11
+ Requires-Dist: orjson<3.12,>=3.11.3
10
12
  Requires-Dist: packaging<25.1,>=25.0
11
13
  Requires-Dist: tomlkit<0.14,>=0.13.2
14
+ Requires-Dist: xdg-base-dirs<6.1,>=6.0.2
12
15
  Description-Content-Type: text/markdown
13
16
 
14
17
  # pre-commit-hooks
@@ -28,9 +31,10 @@ My [`pre-commit`](https://pre-commit.com/) hooks.
28
31
  - repo: https://github.com/dycw/pre-commit-hooks
29
32
  rev: master
30
33
  hooks:
34
+ - id: format-requirements
31
35
  - id: replace-sequence-str
32
36
  - id: run-bump-my-version
33
- - id: run-ruff-format
37
+ - id: tag-commits
34
38
  ```
35
39
 
36
40
  1. Update your `.pre-commit-config.yaml`:
@@ -0,0 +1,15 @@
1
+ pre_commit_hooks/__init__.py,sha256=bMyIveYyxCjAv2_JH9Hd38cd4RL7NmCC4HiW8Td0nQU,59
2
+ pre_commit_hooks/common.py,sha256=x-a-v0esyBPJqPZf6uypbrFaxUKEgfnuAxiUvYSaLbA,2350
3
+ pre_commit_hooks/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ pre_commit_hooks/format_requirements/__init__.py,sha256=bn1JsblINCf0ro2QrhsM8XtTOHnPFSZC2t-Hx5AJDfA,3392
5
+ pre_commit_hooks/format_requirements/__main__.py,sha256=15JSp_rhjI_Ddoj4MRkHFShfnYxs6GggUhLRlGtrQ0E,156
6
+ pre_commit_hooks/replace_sequence_str/__init__.py,sha256=a-pCAcBaa7FDsGDNLzMiD-xG20wKl-SRmhWSbm9hrvo,1618
7
+ pre_commit_hooks/replace_sequence_str/__main__.py,sha256=B1dxOxngV4vUVnDVrXSywiySOs1P_zF30_4ZMRsOSaY,157
8
+ pre_commit_hooks/run_bump_my_version/__init__.py,sha256=UGaOu7Ie5LRGsMe34eygSgMWteKfDRGNX49R3kafczk,1353
9
+ pre_commit_hooks/run_bump_my_version/__main__.py,sha256=w2V3y61jrSau-zxjl8ciHtWPlJQwXbYxNJ2tGYVyI4s,156
10
+ pre_commit_hooks/tag_commits/__init__.py,sha256=BvpAqdRDw06Gab3jNXYO6BgosnMybowZUsXV8wQA2gU,3699
11
+ pre_commit_hooks/tag_commits/__main__.py,sha256=qefgYw7LWbvmzZS45-ym6olS4cHqw1Emw2wlqZBXN_o,148
12
+ dycw_pre_commit_hooks-0.12.1.dist-info/METADATA,sha256=CvLbPZLgmY-MZS2dZulcvQHAmS64lk64xP96uvC-dVY,1029
13
+ dycw_pre_commit_hooks-0.12.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
+ dycw_pre_commit_hooks-0.12.1.dist-info/entry_points.txt,sha256=r1tnPzaGvOrSdX-ZRriidf8AhZgUtXfjdTpwpkTBA8U,260
15
+ dycw_pre_commit_hooks-0.12.1.dist-info/RECORD,,
@@ -2,4 +2,4 @@
2
2
  format-requirements = pre_commit_hooks.format_requirements:main
3
3
  replace-sequence-str = pre_commit_hooks.replace_sequence_str:main
4
4
  run-bump-my-version = pre_commit_hooks.run_bump_my_version:main
5
- run-ruff-format = pre_commit_hooks.run_ruff_format:main
5
+ tag-commits = pre_commit_hooks.tag_commits:main
@@ -1,3 +1,3 @@
1
1
  from __future__ import annotations
2
2
 
3
- __version__ = "0.11.3"
3
+ __version__ = "0.12.1"
@@ -1,33 +1,66 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dataclasses import dataclass
3
+ from pathlib import Path
4
+ from typing import assert_never
4
5
 
5
6
  from loguru import logger
6
7
  from tomlkit import TOMLDocument, parse
8
+ from tomlkit.items import Table
7
9
  from utilities.pathlib import get_repo_root
10
+ from utilities.version import Version, parse_version
8
11
 
9
- _ROOT = get_repo_root()
10
- PYPROJECT_TOML = _ROOT.joinpath("pyproject.toml")
12
+ PYPROJECT_TOML = get_repo_root().joinpath("pyproject.toml")
11
13
 
12
14
 
13
- ##
15
+ def get_version(
16
+ path_or_text: Path | str | bytes | TOMLDocument, /, *, desc: str = ""
17
+ ) -> Version:
18
+ """Parse the version from a block of text."""
19
+ match path_or_text:
20
+ case Path() as path:
21
+ return get_version(
22
+ path.read_text(), desc=f" from {str(path)!r}" if desc == "" else desc
23
+ )
24
+ case str() | bytes() as text:
25
+ return get_version(parse(text), desc=desc)
26
+ case TOMLDocument() as doc:
27
+ try:
28
+ tool = doc["tool"]
29
+ except KeyError:
30
+ logger.exception(
31
+ f"Failed to get version{desc}; key 'tool' does not exist"
32
+ )
33
+ raise
34
+ if not isinstance(tool, Table):
35
+ logger.exception(f"Failed to get version{desc}; `tool` is not a Table")
36
+ raise TypeError
37
+ try:
38
+ bumpversion = tool["bumpversion"]
39
+ except KeyError:
40
+ logger.exception(
41
+ f"Failed to get version{desc}; key 'bumpversion' does not exist"
42
+ )
43
+ raise
44
+ if not isinstance(bumpversion, Table):
45
+ logger.exception(
46
+ f"Failed to get version{desc}; `bumpversion` is not a Table"
47
+ )
48
+ raise TypeError
49
+ try:
50
+ version = bumpversion["current_version"]
51
+ except KeyError:
52
+ logger.exception(
53
+ f"Failed to get version{desc}; key 'current_version' does not exist"
54
+ )
55
+ raise
56
+ if not isinstance(version, str):
57
+ logger.exception(
58
+ f"Failed to get version{desc}; `version` is not a string"
59
+ )
60
+ raise TypeError
61
+ return parse_version(version)
62
+ case never: # pyright: ignore[reportUnnecessaryComparison]
63
+ assert_never(never)
14
64
 
15
65
 
16
- @dataclass(kw_only=True)
17
- class PyProject:
18
- contents: str
19
- doc: TOMLDocument
20
-
21
-
22
- def read_pyproject() -> PyProject:
23
- try:
24
- with PYPROJECT_TOML.open(mode="r") as fh:
25
- contents = fh.read()
26
- except FileNotFoundError:
27
- logger.exception("pyproject.toml not found")
28
- raise
29
- doc = parse(contents)
30
- return PyProject(contents=contents, doc=doc)
31
-
32
-
33
- __all__ = ["PYPROJECT_TOML", "read_pyproject"]
66
+ __all__ = ["PYPROJECT_TOML", "get_version"]
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, Any, override
4
4
 
5
+ import utilities.click
5
6
  from click import argument, command
6
7
  from packaging._tokenizer import ParserSyntaxError
7
8
  from packaging.requirements import (
@@ -13,7 +14,6 @@ from packaging.specifiers import Specifier, SpecifierSet
13
14
  from tomlkit import array, dumps, loads, string
14
15
  from tomlkit.items import Array, Table
15
16
  from utilities.atomicwrites import writer
16
- from utilities.click import FilePath
17
17
 
18
18
  from pre_commit_hooks.common import PYPROJECT_TOML
19
19
 
@@ -25,10 +25,10 @@ if TYPE_CHECKING:
25
25
 
26
26
 
27
27
  @command()
28
- @argument("paths", nargs=-1, type=FilePath)
28
+ @argument("paths", nargs=-1, type=utilities.click.Path())
29
29
  def main(*, paths: tuple[Path, ...]) -> bool:
30
30
  """CLI for the `format_requirements` hook."""
31
- results = list(map(_process, paths))
31
+ results = list(map(_process, paths)) # run all
32
32
  return all(results)
33
33
 
34
34
 
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING, override
4
4
 
5
+ import utilities.click
5
6
  from click import argument, command
6
7
  from libcst import CSTTransformer, Name, Subscript, parse_module
7
8
  from libcst.matchers import Index as MIndex
@@ -10,17 +11,16 @@ from libcst.matchers import Subscript as MSubscript
10
11
  from libcst.matchers import SubscriptElement as MSubscriptElement
11
12
  from libcst.matchers import matches
12
13
  from libcst.metadata import MetadataWrapper
13
- from utilities.click import FilePath
14
14
 
15
15
  if TYPE_CHECKING:
16
16
  from pathlib import Path
17
17
 
18
18
 
19
19
  @command()
20
- @argument("paths", nargs=-1, type=FilePath)
20
+ @argument("paths", nargs=-1, type=utilities.click.Path())
21
21
  def main(*, paths: tuple[Path, ...]) -> bool:
22
22
  """CLI for the `replace_sequence_str` hook."""
23
- results = list(map(_process, paths))
23
+ results = list(map(_process, paths)) # run all
24
24
  return all(results)
25
25
 
26
26
 
@@ -1,15 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
- import re
4
3
  from pathlib import Path
5
- from re import MULTILINE
6
4
  from subprocess import PIPE, STDOUT, CalledProcessError, check_call, check_output
7
5
 
8
6
  from click import command
9
7
  from loguru import logger
10
- from utilities.version import Version, parse_version
11
8
 
12
- from pre_commit_hooks.common import PYPROJECT_TOML
9
+ from pre_commit_hooks.common import PYPROJECT_TOML, get_version
13
10
 
14
11
 
15
12
  @command()
@@ -20,10 +17,10 @@ def main() -> bool:
20
17
 
21
18
  def _process() -> bool:
22
19
  path = PYPROJECT_TOML.relative_to(Path.cwd())
23
- current = _parse_version_from_file_or_text(path)
20
+ current = get_version(path)
24
21
  commit = check_output(["git", "rev-parse", "origin/master"], text=True).rstrip("\n")
25
22
  contents = check_output(["git", "show", f"{commit}:{path}"], text=True)
26
- master = _parse_version_from_file_or_text(contents)
23
+ master = get_version(contents)
27
24
  if current in {master.bump_patch(), master.bump_minor(), master.bump_major()}:
28
25
  return True
29
26
  cmd = [
@@ -47,18 +44,4 @@ def _process() -> bool:
47
44
  return False
48
45
 
49
46
 
50
- _PATTERN = re.compile(r'^current_version = "(\d+\.\d+\.\d+)"$', flags=MULTILINE)
51
-
52
-
53
- def _parse_version_from_file_or_text(path_or_text: Path | str, /) -> Version:
54
- """Parse the version from a block of text."""
55
- match path_or_text:
56
- case Path() as path:
57
- with path.open() as fh:
58
- return _parse_version_from_file_or_text(fh.read())
59
- case str() as text:
60
- (match,) = _PATTERN.findall(text)
61
- return parse_version(match)
62
-
63
-
64
47
  __all__ = ["main"]
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ import utilities.click
6
+ from click import command, option
7
+ from git import Commit, GitCommandError, Repo
8
+ from loguru import logger
9
+ from utilities.hashlib import md5_hash
10
+ from utilities.pathlib import get_repo_root
11
+ from utilities.tzlocal import LOCAL_TIME_ZONE_NAME
12
+ from utilities.whenever import from_timestamp, get_now_local
13
+ from whenever import DateTimeDelta, ZonedDateTime
14
+ from xdg_base_dirs import xdg_cache_home
15
+
16
+ from pre_commit_hooks.common import get_version
17
+
18
+ if TYPE_CHECKING:
19
+ from collections.abc import Set as AbstractSet
20
+
21
+ _RUN_EVERY: DateTimeDelta | None = None
22
+ _MAX_AGE: DateTimeDelta | None = None
23
+
24
+
25
+ @command()
26
+ @option(
27
+ "--run-every",
28
+ type=utilities.click.DateTimeDelta(),
29
+ default=_RUN_EVERY,
30
+ show_default=True,
31
+ )
32
+ @option(
33
+ "--max-age",
34
+ type=utilities.click.DateTimeDelta(),
35
+ default=_MAX_AGE,
36
+ show_default=True,
37
+ )
38
+ def main(
39
+ *,
40
+ run_every: DateTimeDelta | None = _RUN_EVERY,
41
+ max_age: DateTimeDelta | None = _MAX_AGE,
42
+ ) -> bool:
43
+ """CLI for the `tag_commits` hook."""
44
+ return _process(run_every=run_every, max_age=max_age)
45
+
46
+
47
+ def _process(
48
+ *,
49
+ run_every: DateTimeDelta | None = _RUN_EVERY,
50
+ max_age: DateTimeDelta | None = _MAX_AGE,
51
+ ) -> bool:
52
+ if run_every is not None:
53
+ last = _get_last_run()
54
+ min_date_time = get_now_local() - run_every
55
+ if (last is not None) and (min_date_time <= last):
56
+ return True
57
+ return _process_commits(max_age=max_age)
58
+
59
+
60
+ def _get_last_run() -> ZonedDateTime | None:
61
+ hash_ = md5_hash(get_repo_root())
62
+ path = xdg_cache_home().joinpath("tag-commits", hash_)
63
+ try:
64
+ text = path.read_text()
65
+ except FileNotFoundError:
66
+ return None
67
+ try:
68
+ return ZonedDateTime.parse_common_iso(text.strip("\n"))
69
+ except ValueError:
70
+ return None
71
+
72
+
73
+ def _process_commits(*, max_age: DateTimeDelta | None = None) -> bool:
74
+ repo = Repo(".", search_parent_directories=True)
75
+ tagged = {tag.commit.hexsha for tag in repo.tags}
76
+ min_date_time = None if max_age is None else (get_now_local() - max_age)
77
+ commits = reversed(list(repo.iter_commits(repo.refs["origin/master"])))
78
+ results = [
79
+ _process_commit(c, tagged, repo, min_date_time=min_date_time) for c in commits
80
+ ] # run all
81
+ return all(results)
82
+
83
+
84
+ def _process_commit(
85
+ commit: Commit,
86
+ tagged: AbstractSet[str],
87
+ repo: Repo,
88
+ /,
89
+ *,
90
+ min_date_time: ZonedDateTime | None = None,
91
+ ) -> bool:
92
+ if (commit.hexsha in tagged) or (
93
+ (min_date_time is not None) and (_get_date_time(commit) < min_date_time)
94
+ ):
95
+ return True
96
+ try:
97
+ _tag_commit(commit, repo)
98
+ except GitCommandError:
99
+ return False
100
+ return True
101
+
102
+
103
+ def _get_date_time(commit: Commit, /) -> ZonedDateTime:
104
+ return from_timestamp(commit.committed_date, time_zone=LOCAL_TIME_ZONE_NAME)
105
+
106
+
107
+ def _tag_commit(commit: Commit, repo: Repo, /) -> None:
108
+ sha = commit.hexsha[:7]
109
+ date = _get_date_time(commit)
110
+ try:
111
+ joined = commit.tree.join("pyproject.toml")
112
+ except KeyError:
113
+ logger.exception(f"`pyproject.toml` not found; failed to tag {sha!r} ({date})")
114
+ return
115
+ text = joined.data_stream.read()
116
+ version = get_version(text, desc=f"'pyproject.toml' @ {sha}")
117
+ try:
118
+ tag = repo.create_tag(str(version), ref=sha)
119
+ except GitCommandError as error:
120
+ desc = error.stderr.strip("\n").strip()
121
+ logger.exception(f"Failed to tag {sha!r} ({date}) due to {desc}")
122
+ return
123
+ logger.info(f"Tagging {sha!r} ({date}) as {str(version)!r}...")
124
+ _ = repo.remotes.origin.push(f"refs/tags/{tag.name}")
125
+
126
+
127
+ __all__ = ["main"]
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from pre_commit_hooks.run_ruff_format import main
3
+ from pre_commit_hooks.tag_commits import main
4
4
 
5
5
  if __name__ == "__main__":
6
6
  raise SystemExit(int(not main()))
@@ -1,15 +0,0 @@
1
- pre_commit_hooks/__init__.py,sha256=A7kvce4fHYgV1tEVj_d1Ynre-ZheJHHRqpWXzriT7ko,59
2
- pre_commit_hooks/common.py,sha256=JDpKC4-T4ucPZkK3bAQZ9xrYKsJ7wi1mTo5gVXFGe80,695
3
- pre_commit_hooks/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- pre_commit_hooks/format_requirements/__init__.py,sha256=OXqdd3J5p9sstWcdEOCQb5wH3tDAZStecO6CKRZvBuA,3381
5
- pre_commit_hooks/format_requirements/__main__.py,sha256=15JSp_rhjI_Ddoj4MRkHFShfnYxs6GggUhLRlGtrQ0E,156
6
- pre_commit_hooks/replace_sequence_str/__init__.py,sha256=bPkc3KHbu8RMFykIsegW06hAKUXtXjJD36KtECDsNR8,1607
7
- pre_commit_hooks/replace_sequence_str/__main__.py,sha256=B1dxOxngV4vUVnDVrXSywiySOs1P_zF30_4ZMRsOSaY,157
8
- pre_commit_hooks/run_bump_my_version/__init__.py,sha256=9RpVVqTmtmKkV8rMR4sMfI-i_7gaLOmNrfi2GWgtNSk,1953
9
- pre_commit_hooks/run_bump_my_version/__main__.py,sha256=w2V3y61jrSau-zxjl8ciHtWPlJQwXbYxNJ2tGYVyI4s,156
10
- pre_commit_hooks/run_ruff_format/__init__.py,sha256=4QYVE-ktOZjJZGqLMpy4_Te1dASzEkEYVBHzpa1EdGM,2062
11
- pre_commit_hooks/run_ruff_format/__main__.py,sha256=faesqqpMaesg5r-LvkkQt1W9kahvNr-60K3SMYv1NgY,152
12
- dycw_pre_commit_hooks-0.11.3.dist-info/METADATA,sha256=SXc54B_PuqaSNaiPsVp03dzKQyp_aFyNFtnPz4Yxl58,884
13
- dycw_pre_commit_hooks-0.11.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
14
- dycw_pre_commit_hooks-0.11.3.dist-info/entry_points.txt,sha256=V5pxLTDfSr4UJAWI-1bNEazF1SCSkkpYr0nRx3B0QTg,268
15
- dycw_pre_commit_hooks-0.11.3.dist-info/RECORD,,
@@ -1,81 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from subprocess import CalledProcessError, check_call
4
- from typing import TYPE_CHECKING, cast
5
-
6
- from click import command
7
- from loguru import logger
8
- from tomlkit import dumps, table
9
-
10
- from pre_commit_hooks.common import PYPROJECT_TOML, PyProject, read_pyproject
11
-
12
- if TYPE_CHECKING:
13
- from tomlkit.container import Container
14
-
15
-
16
- @command()
17
- def main() -> bool:
18
- """CLI for the `run-ruff-format` hook."""
19
- return _process()
20
-
21
-
22
- def _process() -> bool:
23
- curr = read_pyproject()
24
- new = _get_modified_pyproject()
25
- result1 = _run_ruff_format(new)
26
- result2 = _run_ruff_format(curr)
27
- _write_pyproject(curr)
28
- return result1 and result2
29
-
30
-
31
- def _get_modified_pyproject() -> PyProject:
32
- pyproject = read_pyproject()
33
- doc = pyproject.doc
34
- try:
35
- tool = cast("Container", doc["tool"])
36
- except KeyError:
37
- tool = table()
38
- try:
39
- ruff = cast("Container", tool["ruff"])
40
- except KeyError:
41
- ruff = table()
42
- ruff["line-length"] = 320
43
- try:
44
- format_ = cast("Container", ruff["format"])
45
- except KeyError:
46
- format_ = table()
47
- format_["skip-magic-trailing-comma"] = True
48
- try:
49
- lint = cast("Container", ruff["lint"])
50
- except KeyError:
51
- lint = table()
52
- try:
53
- isort = cast("Container", lint["isort"])
54
- except KeyError:
55
- isort = table()
56
- isort["split-on-trailing-comma"] = False
57
- doc["tool"] = tool
58
- tool["ruff"] = ruff
59
- ruff["format"] = format_
60
- ruff["lint"] = lint
61
- lint["isort"] = isort
62
- return PyProject(contents=dumps(doc), doc=doc)
63
-
64
-
65
- def _run_ruff_format(pyproject: PyProject, /) -> bool:
66
- _write_pyproject(pyproject)
67
- cmd = ["ruff", "format", "."]
68
- try:
69
- code = check_call(cmd)
70
- except CalledProcessError:
71
- logger.exception("Failed to run {cmd!r}", cmd=" ".join(cmd))
72
- return False
73
- return code == 0
74
-
75
-
76
- def _write_pyproject(pyproject: PyProject, /) -> None:
77
- with PYPROJECT_TOML.open(mode="w") as fh:
78
- _ = fh.write(pyproject.contents)
79
-
80
-
81
- __all__ = ["main"]