Skip to content

Commit c0dcda9

Browse files
fix(#16): use Pydantic V2 models (#37)
* using pydantic v2, tested, WIP * adjusted model config for V2, added models for V1 with wrapper to keep compability with V2 * Collecting coverage with tox to include both pydantic v1 and pydantic v2 * small fixes * fixes based on PR review, + ruff formatting --------- Co-authored-by: Charles <[email protected]>
1 parent 21ed02f commit c0dcda9

File tree

6 files changed

+156
-64
lines changed

6 files changed

+156
-64
lines changed

.pre-commit-config.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,14 @@ repos:
6464
- id: cov-check
6565
name: Coverage
6666
language: system
67-
entry: pytest -v --cov=arta --cov-fail-under=90
67+
entry: coverage report -m --fail-under=90
68+
types: [python]
69+
pass_filenames: false
70+
always_run: true
71+
- id: cov-erase
72+
name: Coverage - Erase
73+
language: system
74+
entry: coverage erase
6875
types: [python]
6976
pass_filenames: false
7077
always_run: true

pyproject.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,21 @@ exclude = ["tests/*"]
9393

9494
[tool.ruff.format]
9595
docstring-code-format = true
96+
97+
[tool.coverage.paths]
98+
source = [
99+
"src/arta",
100+
"*/.tox/*/lib/python*/site-packages/arta",
101+
"*/.tox/pypy*/site-packages/arta",
102+
"*/.tox\\*\\Lib\\site-packages\\arta",
103+
"*/src/arta",
104+
"*\\src\\arta"]
105+
106+
[tool.coverage.run]
107+
branch = true
108+
parallel = false
109+
source = ["arta"]
110+
omit = ["*/tests/*"]
111+
112+
[tool.coverage.report]
113+
omit = ["tests"]

src/arta/_engine.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ def __init__(
8787
# Initialize directly with a rules dict
8888
if rules_dict is not None:
8989
# Data validation
90-
RulesDict.parse_obj(rules_dict)
90+
# RulesDict.parse_obj(rules_dict)
91+
RulesDict.model_validate(rules_dict)
9192

9293
# Edge cases data validation
9394
if not isinstance(rules_dict, dict):
@@ -104,9 +105,8 @@ def __init__(
104105
# Load config in attribute
105106
config_dict = load_config(config_path)
106107

107-
if config_dict is not None:
108-
# Data validation
109-
config: Configuration = Configuration(**config_dict)
108+
# Data validation
109+
config: Configuration = Configuration.model_validate(config_dict)
110110

111111
if config.parsing_error_strategy is not None:
112112
# Set parsing error handling strategy from config
@@ -125,7 +125,7 @@ def __init__(
125125
# Dictionary of condition instances (k: condition id, v: instance), built from config data
126126
if len(std_condition_functions) > 0:
127127
std_condition_instances = self._build_std_conditions(
128-
config=config.dict(), condition_functions_dict=std_condition_functions
128+
config=config.model_dump(), condition_functions_dict=std_condition_functions
129129
)
130130

131131
# User-defined/custom conditions
@@ -150,7 +150,7 @@ def __init__(
150150
self.rules = self._build_rules(
151151
std_condition_instances=std_condition_instances,
152152
action_functions=action_functions,
153-
config=config.dict(),
153+
config=config.model_dump(),
154154
factory_mapping_classes=factory_mapping_classes,
155155
)
156156

@@ -288,9 +288,9 @@ def _build_rules(
288288
Return a dictionary of Rule instances built from the configuration.
289289
290290
Args:
291-
rule_sets: Sets of rules to be loaded in the Rules Engine (as needed by further uses).
291+
# rule_sets: Sets of rules to be loaded in the Rules Engine (as needed by further uses).
292292
std_condition_instances: Dictionary of condition instances (k: condition id, v: StandardCondition instance)
293-
actions_dict: Dictionary of action functions (k: action name, v: Callable)
293+
action_functions: Dictionary of action functions (k: action name, v: Callable)
294294
config: Dictionary of the imported configuration from yaml files.
295295
factory_mapping_classes: A mapping dictionary (k: condition conf. key, v: custom class object)
296296

src/arta/models.py

Lines changed: 116 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,74 +3,143 @@
33
Note: Having no "from __future__ import annotations" here is wanted (pydantic compatibility).
44
"""
55

6-
from typing import Any, Callable, Dict, List, Optional
6+
from typing import Annotated, Any, Callable, Dict, List, Optional
77

8-
try:
9-
from pydantic import v1 as pydantic
10-
except ImportError:
11-
import pydantic # type: ignore
8+
import pydantic
9+
from pydantic.version import VERSION
1210

1311
from arta.utils import ParsingErrorStrategy
1412

13+
if not VERSION.startswith("1."):
14+
# ----------------------------------
15+
# For instantiation using rules_dict
16+
class RuleRaw(pydantic.BaseModel):
17+
"""Pydantic model for validating a rule."""
1518

16-
# ----------------------------------
17-
# For instantiation using rules_dict
18-
class RuleRaw(pydantic.BaseModel):
19-
"""Pydantic model for validating a rule."""
19+
condition: Optional[Callable]
20+
condition_parameters: Optional[Dict[str, Any]]
21+
action: Callable
22+
action_parameters: Optional[Dict[str, Any]]
2023

21-
condition: Optional[Callable]
22-
condition_parameters: Optional[Dict[str, Any]]
23-
action: Callable
24-
action_parameters: Optional[Dict[str, Any]]
24+
model_config = pydantic.ConfigDict(extra="forbid")
2525

26-
class Config:
27-
extra = "forbid"
26+
class RulesGroup(pydantic.RootModel): # noqa
27+
"""Pydantic model for validating a rules group."""
2828

29+
root: Dict[str, RuleRaw]
2930

30-
class RulesGroup(pydantic.BaseModel):
31-
"""Pydantic model for validating a rules group."""
31+
class RulesDict(pydantic.RootModel): # noqa
32+
"""Pydantic model for validating rules dict instanciation."""
3233

33-
__root__: Dict[str, RuleRaw]
34+
root: Dict[str, RulesGroup]
3435

36+
# ----------------------------------
37+
# For instantiation using config_path
38+
class Condition(pydantic.BaseModel):
39+
"""Pydantic model for validating a condition."""
3540

36-
class RulesDict(pydantic.BaseModel):
37-
"""Pydantic model for validating rules dict instanciation."""
41+
description: str
42+
validation_function: str
43+
condition_parameters: Optional[Dict[str, Any]] = None
3844

39-
__root__: Dict[str, RulesGroup]
45+
class RulesConfig(pydantic.BaseModel):
46+
"""Pydantic model for validating a rule group from config file."""
4047

48+
condition: Optional[str] = None
49+
simple_condition: Optional[str] = None
50+
action: Annotated[str, pydantic.StringConstraints(to_lower=True)] # type: ignore
51+
action_parameters: Optional[Any] = None
4152

42-
# ----------------------------------
43-
# For instantiation using config_path
44-
class Condition(pydantic.BaseModel):
45-
"""Pydantic model for validating a condition."""
53+
model_config = pydantic.ConfigDict(extra="allow")
4654

47-
description: str
48-
validation_function: str
49-
condition_parameters: Optional[Dict[str, Any]]
55+
class Configuration(pydantic.BaseModel):
56+
"""Pydantic model for validating configuration files."""
5057

58+
conditions: Optional[Dict[str, Condition]] = None
59+
conditions_source_modules: Optional[List[str]] = None
60+
actions_source_modules: List[str]
61+
custom_classes_source_modules: Optional[List[str]] = None
62+
condition_factory_mapping: Optional[Dict[str, str]] = None
63+
rules: Dict[str, Dict[str, Dict[str, RulesConfig]]]
64+
parsing_error_strategy: Optional[ParsingErrorStrategy] = None
5165

52-
class RulesConfig(pydantic.BaseModel):
53-
"""Pydantic model for validating a rule group from config file."""
66+
model_config = pydantic.ConfigDict(extra="ignore")
5467

55-
condition: Optional[str]
56-
simple_condition: Optional[str]
57-
action: pydantic.constr(to_lower=True) # type: ignore
58-
action_parameters: Optional[Any]
68+
@pydantic.field_validator("rules", mode="before") # noqa
69+
def upper_key(cls, vl): # noqa
70+
"""Validate and uppercase keys for RulesConfig"""
71+
for k, v in vl.items():
72+
for kk, vv in v.items():
73+
for key, rules in [*vv.items()]:
74+
if key != str(key).upper():
75+
del vl[k][kk][key]
76+
vl[k][kk][str(key).upper()] = rules
77+
return vl
5978

60-
class Config:
61-
extra = "allow"
79+
else:
6280

81+
class BaseModelV2(pydantic.BaseModel):
82+
"""Wrapper to expose missed methods used elsewhere in the code"""
6383

64-
class Configuration(pydantic.BaseModel):
65-
"""Pydantic model for validating configuration files."""
84+
model_dump: Callable = pydantic.BaseModel.dict # noqa
6685

67-
conditions: Optional[Dict[str, Condition]]
68-
conditions_source_modules: Optional[List[str]]
69-
actions_source_modules: List[str]
70-
custom_classes_source_modules: Optional[List[str]]
71-
condition_factory_mapping: Optional[Dict[str, str]]
72-
rules: Dict[str, Dict[str, Dict[pydantic.constr(to_upper=True), RulesConfig]]] # type: ignore
73-
parsing_error_strategy: Optional[ParsingErrorStrategy]
86+
@classmethod
87+
def model_validate(cls, obj): # noqa
88+
return cls.parse_obj(obj) # noqa
7489

75-
class Config:
76-
extra = "ignore"
90+
# ----------------------------------
91+
# For instantiation using rules_dict
92+
class RuleRaw(BaseModelV2): # type: ignore[no-redef]
93+
"""Pydantic model for validating a rule."""
94+
95+
condition: Optional[Callable]
96+
condition_parameters: Optional[Dict[str, Any]]
97+
action: Callable
98+
action_parameters: Optional[Dict[str, Any]]
99+
100+
class Config:
101+
extra = "forbid"
102+
103+
class RulesGroup(pydantic.BaseModel): # type: ignore[no-redef] # noqa
104+
"""Pydantic model for validating a rules group."""
105+
106+
__root__: Dict[str, RuleRaw] # noqa
107+
108+
class RulesDict(BaseModelV2): # type: ignore[no-redef] # noqa
109+
"""Pydantic model for validating rules dict instanciation."""
110+
111+
__root__: Dict[str, RulesGroup] # noqa
112+
113+
# ----------------------------------
114+
# For instantiation using config_path
115+
class Condition(BaseModelV2): # type: ignore[no-redef]
116+
"""Pydantic model for validating a condition."""
117+
118+
description: str
119+
validation_function: str
120+
condition_parameters: Optional[Dict[str, Any]]
121+
122+
class RulesConfig(BaseModelV2): # type: ignore[no-redef]
123+
"""Pydantic model for validating a rule group from config file."""
124+
125+
condition: Optional[str]
126+
simple_condition: Optional[str]
127+
action: pydantic.constr(to_lower=True) # type: ignore
128+
action_parameters: Optional[Any]
129+
130+
class Config:
131+
extra = "allow"
132+
133+
class Configuration(BaseModelV2): # type: ignore[no-redef]
134+
"""Pydantic model for validating configuration files."""
135+
136+
conditions: Optional[Dict[str, Condition]]
137+
conditions_source_modules: Optional[List[str]]
138+
actions_source_modules: List[str]
139+
custom_classes_source_modules: Optional[List[str]]
140+
condition_factory_mapping: Optional[Dict[str, str]]
141+
rules: Dict[str, Dict[str, Dict[pydantic.constr(to_upper=True), RulesConfig]]] # type: ignore
142+
parsing_error_strategy: Optional[ParsingErrorStrategy]
143+
144+
class Config:
145+
extra = "ignore"

tests/unit/test_engine_errors.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@
66
from arta import RulesEngine
77
from arta.exceptions import ConditionExecutionError, RuleExecutionError
88

9-
try:
10-
from pydantic import v1 as pydantic
11-
except ImportError:
12-
import pydantic # type: ignore
9+
import pydantic
1310

1411

1512
@pytest.mark.parametrize(

tox.ini

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@ env_list =
66
py310
77
py39
88
pydantic1-{py39,py310,py311,py312}
9-
109
[testenv]
1110
description = run unit tests
1211
deps =
1312
pytest
13+
pytest-cov
1414
pydantic>=2.0.0
15-
commands = pytest tests
15+
commands = pytest tests --cov --cov-append
1616

17-
[testenv:pydantic1]
17+
[testenv:pydantic1-{py39,py310,py311,py312}]
1818
description = check backward compatibility with pydantic < 2.0.0
1919
deps =
2020
pytest
21+
pytest-cov
2122
pydantic<2.0.0
22-
commands = pytest tests
23+
commands = pytest tests --cov --cov-append

0 commit comments

Comments
 (0)