Skip to content

Commit 57f3b83

Browse files
authored
Merge branch 'main' into chore/update-plan-code-smells
2 parents eb91c42 + b9eeecc commit 57f3b83

9 files changed

Lines changed: 95 additions & 40 deletions

File tree

src/reqstool/command.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717

1818
from reqstool_python_decorators.decorators.decorators import Requirements
1919

20-
from reqstool.commands.exit_codes import EXIT_CODE_ALL_REQS_NOT_IMPLEMENTED, EXIT_CODE_MISSING_REQUIREMENTS_FILE
21-
from reqstool.common.exceptions import MissingRequirementsFileError
20+
from reqstool.commands.exit_codes import (
21+
EXIT_CODE_ALL_REQS_NOT_IMPLEMENTED,
22+
EXIT_CODE_ARTIFACT_ERROR,
23+
EXIT_CODE_MISSING_REQUIREMENTS_FILE,
24+
)
25+
from reqstool.common.exceptions import ArtifactDownloadError, ArtifactExtractionError, MissingRequirementsFileError
2226
from reqstool.commands.generate_json.generate_json import GenerateJsonCommand
2327
from reqstool.commands.report import report
2428
from reqstool.commands.report.criterias.group_by import GroupbyOptions
@@ -405,6 +409,9 @@ def main():
405409
except MissingRequirementsFileError as exc:
406410
logging.fatal(str(exc))
407411
sys.exit(EXIT_CODE_MISSING_REQUIREMENTS_FILE)
412+
except (ArtifactDownloadError, ArtifactExtractionError) as exc:
413+
logging.fatal(str(exc))
414+
sys.exit(EXIT_CODE_ARTIFACT_ERROR)
408415

409416
sys.exit(exit_code)
410417

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
EXIT_CODE_MISSING_REQUIREMENTS_FILE = 1
2+
EXIT_CODE_ARTIFACT_ERROR = 2
23
EXIT_CODE_SYNTAX_VALIDATION_ERROR = 128
34
EXIT_CODE_ALL_REQS_NOT_IMPLEMENTED = 200

src/reqstool/common/exceptions.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,17 @@ class CircularImplementationError(Exception):
2323
def __init__(self, urn: str, chain: list[str]):
2424
self.urn = urn
2525
super().__init__(f"Circular implementation detected: {' -> '.join(chain)} -> {urn}")
26+
27+
28+
class ArtifactDownloadError(Exception):
29+
"""Raised when a remote artifact cannot be downloaded."""
30+
31+
def __init__(self, message: str):
32+
super().__init__(message)
33+
34+
35+
class ArtifactExtractionError(Exception):
36+
"""Raised when a downloaded artifact cannot be extracted."""
37+
38+
def __init__(self, message: str):
39+
super().__init__(message)

src/reqstool/common/utils.py

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -201,30 +201,27 @@ def parse_version(version_str: str, urn_id: UrnId) -> PkgVersion:
201201
raise TypeError(f"Invalid version: {e} for: {urn_id}")
202202

203203

204-
class TempDirectoryUtil:
205-
tmpdir: tempfile.TemporaryDirectory = None
206-
count: int = 0
204+
class TempDirectoryManager:
205+
"""Instance-based temporary directory manager with guaranteed cleanup."""
207206

208-
@staticmethod
209-
def _get_tmpdir() -> tempfile.TemporaryDirectory:
210-
if TempDirectoryUtil.tmpdir is None:
211-
TempDirectoryUtil.tmpdir = tempfile.TemporaryDirectory()
212-
return TempDirectoryUtil.tmpdir
207+
def __init__(self):
208+
self._tmpdir = tempfile.TemporaryDirectory()
209+
self._count = 0
213210

214-
@staticmethod
215-
def get_path() -> Path:
216-
return Path(TempDirectoryUtil._get_tmpdir().name)
211+
def get_path(self) -> Path:
212+
return Path(self._tmpdir.name)
217213

