Skip to content

Commit 87e6fed

Browse files
jimisolaJimisola Laursen
andauthored
refactor: extract shared lifecycle parsing and version parsing utilities (#303)
* refactor: extract shared lifecycle parsing and version parsing utilities - Add LifecycleData.from_dict() classmethod to centralize lifecycle parsing from YAML dicts, replacing duplicated inline blocks in requirements_model_generator.py and svcs_model_generator.py - Add Utils.parse_version() to centralize version string parsing, replacing duplicated __parse_req_version and __parse_svc_version private methods - Remove unused LIFECYCLESTATE, InvalidVersion, and Version imports from both generators - Add 4 unit tests for LifecycleData.from_dict() (TDD) Signed-off-by: jimisola <jimisola@jimisola.com> * style: fix import ordering in utils.py Signed-off-by: jimisola <jimisola@jimisola.com> --------- Signed-off-by: jimisola <jimisola@jimisola.com> Co-authored-by: Jimisola Laursen <jimisola.laursen@resurs.se>
1 parent 500c6b8 commit 87e6fed

5 files changed

Lines changed: 51 additions & 36 deletions

File tree

src/reqstool/common/dataclasses/lifecycle.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
# Copyright © LFV
22

3+
from __future__ import annotations
4+
35
from dataclasses import dataclass, field
46
from enum import Enum, unique
7+
from typing import Optional
58

69

710
@unique
@@ -24,3 +27,12 @@ class LIFECYCLESTATE(Enum):
2427
class LifecycleData:
2528
reason: str = field(default=str)
2629
state: LIFECYCLESTATE = field(default=LIFECYCLESTATE.EFFECTIVE)
30+
31+
@classmethod
32+
def from_dict(cls, data: Optional[dict]) -> LifecycleData:
33+
if data is None:
34+
return cls(state=LIFECYCLESTATE.EFFECTIVE, reason=None)
35+
return cls(
36+
state=LIFECYCLESTATE(data["state"]),
37+
reason=data.get("reason"),
38+
)

src/reqstool/common/utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from zipfile import ZipFile
1212

1313
import requests
14+
from packaging.version import InvalidVersion, Version as PkgVersion
1415
from requests_file import FileAdapter
1516

1617
from reqstool.common.dataclasses.urn_id import UrnId
@@ -334,6 +335,13 @@ def get_accessible_nodes(graph: Dict[str, List[str]], start_node: str):
334335

335336
return accessible_nodes_dict
336337

338+
@staticmethod
339+
def parse_version(version_str: str, urn_id: UrnId) -> PkgVersion:
340+
try:
341+
return PkgVersion(version_str)
342+
except InvalidVersion as e:
343+
raise TypeError(f"Invalid version: {e} for: {urn_id}")
344+
337345

338346
class TempDirectoryUtil:
339347
tmpdir: tempfile.TemporaryDirectory = None

src/reqstool/model_generators/requirements_model_generator.py

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,11 @@
55
from enum import Enum, unique
66
from typing import Dict, List, Set
77

8-
from packaging.version import InvalidVersion, Version
98
from reqstool_python_decorators.decorators.decorators import Requirements
109
from ruamel.yaml import YAML
1110

1211
from reqstool.commands.exit_codes import EXIT_CODE_SYNTAX_VALIDATION_ERROR
13-
from reqstool.common.dataclasses.lifecycle import LIFECYCLESTATE, LifecycleData
12+
from reqstool.common.dataclasses.lifecycle import LifecycleData
1413
from reqstool.common.dataclasses.urn_id import UrnId
1514
from reqstool.common.utils import Utils
1615
from reqstool.common.validators.semantic_validator import SemanticValidator
@@ -342,14 +341,6 @@ def __parse_requirements(self, data): # NOSONAR
342341
rationale = req["rationale"] if "rationale" in req else None
343342
# Check if implementation is defined, or set it to True
344343
implementation = req["implementation"] if "implementation" in req else IMPLEMENTATION.IN_CODE.value
345-
# Get lifecycle variables or use defaults
346-
if "lifecycle" in req:
347-
lifecycle_state = LIFECYCLESTATE(req["lifecycle"]["state"])
348-
lifecycle_reason = req["lifecycle"]["reason"] if "reason" in req["lifecycle"] else None
349-
else:
350-
lifecycle_state = LIFECYCLESTATE.EFFECTIVE
351-
lifecycle_reason = None
352-
353344
urn_id = UrnId(urn=urn, id=req["id"])
354345
req_data = RequirementData(
355346
id=urn_id,
@@ -360,17 +351,11 @@ def __parse_requirements(self, data): # NOSONAR
360351
implementation=IMPLEMENTATION(implementation),
361352
categories=[CATEGORIES(c) for c in req["categories"]],
362353
references=refs_data,
363-
revision=self.__parse_req_version(version=req["revision"], urn_id=urn_id),
364-
lifecycle=LifecycleData(state=lifecycle_state, reason=lifecycle_reason),
354+
revision=Utils.parse_version(version_str=req["revision"], urn_id=urn_id),
355+
lifecycle=LifecycleData.from_dict(req.get("lifecycle")),
365356
)
366357

367358
if req_data.id not in r_reqs:
368359
r_reqs[req_data.id] = req_data
369360

370361
return r_reqs
371-
372-
def __parse_req_version(self, version: str, urn_id: UrnId) -> Version:
373-
try:
374-
return Version(version)
375-
except InvalidVersion as e:
376-
raise TypeError(f"Invalid version: {e} for: {urn_id}")

src/reqstool/model_generators/svcs_model_generator.py

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@
33
import sys
44
from typing import Dict, Set
55

6-
from packaging.version import InvalidVersion, Version
76
from ruamel.yaml import YAML
87

98
from reqstool.commands.exit_codes import EXIT_CODE_SYNTAX_VALIDATION_ERROR
10-
from reqstool.common.dataclasses.lifecycle import LIFECYCLESTATE, LifecycleData
9+
from reqstool.common.dataclasses.lifecycle import LifecycleData
1110
from reqstool.common.dataclasses.urn_id import UrnId
1211
from reqstool.common.utils import Utils
1312
from reqstool.common.validators.semantic_validator import SemanticValidator
@@ -52,23 +51,15 @@ def __parse_svcs(self, data) -> dict[UrnId, SVCData]:
5251
for case in data["cases"]:
5352
urn_id = UrnId(urn=self.urn, id=case["id"])
5453

55-
# Get lifecycle variables or use defaults
56-
if "lifecycle" in case:
57-
lifecycle_state = LIFECYCLESTATE(case["lifecycle"]["state"])
58-
lifecycle_reason = case["lifecycle"]["reason"] if "reason" in case["lifecycle"] else None
59-
else:
60-
lifecycle_state = LIFECYCLESTATE.EFFECTIVE
61-
lifecycle_reason = None
62-
6354
svc = SVCData(
6455
id=urn_id,
6556
requirement_ids=Utils.convert_ids_to_urn_id(ids=case["requirement_ids"], urn=self.urn),
6657
title=case["title"],
6758
description=case["description"] if "description" in case else None,
6859
verification=VERIFICATIONTYPES(case["verification"]),
6960
instructions=case["instructions"] if "instructions" in case else None,
70-
revision=self.__parse_svc_version(version=case["revision"], urn_id=urn_id),
71-
lifecycle=LifecycleData(state=lifecycle_state, reason=lifecycle_reason),
61+
revision=Utils.parse_version(version_str=case["revision"], urn_id=urn_id),
62+
lifecycle=LifecycleData.from_dict(case.get("lifecycle")),
7263
)
7364

7465
if svc.id not in r_result:
@@ -120,9 +111,3 @@ def __parse_svc_filters(self, data) -> Dict[str, SVCFilter]: # NOSONAR
120111
r_filters[urn] = svc_filter
121112

122113
return r_filters
123-
124-
def __parse_svc_version(self, version: str, urn_id: UrnId) -> Version:
125-
try:
126-
return Version(version)
127-
except InvalidVersion as e:
128-
raise TypeError(f"Invalid version: {e} for: {urn_id}")
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from reqstool.common.dataclasses.lifecycle import LIFECYCLESTATE, LifecycleData
2+
3+
4+
def test_from_dict_with_state_and_reason():
5+
result = LifecycleData.from_dict({"state": "deprecated", "reason": "replaced by REQ_002"})
6+
assert result.state == LIFECYCLESTATE.DEPRECATED
7+
assert result.reason == "replaced by REQ_002"
8+
9+
10+
def test_from_dict_with_state_only():
11+
result = LifecycleData.from_dict({"state": "effective"})
12+
assert result.state == LIFECYCLESTATE.EFFECTIVE
13+
assert result.reason is None
14+
15+
16+
def test_from_dict_draft_state():
17+
result = LifecycleData.from_dict({"state": "draft", "reason": "under review"})
18+
assert result.state == LIFECYCLESTATE.DRAFT
19+
assert result.reason == "under review"
20+
21+
22+
def test_from_dict_none_returns_default():
23+
result = LifecycleData.from_dict(None)
24+
assert result.state == LIFECYCLESTATE.EFFECTIVE
25+
assert result.reason is None

0 commit comments

Comments
 (0)