Skip to content

Commit d31f913

Browse files
Add support for pre-commit plugins
Enhance `pre-commit` task to support plugins and extensions. Implement an extension to update versions of GitHub actions in workflow templates. --------- Co-authored-by: Jannis Mittenzwei <[email protected]>
1 parent 4e16ec1 commit d31f913

File tree

10 files changed

+243
-6
lines changed

10 files changed

+243
-6
lines changed

.github/PULL_REQUEST_TEMPLATE/pull_request_template.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,4 @@
1010
* [ ] Have you checked to ensure there aren't other open Pull Requests for the same update/change?
1111
* [ ] Are you mentioning the issue which this PullRequest fixes ("Fixes...")
1212

13-
# 🦺 Github Actions
14-
* [ ] Did you update the version pinning in the action(s)
15-
* security-issues (exasol-toolbox)
16-
-> Only for releases!!
17-
1813
Note: If any of the above is not relevant to your PR just check the box.

doc/changes/unreleased.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@
4444
nox -s prepare-release -- -h
4545
```
4646

47+
* **Added Plugin Support for Nox Task `prepare-release`**
48+
49+
- For further details on the plugin specification, refer to `exasol.toolbox.nox.plugin`.
50+
- For an example of usage, refer to the `noxconfig` of the Python toolbox.
51+
4752
## 📚 Documentation
4853
* Fixed typos and updated documentation
4954

exasol/toolbox/nox/_release.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
Mode,
1616
_version,
1717
)
18+
from exasol.toolbox.nox.plugin import NoxTasks
1819
from exasol.toolbox.release import (
1920
Version,
2021
extract_release_notes,
@@ -108,9 +109,15 @@ def prepare_release(session: Session, python=False) -> None:
108109
if not args.no_branch and not args.no_add:
109110
session.run("git", "switch", "-c", f"release/prepare-{new_version}")
110111

112+
pm = NoxTasks.plugin_manager(PROJECT_CONFIG)
113+
111114
_ = _update_project_version(session, new_version)
112115
changelog, changes, unreleased = _update_changelog(new_version)
113116

117+
pm.hook.prepare_release_update_version(
118+
session=session, config=PROJECT_CONFIG, version=new_version
119+
)
120+
114121
if args.no_add:
115122
return
116123

@@ -124,6 +131,9 @@ def prepare_release(session: Session, python=False) -> None:
124131
PROJECT_CONFIG.version_file,
125132
],
126133
)
134+
pm.hook.prepare_release_add_files(
135+
session=session, config=PROJECT_CONFIG, add=_add_files_to_index
136+
)
127137
session.run("git", "commit", "-m", f"Prepare release {new_version}")
128138

129139
if not args.no_pr:

exasol/toolbox/nox/plugin.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import pluggy
2+
3+
_PLUGIN_MARKER = "python-toolbox"
4+
hookspec = pluggy.HookspecMarker("python-toolbox")
5+
hookimpl = pluggy.HookimplMarker("python-toolbox")
6+
7+
8+
class NoxTasks:
9+
@hookspec
10+
def prepare_release_update_version(self, session, config, version):
11+
"""
12+
This hook is called during version updating when a release is being prepared.
13+
Implementors can add their own logic and tasks required to be run during the version update here.
14+
15+
Args:
16+
session (nox.Session):
17+
The nox session running the release preparation.
18+
This it can be used to run commands, etc.
19+
20+
config (class):
21+
The project configuration object from the noxconfig.py file.
22+
23+
version (str):
24+
A string representation of the version to be released.
25+
This follows the pattern of Semantic Versioning, i.e., "MAJOR.MINOR.PATCH".
26+
An example would be "1.4.2".
27+
"""
28+
29+
@hookspec
30+
def prepare_release_add_files(self, session, config, add):
31+
"""
32+
Files which should be added to the prepare relase commit should be added using add.
33+
34+
Args:
35+
session (nox.Session):
36+
The nox session running the release preparation.
37+
This it can be used to run commands, etc.
38+
39+
config (class):
40+
The project configuration object from the noxconfig.py file.
41+
42+
add (function):
43+
Function which takes a nox session and a list of files which will be added to the index.
44+
e.g. `add(session, ["file1.txt", "file2.txt"])`
45+
"""
46+
47+
@staticmethod
48+
def plugin_manager(config) -> pluggy.PluginManager:
49+
pm = pluggy.PluginManager(_PLUGIN_MARKER)
50+
pm.add_hookspecs(NoxTasks)
51+
for plugin in getattr(config, "plugins", []):
52+
pm.register(plugin())
53+
return pm
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from pathlib import Path
2+
from typing import List
3+
4+
5+
def update_workflow(template: Path, version: str) -> None:
6+
"""Updates versions of XYZ in GitHub workflow ..."""
7+
with open(template, encoding="utf-8") as file:
8+
content = file.readlines()
9+
10+
content = update_versions(
11+
lines=content, matcher="exasol/python-toolbox/.github/", version=version
12+
)
13+
14+
with open(template, "w", encoding="utf-8") as file:
15+
file.writelines(content)
16+
17+
18+
def is_update_required(line, matcher):
19+
return matcher in line and "@" in line
20+
21+
22+
def update_version(line, version):
23+
keep = line[: line.index("@") + 1]
24+
updated = f"{version}\n"
25+
return f"{keep}{updated}"
26+
27+
28+
def update_versions(lines, matcher, version) -> List[str]:
29+
result = []
30+
for line in lines:
31+
if is_update_required(line, matcher):
32+
line = update_version(line, version)
33+
result.append(line)
34+
return result

noxconfig.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,28 @@
1111

1212
from nox import Session
1313

14+
from exasol.toolbox.nox.plugin import hookimpl
15+
from exasol.toolbox.tools.replace_version import update_workflow
16+
17+
18+
class UpdateTemplates:
19+
TEMPLATE_PATH: Path = Path(__file__).parent / "exasol" / "toolbox" / "templates"
20+
21+
@property
22+
def workflows(self):
23+
gh_workflows = self.TEMPLATE_PATH / "github" / "workflows"
24+
gh_workflows = [f for f in gh_workflows.iterdir() if f.is_file()]
25+
return gh_workflows
26+
27+
@hookimpl
28+
def prepare_release_update_version(self, session, config, version):
29+
for workflow in self.workflows:
30+
update_workflow(workflow, version)
31+
32+
@hookimpl
33+
def prepare_release_add_files(self, session, config, add):
34+
add(session, self.workflows)
35+
1436

1537
@dataclass(frozen=True)
1638
class Config:
@@ -20,6 +42,7 @@ class Config:
2042
doc: Path = Path(__file__).parent / "doc"
2143
version_file: Path = Path(__file__).parent / "exasol" / "toolbox" / "version.py"
2244
path_filters: Iterable[str] = ("dist", ".eggs", "venv", "metrics-schema")
45+
plugins = [UpdateTemplates]
2346

2447
@staticmethod
2548
def pre_integration_tests_hook(

noxfile.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,60 @@
66

77
# default actions to be run if nothing is explicitly specified with the -s option
88
nox.options.sessions = ["fix"]
9+
10+
11+
def main() -> None:
12+
"""
13+
This excerpt was taken from nox.__main__.py. Generally, users should invoke Nox using the CLI provided by the Nox package.
14+
However, when using Nox directly, it spawns a process and therefore, debugging isn't straightforward.
15+
To cope with this issue, this entry point was added to enable a straightforward way to debug Nox tasks.
16+
"""
17+
import sys
18+
from typing import Any
19+
20+
from nox import (
21+
_options,
22+
tasks,
23+
workflow,
24+
)
25+
from nox._version import get_nox_version
26+
from nox.logger import setup_logging
27+
28+
def execute_workflow(args: Any) -> int:
29+
"""
30+
Execute the appropriate tasks.
31+
"""
32+
return workflow.execute(
33+
global_config=args,
34+
workflow=(
35+
tasks.load_nox_module,
36+
tasks.merge_noxfile_options,
37+
tasks.discover_manifest,
38+
tasks.filter_manifest,
39+
tasks.honor_list_request,
40+
tasks.run_manifest,
41+
tasks.print_summary,
42+
tasks.create_report,
43+
tasks.final_reduce,
44+
),
45+
)
46+
47+
args = _options.options.parse_args()
48+
49+
if args.help:
50+
_options.options.print_help()
51+
return
52+
53+
if args.version:
54+
print(get_nox_version(), file=sys.stderr)
55+
return
56+
57+
setup_logging(
58+
color=args.color, verbose=args.verbose, add_timestamp=args.add_timestamp
59+
)
60+
exit_code = execute_workflow(args)
61+
sys.exit(exit_code)
62+
63+
64+
if __name__ == "__main__": # pragma: no cover
65+
main()

poetry.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ typer = {extras = ["all"], version = ">=0.7.0"}
5151
prysk = {extras = ["pytest-plugin"], version = "^0.17.0"}
5252
importlib-resources = ">=5.12.0"
5353
myst-parser = "^2.0.0"
54+
pluggy = "^1.5.0"
5455

5556

5657
[tool.poetry.group.dev.dependencies]

test/unit/replace_version_test.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import pytest
2+
3+
from exasol.toolbox.tools.replace_version import (
4+
is_update_required,
5+
update_version,
6+
update_versions,
7+
)
8+
9+
10+
@pytest.mark.parametrize(
11+
"line,matcher,expected",
12+
[
13+
("hallo/world@all", "hallo", True),
14+
("hallo/world@all", "foo", False),
15+
("hallo/world/all", "hallo", False),
16+
("hallo/world/all", "foo", False),
17+
],
18+
)
19+
def test_is_update_required(line, matcher, expected):
20+
actual = is_update_required(line, matcher)
21+
assert actual == expected
22+
23+
24+
@pytest.mark.parametrize(
25+
"line,version,expected",
26+
[
27+
("hallo/[email protected]\n", "2.3.4", "hallo/[email protected]\n"),
28+
("hallo/[email protected]\n", "9.9.9", "hallo/[email protected]\n"),
29+
],
30+
)
31+
def test_update_version(line, version, expected):
32+
actual = update_version(line, version)
33+
assert actual == expected
34+
35+
36+
@pytest.mark.parametrize(
37+
"lines,matcher,version,expected",
38+
[
39+
(
40+
[
41+
"hallo/[email protected]\n",
42+
43+
"hallo/world/3.4.5\n",
44+
"foo/world/3.4.5\n",
45+
],
46+
"hallo",
47+
"4.5.6",
48+
[
49+
"hallo/[email protected]\n",
50+
51+
"hallo/world/3.4.5\n",
52+
"foo/world/3.4.5\n",
53+
],
54+
)
55+
],
56+
)
57+
def test_update_versions(lines, matcher, version, expected):
58+
actual = update_versions(lines, matcher, version)
59+
assert actual == expected

0 commit comments

Comments
 (0)