Skip to content

Commit 2fa481d

Browse files
authored
14 add publish hook for a python pypi version of maven artifact zip directory structure and upload to pypi registry (#20)
* feat: Add reqstool info to sdist tar.gz
1 parent 722eae0 commit 2fa481d

6 files changed

Lines changed: 210 additions & 41 deletions

File tree

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,19 @@ When you declare this in the pyproject.toml file, you are specifying the require
2727

2828
## Usage
2929

30-
31-
3230
### Configuration
3331

3432
The plugin can be configured through the `pyproject.toml` file. Configure plugin in `pyproject.toml`as follows;
3533

36-
```
37-
[tool.hatch.build.hooks.reqstool_decorators]
34+
```toml
35+
[tool.hatch.build.hooks.reqstool]
3836
dependencies = ["reqstool-python-hatch-plugin == <version>"]
39-
path = ["src","tests"]
40-
37+
sources = ["src", "tests"]
38+
dataset_directory = "docs/reqstool"
39+
output_directory = "build/reqstool"
40+
test_results = ["build/**/junit.xml"]
4141
```
42+
4243
It specifies that the reqstool-python-hatch-plugin is a dependency for the build process, and it should be of a specific version.
4344

4445
Further it defines the paths where the plugin should be applied. In this case, it specifies that the plugin should be applied to files in the src and tests directories.

docs/modules/ROOT/pages/usage.adoc

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
The plugin can be configured through the `pyproject.toml` file. Configure plugin in `pyproject.toml`as follows;
77

88
```toml
9-
[tool.hatch.build.hooks.reqstool_decorators]
9+
[tool.hatch.build.hooks.reqstool]
1010
dependencies = ["reqstool-python-hatch-plugin == <version>"]
11-
path = ["src","tests"]
12-
11+
sources = ["src", "tests"]
12+
junit_xml_file = "build/junit.xml"
13+
dataset_path = "docs/reqstool"
14+
output_directory = "build/reqstool"
1315
```
1416
It specifies that the reqstool-python-hatch-plugin is a dependency for the build process, and it should be of a specific version.
1517

pyproject.toml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,20 @@ requires = ["hatchling", "hatch-vcs", "build", "twine"]
33
build-backend = "hatchling.build"
44

55
[tool.pytest.ini_options]
6-
addopts = ["-s", "--import-mode=importlib", "--log-cli-level=DEBUG"]
6+
addopts = [
7+
"-rsxX",
8+
"-s",
9+
"--import-mode=importlib",
10+
"--log-cli-level=DEBUG",
11+
'-m not slow or not integration',
12+
]
713
pythonpath = [".", "src", "tests"]
8-
testpaths = ["tests/unit"]
14+
testpaths = ["tests"]
15+
markers = [
16+
"flaky: tests that can randomly fail through no change to the code",
17+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
18+
"integration: tests that require external resources",
19+
]
920

1021
[project]
1122
name = "reqstool-python-hatch-plugin"
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Copyright © LFV
2+
3+
import gzip
4+
import io
5+
import os
6+
import tarfile
7+
import tempfile
8+
from importlib.metadata import PackageNotFoundError, version
9+
from pathlib import Path
10+
from typing import Any, Dict, List, Optional
11+
12+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
13+
from hatchling.builders.plugin.interface import BuilderInterface
14+
from reqstool_python_decorators.processors.decorator_processor import DecoratorProcessor
15+
from ruamel.yaml import YAML
16+
17+
18+
class ReqstoolBuildHook(BuildHookInterface):
19+
"""
20+
Build hook that creates reqstool
21+
22+
1. annotations files based on reqstool decorators
23+
2. artifact reqstool-tar.gz file to be uploaded by hatch publish to pypi repo
24+
25+
Attributes:
26+
PLUGIN_NAME (str): The name of the plugin, set to "reqstool".
27+
"""
28+
29+
PLUGIN_NAME: str = "reqstool"
30+
31+
CONFIG_SOURCES = "sources"
32+
CONFIG_DATASET_DIRECTORY = "dataset_directory"
33+
CONFIG_OUTPUT_DIRECTORY = "output_directory"
34+
CONFIG_TEST_RESULTS: str = "test_results"
35+
36+
INPUT_FILE_REQUIREMENTS_YML: str = "requirements.yml"
37+
INPUT_FILE_SOFTWARE_VERIFICATION_CASES_YML: str = "software_verification_cases.yml"
38+
INPUT_FILE_MANUAL_VERIFICATION_RESULTS_YML: str = "manual_verification_results.yml"
39+
INPUT_FILE_JUNIT_XML: str = "build/junit.xml"
40+
INPUT_FILE_ANNOTATIONS_YML: str = "annotations.yml"
41+
INPUT_DIR_DATASET: str = "reqstool"
42+
43+
OUTPUT_DIR_REQSTOOL: str = "build/reqstool"
44+
OUTPUT_SDIST_REQSTOOL_YML: str = "reqstool_config.yml"
45+
46+
ARCHIVE_OUTPUT_DIR_TEST_RESULTS: str = "test_results"
47+
48+
YAML_LANGUAGE_SERVER = "# yaml-language-server: $schema=https://raw.githubusercontent.com/Luftfartsverket/reqstool-client/main/src/reqstool/resources/schemas/v1/reqstool_index.schema.json\n" # noqa: E501
49+
50+
def __init__(self, *args: Any, **kwargs: Any) -> None:
51+
super().__init__(*args, **kwargs)
52+
self.__config_path: Optional[str] = None
53+
54+
def initialize(self, version: str, build_data: Dict[str, Any]) -> None:
55+
"""
56+
Executes custom actions during the build process.
57+
58+
Args:
59+
version (str): The version of the project.
60+
build_data (dict): The build-related data.
61+
"""
62+
self.app.display_info(f"[reqstool] plugin {ReqstoolBuildHook.get_version()} loaded")
63+
64+
self._create_annotations_file()
65+
66+
def finalize(self, version: str, build_data: dict[str, Any], artifact_path: str) -> None:
67+
68+
if artifact_path.endswith(".tar.gz"):
69+
self._append_to_sdist_tar_gz(version=version, build_data=build_data, artifact_path=artifact_path)
70+
71+
def _create_annotations_file(self) -> None:
72+
"""
73+
Generates the annotations.yml file by processing the reqstool decorators.
74+
"""
75+
self.app.display_debug("[reqstool] parsing reqstool decorators")
76+
sources = self.config.get(self.CONFIG_SOURCES, [])
77+
78+
reqstool_output_directory: Path = Path(self.config.get(self.CONFIG_OUTPUT_DIRECTORY, self.OUTPUT_DIR_REQSTOOL))
79+
annotations_file: Path = Path(reqstool_output_directory, self.INPUT_FILE_ANNOTATIONS_YML)
80+
81+
decorator_processor = DecoratorProcessor()
82+
decorator_processor.process_decorated_data(path_to_python_files=sources, output_file=annotations_file)
83+
84+
self.app.display_debug(f"[reqstool] generated {str(annotations_file)}")
85+
86+
def _append_to_sdist_tar_gz(self, version: str, build_data: Dict[str, Any], artifact_path: str) -> None:
87+
"""
88+
Appends to sdist containing the annotations file and other necessary data.
89+
"""
90+
dataset_directory: Path = Path(self.config.get(self.CONFIG_DATASET_DIRECTORY, self.INPUT_DIR_DATASET))
91+
reqstool_output_directory: Path = Path(self.config.get(self.CONFIG_OUTPUT_DIRECTORY, self.OUTPUT_DIR_REQSTOOL))
92+
test_result_patterns: List[str] = self.config.get(self.CONFIG_TEST_RESULTS, [])
93+
requirements_file: Path = Path(dataset_directory, self.INPUT_FILE_REQUIREMENTS_YML)
94+
svcs_file: Path = Path(dataset_directory, self.INPUT_FILE_SOFTWARE_VERIFICATION_CASES_YML)
95+
mvrs_file: Path = Path(dataset_directory, self.INPUT_FILE_MANUAL_VERIFICATION_RESULTS_YML)
96+
annotations_file: Path = Path(reqstool_output_directory, self.INPUT_FILE_ANNOTATIONS_YML)
97+
98+
resources: Dict[str, str] = {}
99+
100+
if not os.path.exists(requirements_file):
101+
msg: str = f"[reqstool] missing mandatory {self.INPUT_FILE_REQUIREMENTS_YML}: {requirements_file}"
102+
raise RuntimeError(msg)
103+
104+
resources["requirements"] = str(requirements_file)
105+
self.app.display_debug(f"[reqstool] added to {self.OUTPUT_SDIST_REQSTOOL_YML}: {requirements_file}")
106+
107+
if os.path.exists(svcs_file):
108+
resources["software_verification_cases"] = str(svcs_file)
109+
self.app.display_debug(f"[reqstool] added to {self.OUTPUT_SDIST_REQSTOOL_YML}: {svcs_file}")
110+
111+
if os.path.exists(mvrs_file):
112+
resources["manual_verification_results"] = str(mvrs_file)
113+
self.app.display_debug(f"[reqstool] added to {self.OUTPUT_SDIST_REQSTOOL_YML}: {mvrs_file}")
114+
115+
if os.path.exists(annotations_file):
116+
resources["annotations"] = str(annotations_file)
117+
self.app.display_debug(f"[reqstool] added to {self.OUTPUT_SDIST_REQSTOOL_YML}: {annotations_file}")
118+
119+
if test_result_patterns:
120+
resources["test_results"] = str(test_result_patterns)
121+
self.app.display_debug(
122+
f"[reqstool] added test_results to {self.OUTPUT_SDIST_REQSTOOL_YML}: {test_result_patterns}"
123+
)
124+
125+
reqstool_yaml_data = {"language": "python", "build": "hatch", "resources": resources}
126+
127+
yaml = YAML()
128+
yaml.default_flow_style = False
129+
reqstool_yml_io = io.BytesIO()
130+
reqstool_yml_io.write(f"{self.YAML_LANGUAGE_SERVER}\n".encode("utf-8"))
131+
reqstool_yml_io.write(f"# version: {self.metadata.version}\n".encode("utf-8"))
132+
133+
self.app.display_debug(f"[reqstool] reqstool config {reqstool_yaml_data}")
134+
135+
yaml.dump(reqstool_yaml_data, reqstool_yml_io)
136+
reqstool_yml_io.seek(0)
137+
138+
# Path to the existing tar.gz file (constructed from metadata)
139+
original_tar_gz_file = os.path.join(
140+
self.directory,
141+
f"{BuilderInterface.normalize_file_name_component(self.metadata.core.raw_name)}"
142+
f"-{self.metadata.version}.tar.gz",
143+
)
144+
145+
self.app.display_debug(f"[reqstool] tarball: {original_tar_gz_file}")
146+
147+
# Step 1: Extract the original tar.gz file to a temporary directory
148+
with tempfile.NamedTemporaryFile(delete=True) as temp_tar_file:
149+
150+
temp_tar_file = temp_tar_file.name # Get the name of the temporary file
151+
152+
self.app.display_debug(f"[reqstool] temporary tar file: {temp_tar_file}")
153+
154+
# Extract the original tar.gz file
155+
with gzip.open(original_tar_gz_file, "rb") as f_in, open(temp_tar_file, "wb") as f_out:
156+
f_out.write(f_in.read())
157+
158+
# Step 2: Open the extracted tar file and append the new file
159+
with tarfile.open(temp_tar_file, "a") as archive:
160+
file_info = tarfile.TarInfo(
161+
name=f"{BuilderInterface.normalize_file_name_component(self.metadata.core.raw_name)}-"
162+
f"{self.metadata.version}/{self.OUTPUT_SDIST_REQSTOOL_YML}"
163+
)
164+
file_info.size = reqstool_yml_io.getbuffer().nbytes
165+
archive.addfile(tarinfo=file_info, fileobj=reqstool_yml_io)
166+
167+
# Step 3: Recompress the updated tar file back into the original .tar.gz format
168+
with open(temp_tar_file, "rb") as f_in, gzip.open(original_tar_gz_file, "wb") as f_out:
169+
f_out.writelines(f_in)
170+
171+
dist_dir: Path = Path(self.directory)
172+
self.app.display_info(
173+
f"[reqstool] added {self.OUTPUT_SDIST_REQSTOOL_YML} to "
174+
f"{os.path.relpath(original_tar_gz_file, dist_dir.parent)}"
175+
)
176+
177+
def get_version() -> str:
178+
try:
179+
ver: str = f"{version('reqstool-python-hatch-plugin')}"
180+
except PackageNotFoundError:
181+
ver: str = "package-not-found"
182+
183+
return ver

src/reqstool_python_hatch_plugin/build_hooks/reqstool_decorators.py

Lines changed: 0 additions & 28 deletions
This file was deleted.

src/reqstool_python_hatch_plugin/hooks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
from hatchling.plugin import hookimpl
44

5-
from reqstool_python_hatch_plugin.build_hooks.reqstool_decorators import ReqstoolDecorators
5+
from reqstool_python_hatch_plugin.build_hooks.reqstool import ReqstoolBuildHook
66

77

88
@hookimpl
99
def hatch_register_build_hook():
10-
return ReqstoolDecorators
10+
return ReqstoolBuildHook

0 commit comments

Comments
 (0)