dycw-actions 0.6.4__py3-none-any.whl → 0.7.7__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.
Files changed (73) hide show
  1. actions/__init__.py +1 -1
  2. actions/action_dicts/lib.py +8 -8
  3. actions/clean_dir/cli.py +0 -12
  4. actions/clean_dir/constants.py +7 -0
  5. actions/clean_dir/lib.py +1 -0
  6. actions/cli.py +90 -29
  7. actions/constants.py +5 -1
  8. actions/pre_commit/click.py +15 -0
  9. actions/{conformalize_repo → pre_commit/conformalize_repo}/cli.py +2 -14
  10. actions/{conformalize_repo → pre_commit/conformalize_repo}/constants.py +8 -2
  11. actions/{conformalize_repo → pre_commit/conformalize_repo}/lib.py +97 -313
  12. actions/{conformalize_repo → pre_commit/conformalize_repo}/settings.py +1 -1
  13. actions/pre_commit/constants.py +8 -0
  14. actions/pre_commit/format_requirements/cli.py +24 -0
  15. actions/pre_commit/format_requirements/constants.py +7 -0
  16. actions/pre_commit/format_requirements/lib.py +52 -0
  17. actions/pre_commit/replace_sequence_strs/__init__.py +1 -0
  18. actions/pre_commit/replace_sequence_strs/cli.py +24 -0
  19. actions/pre_commit/replace_sequence_strs/constants.py +7 -0
  20. actions/{replace_sequence_strs → pre_commit/replace_sequence_strs}/lib.py +16 -22
  21. actions/pre_commit/touch_empty_py/__init__.py +1 -0
  22. actions/pre_commit/touch_empty_py/cli.py +24 -0
  23. actions/pre_commit/touch_empty_py/constants.py +7 -0
  24. actions/pre_commit/touch_empty_py/lib.py +62 -0
  25. actions/pre_commit/touch_py_typed/__init__.py +1 -0
  26. actions/pre_commit/touch_py_typed/cli.py +24 -0
  27. actions/pre_commit/touch_py_typed/constants.py +7 -0
  28. actions/pre_commit/touch_py_typed/lib.py +72 -0
  29. actions/pre_commit/update_requirements/__init__.py +1 -0
  30. actions/pre_commit/update_requirements/classes.py +130 -0
  31. actions/pre_commit/update_requirements/cli.py +24 -0
  32. actions/pre_commit/update_requirements/constants.py +7 -0
  33. actions/pre_commit/update_requirements/lib.py +140 -0
  34. actions/pre_commit/utilities.py +386 -0
  35. actions/publish_package/cli.py +7 -19
  36. actions/publish_package/constants.py +7 -0
  37. actions/publish_package/lib.py +3 -3
  38. actions/py.typed +0 -0
  39. actions/random_sleep/cli.py +6 -15
  40. actions/random_sleep/constants.py +7 -0
  41. actions/register_gitea_runner/__init__.py +1 -0
  42. actions/register_gitea_runner/cli.py +32 -0
  43. actions/register_gitea_runner/configs/config.yml +110 -0
  44. actions/register_gitea_runner/configs/entrypoint.sh +23 -0
  45. actions/register_gitea_runner/constants.py +23 -0
  46. actions/register_gitea_runner/lib.py +289 -0
  47. actions/register_gitea_runner/settings.py +33 -0
  48. actions/run_hooks/cli.py +3 -15
  49. actions/run_hooks/constants.py +7 -0
  50. actions/run_hooks/lib.py +2 -2
  51. actions/setup_cronjob/cli.py +0 -12
  52. actions/setup_cronjob/constants.py +5 -1
  53. actions/tag_commit/cli.py +7 -19
  54. actions/tag_commit/constants.py +7 -0
  55. actions/tag_commit/lib.py +8 -8
  56. actions/types.py +4 -1
  57. actions/utilities.py +68 -14
  58. {dycw_actions-0.6.4.dist-info → dycw_actions-0.7.7.dist-info}/METADATA +5 -3
  59. dycw_actions-0.7.7.dist-info/RECORD +84 -0
  60. actions/format_requirements/cli.py +0 -37
  61. actions/format_requirements/lib.py +0 -121
  62. actions/publish_package/doc.py +0 -6
  63. actions/random_sleep/doc.py +0 -6
  64. actions/replace_sequence_strs/cli.py +0 -37
  65. actions/run_hooks/doc.py +0 -6
  66. actions/tag_commit/doc.py +0 -6
  67. dycw_actions-0.6.4.dist-info/RECORD +0 -56
  68. /actions/{conformalize_repo → pre_commit}/__init__.py +0 -0
  69. /actions/{format_requirements → pre_commit/conformalize_repo}/__init__.py +0 -0
  70. /actions/{conformalize_repo → pre_commit/conformalize_repo}/configs/gitignore +0 -0
  71. /actions/{replace_sequence_strs → pre_commit/format_requirements}/__init__.py +0 -0
  72. {dycw_actions-0.6.4.dist-info → dycw_actions-0.7.7.dist-info}/WHEEL +0 -0
  73. {dycw_actions-0.6.4.dist-info → dycw_actions-0.7.7.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from typing import TYPE_CHECKING
5
+
6
+ from utilities.text import repr_str, strip_and_dedent
7
+
8
+ from actions import __version__
9
+ from actions.logging import LOGGER
10
+ from actions.pre_commit.utilities import get_pyproject_dependencies, yield_toml_doc
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import MutableSet
14
+ from pathlib import Path
15
+
16
+ from utilities.packaging import Requirement
17
+ from utilities.types import PathLike
18
+
19
+
20
+ def format_requirements(*paths: PathLike) -> None:
21
+ LOGGER.info(
22
+ strip_and_dedent("""
23
+ Running '%s' (version %s) with settings:
24
+ - paths = %s
25
+ """),
26
+ format_requirements.__name__,
27
+ __version__,
28
+ paths,
29
+ )
30
+ modifications: set[Path] = set()
31
+ for path in paths:
32
+ _format_path(path, modifications=modifications)
33
+ if len(modifications) >= 1:
34
+ LOGGER.info(
35
+ "Exiting due to modifications: %s",
36
+ ", ".join(map(repr_str, sorted(modifications))),
37
+ )
38
+ sys.exit(1)
39
+
40
+
41
+ def _format_path(
42
+ path: PathLike, /, *, modifications: MutableSet[Path] | None = None
43
+ ) -> None:
44
+ with yield_toml_doc(path, modifications=modifications) as doc:
45
+ get_pyproject_dependencies(doc).apply(_format_req)
46
+
47
+
48
+ def _format_req(requirement: Requirement, /) -> Requirement:
49
+ return requirement
50
+
51
+
52
+ __all__ = ["format_requirements"]
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from utilities.logging import basic_config
6
+ from utilities.os import is_pytest
7
+
8
+ from actions.logging import LOGGER
9
+ from actions.pre_commit.click import path_argument
10
+ from actions.pre_commit.replace_sequence_strs.lib import replace_sequence_strs
11
+
12
+ if TYPE_CHECKING:
13
+ from pathlib import Path
14
+
15
+
16
+ @path_argument
17
+ def replace_sequence_strs_sub_cmd(*, paths: tuple[Path, ...]) -> None:
18
+ if is_pytest():
19
+ return
20
+ basic_config(obj=LOGGER)
21
+ replace_sequence_strs(*paths)
22
+
23
+
24
+ __all__ = ["replace_sequence_strs_sub_cmd"]
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ REPLACE_SEQUENCE_STRS_DOCSTRING = "Replace 'Sequence[str]' with 'list[str]'"
4
+ REPLACE_SEQUENCE_STRS_SUB_CMD = "replace-sequence-strs"
5
+
6
+
7
+ __all__ = ["REPLACE_SEQUENCE_STRS_DOCSTRING", "REPLACE_SEQUENCE_STRS_SUB_CMD"]
@@ -1,10 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import sys
4
- from pathlib import Path
5
4
  from typing import TYPE_CHECKING, override
6
5
 
7
- from libcst import CSTTransformer, Module, Name, Subscript, parse_module
6
+ from libcst import CSTTransformer, Name, Subscript
8
7
  from libcst.matchers import Index as MIndex
9
8
  from libcst.matchers import Name as MName
10
9
  from libcst.matchers import Subscript as MSubscript
@@ -15,12 +14,13 @@ from utilities.text import repr_str, strip_and_dedent
15
14
 
16
15
  from actions import __version__
17
16
  from actions.logging import LOGGER
17
+ from actions.pre_commit.utilities import yield_python_file
18
18
 
19
19
  if TYPE_CHECKING:
20
- from utilities.types import PathLike
21
-
20
+ from collections.abc import MutableSet
21
+ from pathlib import Path
22
22
 
23
- _MODIFICATIONS: set[Path] = set()
23
+ from utilities.types import PathLike
24
24
 
25
25
 
26
26
  def replace_sequence_strs(*paths: PathLike) -> None:
@@ -33,30 +33,24 @@ def replace_sequence_strs(*paths: PathLike) -> None:
33
33
  __version__,
34
34
  paths,
35
35
  )