218-
@staticmethod
219-
def get_suffix_path(suffix: str) -> Path:
220-
new_path = Path(
221-
os.path.join(
222-
TempDirectoryUtil._get_tmpdir().name,
223-
str(TempDirectoryUtil.count),
224-
suffix,
225-
)
226-
)
214+
def get_suffix_path(self, suffix: str) -> Path:
215+
new_path = Path(self._tmpdir.name) / str(self._count) / suffix
227216
new_path.mkdir(parents=True, exist_ok=True)
228-
TempDirectoryUtil.count += 1
229-
217+
self._count += 1
230218
return new_path
219+
220+
def cleanup(self):
221+
self._tmpdir.cleanup()
222+
223+
def __enter__(self):
224+
return self
225+
226+
def __exit__(self, *args):
227+
self.cleanup()

src/reqstool/locations/maven_location.py

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

33
import logging
44
import os
5-
import sys
65
from typing import Optional
76

87
from maven_artifact import Artifact, Downloader, RequestException
98
from reqstool_python_decorators.decorators.decorators import Requirements
109

10+
from reqstool.common.exceptions import ArtifactDownloadError, ArtifactExtractionError
1111
from reqstool.common.utils import Utils
1212
from reqstool.locations.location import LocationInterface
1313

@@ -41,13 +41,11 @@ def _make_available_on_localdisk(self, dst_path: str):
4141
if not downloader.download(artifact, filename=dst_path):
4242
raise RequestException(f"Error downloading artifact {artifact} from: {self.url}")
4343
except RequestException as e:
44-
logging.fatal(e.msg)
45-
sys.exit(1)
44+
raise ArtifactDownloadError(f"Error downloading artifact {artifact} from {self.url}: {e.msg}") from e
4645

4746
logging.debug(f"Unzipping {artifact.get_filename(dst_path)} to {dst_path}\n")
4847

4948
try:
5049
return Utils.extract_zip(artifact.get_filename(dst_path), dst_path)
5150
except ValueError as e:
52-
logging.fatal(f"Maven artifact {artifact} from {self.url}: {e}")
53-
sys.exit(1)
51+
raise ArtifactExtractionError(f"Maven artifact {artifact} from {self.url}: {e}") from e

src/reqstool/locations/pypi_location.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import logging
22
import os
33
import re
4-
import sys
54
import tarfile
65
from typing import Optional
76
from urllib.parse import urljoin
87

98
import requests
109
from bs4 import BeautifulSoup
10+
from reqstool.common.exceptions import ArtifactDownloadError, ArtifactExtractionError
1111
from reqstool.common.utils import Utils
1212
from reqstool.locations.location import LocationInterface
1313

@@ -47,15 +47,12 @@ def _make_available_on_localdisk(self, dst_path: str):
4747
logging.debug(f"Extracting {downloaded_file} to {dst_path}\n")
4848
return Utils.extract_targz(downloaded_file, dst_path)
4949
except (ValueError, tarfile.TarError) as e:
50-
logging.fatal(str(e))
51-
sys.exit(1)
50+
raise ArtifactExtractionError(str(e)) from e
5251
except Exception as e:
53-
logging.fatal(
52+
raise ArtifactDownloadError(
5453
f"Error when downloading etc sdist pypi package for {self.package}=={self.version}"
55-
f" in repo {self.url} {'with token' if token else ''}",
56-
e,
57-
)
58-
sys.exit(1)
54+
f" in repo {self.url} {'with token' if token else ''}: {e}"
55+
) from e
5956

6057
@staticmethod
6158
def get_package_url(package, version, base_url, token) -> str:

src/reqstool/model_generators/combined_raw_datasets_generator.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from reqstool_python_decorators.decorators.decorators import Requirements
88

