Skip to content

Commit c265a47

Browse files
authored
fix: support additional config hints (#690)
1 parent 649f318 commit c265a47

File tree

2 files changed

+197
-8
lines changed

2 files changed

+197
-8
lines changed

diracx-core/src/diracx/core/config/schema.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,24 +35,34 @@ def legacy_adaptor(cls, v):
3535
"""Apply transformations to interpret the legacy DIRAC CFG format."""
3636
if not os.environ.get("DIRAC_COMPAT_ENABLE_CS_CONVERSION"):
3737
return v
38+
3839
# If we're running with DIRAC_COMPAT_ENABLE_CS_CONVERSION set we apply
3940
# some hacky transformations to the content to ease the transition from
4041
# a CFG file. This is done by analysing the type hints as strings
4142
# though ideally we should parse the type hints properly.
43+
convertible_hints = {
44+
"list[str]",
45+
"SerializableSet[str]",
46+
"SerializableSet[SecurityProperty]",
47+
}
4248
for field, hint in cls.__annotations__.items():
4349
# Convert comma separated lists to actual lists
4450
if hint.startswith("set"):
4551
raise NotImplementedError("Use SerializableSet instead!")
46-
if hint in {
47-
"list[str]",
48-
"SerializableSet[str]",
49-
"SerializableSet[SecurityProperty]",
50-
} and isinstance(v.get(field), str):
52+
53+
if field not in v:
54+
continue
55+
56+
# Get the base hint without the optional part
57+
base_hint = hint.split(" | ")[0].strip()
58+
59+
# Convert comma-separated strings to lists
60+
if base_hint in convertible_hints and isinstance(v[field], str):
5161
v[field] = [x.strip() for x in v[field].split(",") if x.strip()]
62+
5263
# If the field is optional and the value is "None" convert it to None
53-
if "| None" in hint and field in v:
54-
if v[field] == "None":
55-
v[field] = None
64+
if "| None" in hint and v[field] == "None":
65+
v[field] = None
5666
return v
5767

5868

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
"""Test the legacy_adaptor functionality in BaseModel for CS conversion."""
2+
3+
from __future__ import annotations
4+
5+
import os
6+
7+
import pytest
8+
from pydantic import ValidationError
9+
10+
from diracx.core.config.schema import BaseModel, SerializableSet
11+
from diracx.core.properties import NORMAL_USER, PRODUCTION_MANAGEMENT, SecurityProperty
12+
13+
14+
# Note: In the following tests we use BaseModel coming from the config schema module!
15+
class SimpleModel(BaseModel):
16+
"""Test model with various field types that should be handled by legacy_adaptor."""
17+
18+
# Non-optional list
19+
required_list: list[str]
20+
21+
# Optional list
22+
optional_list: list[str] | None = None
23+
24+
# Non-optional SerializableSet
25+
required_set: SerializableSet[str]
26+
27+
# Optional SerializableSet
28+
optional_set: SerializableSet[str] | None = None
29+
30+
# Optional SerializableSet with SecurityProperty
31+
optional_security_set: SerializableSet[SecurityProperty] | None = None
32+
33+
34+
def test_legacy_adaptor_without_env_var():
35+
"""Test that legacy_adaptor does nothing when DIRAC_COMPAT_ENABLE_CS_CONVERSION is not set."""
36+
# Ensure the env var is not set
37+
os.environ.pop("DIRAC_COMPAT_ENABLE_CS_CONVERSION", None)
38+
39+
with pytest.raises(ValidationError) as exc_info:
40+
SimpleModel.model_validate(
41+
{
42+
"required_list": "item1, item2",
43+
"required_set": "item3, item4",
44+
}
45+
)
46+
47+
# Verify the error is about type validation
48+
errors = exc_info.value.errors()
49+
assert any(error["type"] == "list_type" for error in errors)
50+
51+
52+
def test_legacy_adaptor_required_list(monkeypatch):
53+
"""Test that required list[str] fields are converted from comma-separated strings."""
54+
monkeypatch.setenv("DIRAC_COMPAT_ENABLE_CS_CONVERSION", "yes")
55+
56+
model = SimpleModel.model_validate(
57+
{
58+
"required_list": "xroot, root",
59+
"required_set": "item1, item2",
60+
}
61+
)
62+
63+
assert model.required_list == ["xroot", "root"]
64+
assert model.required_set == {"item1", "item2"}
65+
66+
67+
def test_legacy_adaptor_optional_list_with_value(monkeypatch):
68+
"""Test that optional list[str] | None fields are converted from comma-separated strings.
69+
70+
This is the failing test case that reproduces the issue from the certification environment.
71+
"""
72+
monkeypatch.setenv("DIRAC_COMPAT_ENABLE_CS_CONVERSION", "yes")
73+
74+
model = SimpleModel.model_validate(
75+
{
76+
"required_list": "item1",
77+
"required_set": "item2",
78+
"optional_list": "xroot, root",
79+
}
80+
)
81+
82+
assert model.optional_list == ["xroot", "root"]
83+
84+
85+
def test_legacy_adaptor_optional_list_with_none(monkeypatch):
86+
"""Test that optional list fields can be None."""
87+
monkeypatch.setenv("DIRAC_COMPAT_ENABLE_CS_CONVERSION", "yes")
88+
89+
model = SimpleModel.model_validate(
90+
{
91+
"required_list": "item1",
92+
"required_set": "item2",
93+
"optional_list": None,
94+
}
95+
)
96+
97+
assert model.optional_list is None
98+
99+
100+
def test_legacy_adaptor_optional_set_with_value(monkeypatch):
101+
"""Test that optional SerializableSet[str] | None fields are converted."""
102+
monkeypatch.setenv("DIRAC_COMPAT_ENABLE_CS_CONVERSION", "yes")
103+
104+
model = SimpleModel.model_validate(
105+
{
106+
"required_list": "item1",
107+
"required_set": "item2",
108+
"optional_set": "value1, value2, value3",
109+
}
110+
)
111+
112+
assert model.optional_set == {"value1", "value2", "value3"}
113+
114+
115+
def test_legacy_adaptor_optional_security_property_set(monkeypatch):
116+
"""Test that optional SerializableSet[SecurityProperty] | None fields are converted."""
117+
monkeypatch.setenv("DIRAC_COMPAT_ENABLE_CS_CONVERSION", "yes")
118+
119+
model = SimpleModel.model_validate(
120+
{
121+
"required_list": "item1",
122+
"required_set": "item2",
123+
"optional_security_set": "NormalUser, ProductionManagement",
124+
}
125+
)
126+
127+
assert model.optional_security_set == {
128+
NORMAL_USER,
129+
PRODUCTION_MANAGEMENT,
130+
}
131+
132+
133+
def test_legacy_adaptor_whitespace_handling(monkeypatch):
134+
"""Test that whitespace is properly stripped from comma-separated values."""
135+
monkeypatch.setenv("DIRAC_COMPAT_ENABLE_CS_CONVERSION", "yes")
136+
137+
model = SimpleModel.model_validate(
138+
{
139+
"required_list": " item1 , item2 , item3 ",
140+
"required_set": "value1,value2, value3 ",
141+
"optional_list": "xroot, root, gsiftp",
142+
}
143+
)
144+
145+
assert model.required_list == ["item1", "item2", "item3"]
146+
assert model.required_set == {"value1", "value2", "value3"}
147+
assert model.optional_list == ["xroot", "root", "gsiftp"]
148+
149+
150+
def test_legacy_adaptor_empty_values(monkeypatch):
151+
"""Test that empty values are properly filtered out."""
152+
monkeypatch.setenv("DIRAC_COMPAT_ENABLE_CS_CONVERSION", "yes")
153+
154+
model = SimpleModel.model_validate(
155+
{
156+
"required_list": "item1,,item2,,,item3", # Empty values between commas
157+
"required_set": "value1, , value2", # Empty value with spaces
158+
}
159+
)
160+
161+
assert model.required_list == ["item1", "item2", "item3"]
162+
assert model.required_set == {"value1", "value2"}
163+
164+
165+
def test_legacy_adaptor_already_list(monkeypatch):
166+
"""Test that fields that are already lists are not modified."""
167+
monkeypatch.setenv("DIRAC_COMPAT_ENABLE_CS_CONVERSION", "yes")
168+
169+
model = SimpleModel.model_validate(
170+
{
171+
"required_list": ["already", "a", "list"],
172+
"required_set": ["already", "a", "list"],
173+
"optional_list": ["also", "a", "list"],
174+
}
175+
)
176+
177+
assert model.required_list == ["already", "a", "list"]
178+
assert model.required_set == {"already", "a", "list"}
179+
assert model.optional_list == ["also", "a", "list"]

0 commit comments

Comments
 (0)