36
+ modifications: set[Path] = set()
36
37
  for path in paths:
37
- _format_path(path)
38
- if len(_MODIFICATIONS) >= 1:
38
+ _format_path(path, modifications=modifications)
39
+ if len(modifications) >= 1:
39
40
  LOGGER.info(
40
41
  "Exiting due to modifications: %s",
41
- ", ".join(map(repr_str, sorted(_MODIFICATIONS))),
42
+ ", ".join(map(repr_str, sorted(modifications))),
42
43
  )
43
44
  sys.exit(1)
44
45
 
45
46
 
46
- def _format_path(path: PathLike, /) -> None:
47
- path = Path(path)
48
- current = parse_module(path.read_text())
49
- expected = _get_formatted(path)
50
- if current.code != expected.code:
51
- _ = path.write_text(expected.code.rstrip("\n") + "\n")
52
- _MODIFICATIONS.add(path)
53
-
54
-
55
- def _get_formatted(path: PathLike, /) -> Module:
56
- path = Path(path)
57
- existing = path.read_text()
58
- wrapper = MetadataWrapper(parse_module(existing))
59
- return wrapper.module.visit(SequenceToListTransformer())
47
+ def _format_path(
48
+ path: PathLike, /, *, modifications: MutableSet[Path] | None = None
49
+ ) -> None:
50
+ with yield_python_file(path, modifications=modifications) as context:
51
+ context.output = MetadataWrapper(context.input).module.visit(
52
+ SequenceToListTransformer()
53
+ )
60
54
 