99
from reqstool.common.exceptions import CircularImplementationError, CircularImportError, MissingRequirementsFileError
10-
from reqstool.common.utils import TempDirectoryUtil, Utils
10+
from reqstool.common.utils import TempDirectoryManager, Utils
1111
from reqstool.common.validators.semantic_validator import SemanticValidator
1212
from reqstool.location_resolver.location_resolver import LocationResolver
1313
from reqstool.locations.location import LocationInterface
@@ -34,6 +34,7 @@ def __init__(
3434
initial_location: LocationInterface,
3535
semantic_validator: SemanticValidator,
3636
database: Optional[RequirementsDatabase] = None,
37+
tmpdir_manager: Optional[TempDirectoryManager] = None,
3738
):
3839
self.__level: int = 0
3940
self.__initial_location_handler: LocationResolver = LocationResolver(
@@ -43,11 +44,12 @@ def __init__(
4344
self._parsing_order: List[str] = []
4445
self._parsing_graph: Dict[str, List[Tuple[str, str]]] = defaultdict(list)
4546
self._database = database
47+
self._tmpdir_manager = tmpdir_manager if tmpdir_manager is not None else TempDirectoryManager()
4648
self.combined_raw_datasets = self.__generate()
4749

4850
def __generate(self) -> CombinedRawDataset:
4951
# handle initial source
50-
logging.debug(f"Using temporary path: {TempDirectoryUtil.get_path()}\n")
52+
logging.debug(f"Using temporary path: {self._tmpdir_manager.get_path()}\n")
5153

5254
raw_datasets: Dict[str, RawDataset] = {}
5355

@@ -235,7 +237,7 @@ def __parse_source(self, current_location_handler: LocationResolver) -> RawDatas
235237
mvrs_data = None
236238
automated_tests = None
237239

238-
tmp_path = TempDirectoryUtil.get_suffix_path("can_we_use_urn_here").absolute()
240+
tmp_path = self._tmpdir_manager.get_suffix_path("can_we_use_urn_here").absolute()
239241

240242
actual_tmp_path = current_location_handler.make_available_on_localdisk(dst_path=tmp_path)
241243

src/reqstool/storage/pipeline.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from contextlib import contextmanager
66
from typing import Generator
77

8+
from reqstool.common.utils import TempDirectoryManager
89
from reqstool.common.validators.lifecycle_validator import LifecycleValidator
910
from reqstool.common.validators.semantic_validator import SemanticValidator
1011
from reqstool.locations.location import LocationInterface
@@ -20,13 +21,18 @@ def build_database(
2021
location: LocationInterface,
2122
semantic_validator: SemanticValidator,
2223
filter_data: bool = True,
24+
tmpdir_manager: TempDirectoryManager = None,
2325
) -> Generator[tuple[RequirementsDatabase, CombinedRawDataset], None, None]:
26+
_owns_tmpdir = tmpdir_manager is None
27+
if _owns_tmpdir:
28+
tmpdir_manager = TempDirectoryManager()
2429
db = RequirementsDatabase()
2530
try:
2631
crdg = CombinedRawDatasetsGenerator(
2732
initial_location=location,
2833
semantic_validator=semantic_validator,
2934
database=db,
35+
tmpdir_manager=tmpdir_manager,
3036
)
3137
crd = crdg.combined_raw_datasets
3238

@@ -38,3 +44,5 @@ def build_database(
3844
yield db, crd
3945
finally:
4046
db.close()
47+
if _owns_tmpdir:
48+
tmpdir_manager.cleanup()
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from reqstool.common.utils import TempDirectoryManager
2+
3+
4+
def test_get_suffix_path_returns_unique_paths():
5+
with TempDirectoryManager() as mgr:
6+
p1 = mgr.get_suffix_path("test")
7+
p2 = mgr.get_suffix_path("test")
8+
assert p1 != p2
9+
assert p1.exists()
10+
assert p2.exists()
11+
12+
13+
def test_cleanup_removes_directory():
14+
mgr = TempDirectoryManager()
15+
path = mgr.get_path()
16+
assert path.exists()
17+
mgr.cleanup()
18+
assert not path.exists()
19+
20+
21+
def test_context_manager_cleans_up():
22+
with TempDirectoryManager() as mgr:
23+
path = mgr.get_path()
24+
assert path.exists()
25+
assert not path.exists()
26+
27+
28+
def test_two_instances_are_independent():
29+
with TempDirectoryManager() as mgr1:
30+
with TempDirectoryManager() as mgr2:
31+
assert mgr1.get_path() != mgr2.get_path()

0 commit comments

Comments
 (0)