61
55
 
62
56
  class SequenceToListTransformer(CSTTransformer):
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from utilities.logging import basic_config
6
+ from utilities.os import is_pytest
7
+
8
+ from actions.logging import LOGGER
9
+ from actions.pre_commit.click import path_argument
10
+ from actions.pre_commit.touch_empty_py.lib import touch_empty_py
11
+
12
+ if TYPE_CHECKING:
13
+ from pathlib import Path
14
+
15
+
16
+ @path_argument
17
+ def touch_empty_py_sub_cmd(*, paths: tuple[Path, ...]) -> None:
18
+ if is_pytest():
19
+ return
20
+ basic_config(obj=LOGGER)
21
+ touch_empty_py(*paths)
22
+
23
+
24
+ __all__ = ["touch_empty_py_sub_cmd"]
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ TOUCH_EMPTY_PY_DOCSTRING = "Touch empty '.py' files"
4
+ TOUCH_EMPTY_PY_SUB_CMD = "touch-empty-py"
5
+
6
+
7
+ __all__ = ["TOUCH_EMPTY_PY_DOCSTRING", "TOUCH_EMPTY_PY_SUB_CMD"]
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from typing import TYPE_CHECKING
5
+
6
+ from libcst import parse_statement
7
+ from utilities.text import repr_str, strip_and_dedent
8
+ from utilities.throttle import throttle
9
+ from utilities.whenever import HOUR
10
+
11
+ from actions import __version__
12
+ from actions.constants import PATH_THROTTLE_CACHE
13
+ from actions.logging import LOGGER
14
+ from actions.pre_commit.utilities import yield_python_file
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import MutableSet
18
+ from pathlib import Path
19
+
20
+ from utilities.types import PathLike
21
+
22
+
23
+ def _touch_empty_py(*paths: PathLike) -> None:
24
+ LOGGER.info(
25
+ strip_and_dedent("""
26
+ Running '%s' (version %s) with settings:
27
+ - paths = %s
28
+ """),
29
+ touch_empty_py.__name__,
30
+ __version__,
31
+ paths,
32
+ )
33
+ modifications: set[Path] = set()
34
+ for path in paths:
35
+ _format_path(path, modifications=modifications)
36
+ if len(modifications) >= 1:
37
+ LOGGER.info(
38
+ "Exiting due to modifications: %s",
39
+ ", ".join(map(repr_str, sorted(modifications))),
40
+ )
41
+ sys.exit(1)
42
+
43
+
44
+ touch_empty_py = throttle(
45
+ delta=12 * HOUR, path=PATH_THROTTLE_CACHE / _touch_empty_py.__name__
46
+ )(_touch_empty_py)
47
+
48
+
49
+ def _format_path(
50
+ path: PathLike, /, *, modifications: MutableSet[Path] | None = None
51
+ ) -> None:
52
+ with yield_python_file(path, modifications=modifications) as context:
53
+ if len(context.input.body) >= 1:
54
+ return
55
+ body = [
56
+ *context.input.body,
57
+ parse_statement("from __future__ import annotations"),
58
+ ]
59
+ context.output = context.input.with_changes(body=body)
60
+
61
+
62
+ __all__ = ["touch_empty_py"]
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from utilities.logging import basic_config
6
+ from utilities.os import is_pytest
7
+
8
+ from actions.logging import LOGGER
9
+ from actions.pre_commit.click import path_argument
10
+ from actions.pre_commit.touch_py_typed.lib import touch_py_typed
11
+
12
+ if TYPE_CHECKING:
13
+ from pathlib import Path
14
+
15
+
16
+ @path_argument
17
+ def touch_py_typed_sub_cmd(*, paths: tuple[Path, ...]) -> None:
18
+ if is_pytest():
19
+ return
20
+ basic_config(obj=LOGGER)
21
+ touch_py_typed(*paths)
22
+
23
+
24
+ __all__ = ["touch_py_typed_sub_cmd"]
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ TOUCH_PY_TYPED_DOCSTRING = "Touch 'py.typed'"
4
+ TOUCH_PY_TYPED_SUB_CMD = "touch-py-typed"
5
+
6
+
7
+ __all__ = ["TOUCH_PY_TYPED_DOCSTRING", "TOUCH_PY_TYPED_SUB_CMD"]
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING
6
+
7
+ from utilities.iterables import one
8
+ from utilities.text import repr_str, strip_and_dedent
9
+ from utilities.throttle import throttle
10
+ from utilities.whenever import HOUR
11
+
12
+ from actions import __version__
13
+ from actions.constants import PATH_THROTTLE_CACHE
14
+ from actions.logging import LOGGER
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import MutableSet
18
+
19
+ from utilities.types import PathLike
20
+
21
+
22
+ def _touch_py_typed(*paths: PathLike) -> None:
23
+ LOGGER.info(
24
+ strip_and_dedent("""
25
+ Running '%s' (version %s) with settings:
26
+ - paths = %s
27
+ """),
28
+ touch_py_typed.__name__,
29
+ __version__,
30
+ paths,
31
+ )
32
+ modifications: set[Path] = set()
33
+ for path in paths:
34
+ _format_path(path, modifications=modifications)
35
+ if len(modifications) >= 1:
36
+ LOGGER.info(
37
+ "Exiting due to modifications: %s",
38
+ ", ".join(map(repr_str, sorted(modifications))),
39
+ )
40
+ sys.exit(1)
41
+
42
+
43
+ touch_py_typed = throttle(
44
+ delta=12 * HOUR, path=PATH_THROTTLE_CACHE / _touch_py_typed.__name__
45
+ )(_touch_py_typed)
46
+
47
+
48
+ def _format_path(
49
+ path: PathLike, /, *, modifications: MutableSet[Path] | None = None
50
+ ) -> None:
51
+ path = Path(path)
52
+ if not path.is_file():
53
+ msg = f"Expected a file; {str(path)!r} is not"
54
+ raise FileNotFoundError(msg)
55
+ if path.name != "pyproject.toml":
56
+ msg = f"Expected 'pyproject.toml'; got {str(path)!r}"
57
+ raise TypeError(msg)
58
+ src = path.parent / "src"
59
+ if not src.exists():
60
+ return
61
+ if not src.is_dir():
62
+ msg = f"Expected a directory; {str(src)!r} is not"
63
+ raise NotADirectoryError(msg)
64
+ non_tests = one(p for p in src.iterdir() if p.name != "tests")
65
+ py_typed = non_tests / "py.typed"
66
+ if not py_typed.exists():
67
+ py_typed.touch()
68
+ if modifications is not None:
69
+ modifications.add(py_typed)
70
+
71
+
72
+ __all__ = ["touch_py_typed"]
@@ -0,0 +1 @@
1
+ from __future__ import annotations
@@ -0,0 +1,130 @@
1
+ # ruff: noqa: TC003
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from dataclasses import dataclass, field, replace
6
+ from functools import total_ordering
7
+ from pathlib import Path
8
+ from typing import Any, Self, override
9
+
10
+ from pydantic import BaseModel
11
+ from utilities.version import (
12
+ ParseVersionError,
13
+ _VersionEmptySuffixError,
14
+ _VersionNegativeMajorVersionError,
15
+ _VersionNegativeMinorVersionError,
16
+ _VersionZeroError,
17
+ )
18
+ from utilities.version import Version as Version3
19
+ from utilities.version import parse_version as parse_version3
20
+
21
+ type TwoSidedVersions = tuple[Version2or3 | None, Version1or2 | None]
22
+ type Version1or2 = int | Version2
23
+ type Version2or3 = Version2 | Version3
24
+ type VersionSet = dict[str, Version2or3]
25
+
26
+
27
+ ##
28
+
29
+
30
+ class PipListOutput(BaseModel):
31
+ name: str
32
+ version: str
33
+ editable_project_location: Path | None = None
34
+
35
+
36
+ class PipListOutdatedOutput(BaseModel):
37
+ name: str
38
+ version: str
39
+ latest_version: str
40
+ latest_filetype: str
41
+
42
+
43
+ _ = PipListOutput.model_rebuild()
44
+ _ = PipListOutdatedOutput.model_rebuild()
45
+
46
+
47
+ ##
48
+
49
+
50
+ @dataclass(repr=False, frozen=True, slots=True)
51
+ @total_ordering
52
+ class Version2:
53
+ """A version identifier."""
54
+
55
+ major: int = 0
56
+ minor: int = 0
57
+ suffix: str | None = field(default=None, kw_only=True)
58
+
59
+ def __post_init__(self) -> None:
60
+ if (self.major == 0) and (self.minor == 0):
61
+ raise _VersionZeroError(major=self.major, minor=self.minor, patch=0)
62
+ if self.major < 0:
63
+ raise _VersionNegativeMajorVersionError(major=self.major)
64
+ if self.minor < 0:
65
+ raise _VersionNegativeMinorVersionError(minor=self.minor)
66
+ if (self.suffix is not None) and (len(self.suffix) == 0):
67
+ raise _VersionEmptySuffixError(suffix=self.suffix)
68
+
69
+ def __le__(self, other: Any, /) -> bool:
70
+ if not isinstance(other, type(self)):
71
+ return NotImplemented
72
+ self_as_tuple = (self.major, self.minor)
73
+ other_as_tuple = (other.major, other.minor)
74
+ return self_as_tuple <= other_as_tuple
75
+
76
+ @override
77
+ def __repr__(self) -> str:
78
+ version = f"{self.major}.{self.minor}"
79
+ if self.suffix is not None:
80
+ version = f"{version}-{self.suffix}"
81
+ return version
82
+
83
+ def bump_major(self) -> Self:
84
+ return type(self)(self.major + 1, 0)
85
+
86
+ def bump_minor(self) -> Self:
87
+ return type(self)(self.major, self.minor + 1)
88
+
89
+ def with_suffix(self, *, suffix: str | None = None) -> Self:
90
+ return replace(self, suffix=suffix)
91
+
92
+
93
+ def parse_version1_or_2(version: str, /) -> Version1or2:
94
+ try:
95
+ return parse_version2(version)
96
+ except ParseVersionError:
97
+ return int(version)
98
+
99
+
100
+ def parse_version2_or_3(version: str, /) -> Version2or3:
101
+ try:
102
+ return parse_version3(version)
103
+ except ParseVersionError:
104
+ return parse_version2(version)
105
+
106
+
107
+ def parse_version2(version: str, /) -> Version2:
108
+ try:
109
+ ((major, minor, suffix),) = _PARSE_VERSION2_PATTERN.findall(version)
110
+ except ValueError:
111
+ raise ParseVersionError(version=version) from None
112
+ return Version2(int(major), int(minor), suffix=None if suffix == "" else suffix)
113
+
114
+
115
+ _PARSE_VERSION2_PATTERN = re.compile(r"^(\d+)\.(\d+)(?:-(\w+))?")
116
+
117
+
118
+ __all__ = [
119
+ "PipListOutdatedOutput",
120
+ "PipListOutput",
121
+ "TwoSidedVersions",
122
+ "Version1or2",
123
+ "Version2",
124
+ "Version2or3",
125
+ "Version3",
126
+ "VersionSet",
127
+ "parse_version1_or_2",
128
+ "parse_version2",
129
+ "parse_version2_or_3",
130
+ ]
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from utilities.logging import basic_config
6
+ from utilities.os import is_pytest
7
+
8
+ from actions.logging import LOGGER
9
+ from actions.pre_commit.click import path_argument
10
+ from actions.pre_commit.update_requirements.lib import update_requirements
11
+
12
+ if TYPE_CHECKING:
13
+ from pathlib import Path
14
+
15
+
16
+ @path_argument
17
+ def update_requirements_sub_cmd(*, paths: tuple[Path, ...]) -> None:
18
+ if is_pytest():
19
+ return
20
+ basic_config(obj=LOGGER)
21
+ update_requirements(*paths)
22
+
23
+
24
+ __all__ = ["update_requirements_sub_cmd"]
@@ -0,0 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ UPDATE_REQUIREMENTS_DOCSTRING = "Update a set of requirements"
4
+ UPDATE_REQUIREMENTS_SUB_CMD = "update-requirements"
5
+
6
+
7
+ __all__ = ["UPDATE_REQUIREMENTS_DOCSTRING", "UPDATE_REQUIREMENTS_SUB_CMD"]
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from functools import partial
5
+ from typing import TYPE_CHECKING
6
+
7
+ from pydantic import TypeAdapter
8
+ from utilities.functions import max_nullable
9
+ from utilities.text import repr_str, strip_and_dedent
10
+
11
+ from actions import __version__
12
+ from actions.logging import LOGGER
13
+ from actions.pre_commit.update_requirements.classes import (
14
+ PipListOutdatedOutput,
15
+ PipListOutput,
16
+ Version1or2,
17
+ Version2,
18
+ Version3,
19
+ parse_version1_or_2,
20
+ parse_version2_or_3,
21
+ )
22
+ from actions.pre_commit.utilities import get_pyproject_dependencies, yield_toml_doc
23
+ from actions.utilities import logged_run
24
+
25
+ if TYPE_CHECKING:
26
+ from collections.abc import MutableSet
27
+ from pathlib import Path
28
+
29
+ from utilities.packaging import Requirement
30
+ from utilities.types import PathLike
31
+
32
+ from actions.pre_commit.update_requirements.classes import Version2or3, VersionSet
33
+ from actions.types import StrDict
34
+
35
+
36
+ def update_requirements(*paths: PathLike) -> None:
37
+ LOGGER.info(
38
+ strip_and_dedent("""
39
+ Running '%s' (version %s) with settings:
40
+ - paths = %s
41
+ """),
42
+ update_requirements.__name__,
43
+ __version__,
44
+ paths,
45
+ )
46
+ modifications: set[Path] = set()
47
+ for path in paths:
48
+ _format_path(path, modifications=modifications)
49
+ if len(modifications) >= 1:
50
+ LOGGER.info(
51
+ "Exiting due to modifications: %s",
52
+ ", ".join(map(repr_str, sorted(modifications))),
53
+ )
54
+ sys.exit(1)
55
+
56
+
57
+ def _format_path(
58
+ path: PathLike,
59
+ /,
60
+ *,
61
+ versions: VersionSet | None = None,
62
+ modifications: MutableSet[Path] | None = None,
63
+ ) -> None:
64
+ versions_use = _get_versions() if versions is None else versions
65
+ with yield_toml_doc(path, modifications=modifications) as doc:
66
+ get_pyproject_dependencies(doc).apply(
67
+ partial(_format_req, versions=versions_use)
68
+ )
69
+
70
+
71
+ def _get_versions() -> VersionSet:
72
+ json1 = logged_run(
73
+ "uv", "pip", "list", "--format", "json", "--strict", return_=True
74
+ )
75
+ models1 = TypeAdapter(list[PipListOutput]).validate_json(json1)
76
+ versions1 = {p.name: parse_version2_or_3(p.version) for p in models1}
77
+ json2 = logged_run(
78
+ "uv", "pip", "list", "--format", "json", "--outdated", "--strict", return_=True
79
+ )
80
+ models2 = TypeAdapter(list[PipListOutdatedOutput]).validate_json(json2)
81
+ versions2 = {p.name: parse_version2_or_3(p.latest_version) for p in models2}
82
+ out: StrDict = {}
83
+ for key in set(versions1) | set(versions2):
84
+ out[key] = max_nullable([versions1.get(key), versions2.get(key)])
85
+ return out
86
+
87
+
88
+ def _format_req(requirement: Requirement, /, *, versions: VersionSet) -> Requirement:
89
+ try:
90
+ lower = parse_version2_or_3(requirement[">="])
91
+ except KeyError:
92
+ lower = None
93
+ try:
94
+ upper = parse_version1_or_2(requirement["<"])
95
+ except KeyError:
96
+ upper = None
97
+ latest = versions.get(requirement.name)
98
+ new_lower: Version2or3 | None = None
99
+ new_upper: Version1or2 | None = None
100
+ match lower, upper, latest:
101
+ case None, None, None:
102
+ ...
103
+ case None, None, Version2() | Version3():
104
+ new_lower = latest
105
+ new_upper = latest.bump_major().major
106
+ case Version2() | Version3(), None, None:
107
+ new_lower = lower
108
+ case (Version2(), None, Version2()) | (Version3(), None, Version3()):
109
+ new_lower = max(lower, latest)
110
+ case None, int() | Version2(), None:
111
+ new_upper = upper
112
+ case None, int(), Version2():
113
+ new_upper = max(upper, latest.bump_major().major)
114
+ case None, Version2(), Version3():
115
+ bumped = latest.bump_minor()
116
+ new_upper = max(upper, Version2(bumped.major, bumped.minor))
117
+ case (
118
+ (Version2(), int(), None)
119
+ | (Version3(), int(), None)
120
+ | (Version3(), Version2(), None)
121
+ ):
122
+ new_lower = lower
123
+ new_upper = lower.bump_major().major
124
+ case (
125
+ (Version2(), int(), Version2())
126
+ | (Version3(), int(), Version3())
127
+ | (Version3(), Version2(), Version3())
128
+ ):
129
+ new_lower = max(lower, latest)
130
+ new_upper = new_lower.bump_major().major
131
+ case never:
132
+ raise NotImplementedError(never)
133
+ if new_lower is not None:
134
+ requirement = requirement.replace(">=", str(new_lower))
135
+ if new_upper is not None:
136
+ requirement = requirement.replace("<", str(new_upper))
137
+ return requirement
138
+
139
+
140
+ __all__ = ["update_requirements"]