Skip to content

Commit de5c4e5

Browse files
authored
Merge pull request #17 from bcdev/forman-json_serializable
Refactoring: Introducting JsonSerializable
2 parents b1596ff + 1be1aec commit de5c4e5

14 files changed

Lines changed: 334 additions & 163 deletions

File tree

tests/cli/test_main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def xrlint(self, *args: tuple[str, ...]) -> click.testing.Result:
5858
runner = CliRunner()
5959
result = runner.invoke(main, args)
6060
if not isinstance(result.exception, SystemExit):
61-
self.assertIsNone(None, result.exception)
61+
self.assertIsNone(result.exception)
6262
return result
6363

6464
def test_no_files_no_config(self):

tests/test_config.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,37 @@ def test_from_value_ok(self):
6464
),
6565
)
6666

67+
def test_to_json(self):
68+
config = Config(
69+
name="xXx",
70+
files=["**/*.zarr", "**/*.nc"],
71+
linter_options={"a": 4},
72+
opener_options={"b": 5},
73+
settings={"c": 6},
74+
rules={
75+
"hello/no-spaces-in-titles": RuleConfig(severity=2),
76+
"hello/time-without-tz": RuleConfig(severity=0),
77+
"hello/no-empty-units": RuleConfig(
78+
severity=1, args=(12,), kwargs={"indent": 4}
79+
),
80+
},
81+
)
82+
self.assertEqual(
83+
{
84+
"name": "xXx",
85+
"files": ["**/*.zarr", "**/*.nc"],
86+
"linter_options": {"a": 4},
87+
"opener_options": {"b": 5},
88+
"settings": {"c": 6},
89+
"rules": {
90+
"hello/no-empty-units": [1, 12, {"indent": 4}],
91+
"hello/no-spaces-in-titles": 2,
92+
"hello/time-without-tz": 0,
93+
},
94+
},
95+
config.to_json(),
96+
)
97+
6798
def test_from_value_fails(self):
6899
with pytest.raises(
69100
TypeError,

tests/test_rule.py

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,63 @@ class MyRule3(RuleOp):
5050

5151
with pytest.raises(
5252
ValueError,
53-
match="missing rule metadata, apply define_rule\\(\\) to class MyRule3",
53+
match=r"missing rule metadata, apply define_rule\(\) to class MyRule3",
5454
):
5555
Rule.from_value(MyRule3)
5656

57+
def test_to_json(self):
58+
class MyRule3(RuleOp):
59+
"""This is my 3rd rule."""
60+
61+
rule = Rule.from_value("tests.test_rule")
62+
self.assertEqual("tests.test_rule:export_rule", rule.to_json())
63+
64+
rule = Rule(meta=RuleMeta(name="r3", ref="mymod.rules:r3"), op_class=MyRule3)
65+
self.assertEqual("mymod.rules:r3", rule.to_json())
66+
67+
rule = Rule(meta=RuleMeta(name="r3"), op_class=MyRule3)
68+
self.assertEqual(
69+
{
70+
"meta": {"name": "r3", "version": "0.0.0", "type": "problem"},
71+
"op_class": "<class"
72+
" 'tests.test_rule.RuleTest.test_to_json.<locals>.MyRule3'>",
73+
},
74+
rule.to_json(),
75+
)
76+
77+
78+
class RuleMetaTest(unittest.TestCase):
79+
def test_from_value(self):
80+
rule_meta = RuleMeta.from_value(
81+
{
82+
"name": "r-4",
83+
"version": "2.0.1",
84+
"type": "suggestion",
85+
"description": "Be nice, always.",
86+
}
87+
)
88+
self.assertEqual(
89+
RuleMeta(
90+
name="r-4",
91+
version="2.0.1",
92+
type="suggestion",
93+
description="Be nice, always.",
94+
),
95+
rule_meta,
96+
)
97+
98+
def test_to_json(self):
99+
rule_meta = RuleMeta(name="r1", version="0.1.2", description="Nice one.")
100+
self.assertEqual(
101+
{
102+
"name": "r1",
103+
"version": "0.1.2",
104+
"type": "problem",
105+
"description": "Nice one.",
106+
},
107+
rule_meta.to_json(),
108+
)
109+
57110

58111
class DefineRuleTest(unittest.TestCase):
59112

@@ -79,6 +132,18 @@ def test_with_registry(self):
79132
self.assertIs(rule1, registry["my-rule-1"])
80133
self.assertIs(rule2, registry["my-rule-2"])
81134

135+
# noinspection PyMethodMayBeStatic
136+
def test_fail(self):
137+
with pytest.raises(
138+
TypeError,
139+
match=(
140+
r"component decorated by define_rule\(\)"
141+
r" must be a subclass of RuleOp"
142+
),
143+
):
144+
# noinspection PyTypeChecker
145+
define_rule(op_class=DefineRuleTest)
146+
82147

83148
class RuleConfigTest(TestCase):
84149
def test_defaults(self):

tests/util/test_codec.py

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
get_origin,
1010
Mapping,
1111
TYPE_CHECKING,
12+
Literal,
1213
)
1314
from unittest import TestCase
1415

@@ -27,6 +28,20 @@ class UselessContainer(ValueConstructible):
2728
pass
2829

2930

31+
@dataclass()
32+
class RequiredPropsContainer(MappingConstructible):
33+
x: float
34+
y: float
35+
z: float
36+
37+
38+
class NoTypesContainer(MappingConstructible):
39+
def __init__(self, u, v, w):
40+
self.u = u
41+
self.v = v
42+
self.w = w
43+
44+
3045
@dataclass()
3146
class SimpleTypesContainer(MappingConstructible, JsonSerializable):
3247
a: Any = None
@@ -35,6 +50,7 @@ class SimpleTypesContainer(MappingConstructible, JsonSerializable):
3550
d: float = 0.0
3651
e: str = "abc"
3752
f: type = int
53+
g: Literal[0, 1, True, False, "on", "off"] = 0
3854

3955

4056
@dataclass()
@@ -52,20 +68,6 @@ class UnionTypesContainer(MappingConstructible, JsonSerializable):
5268
m: SimpleTypesContainer | ComplexTypesContainer | None = None
5369

5470

55-
@dataclass()
56-
class RequiredPropsContainer(MappingConstructible, JsonSerializable):
57-
x: float
58-
y: float
59-
z: float
60-
61-
62-
@dataclass()
63-
class NoTypesContainer(MappingConstructible, JsonSerializable):
64-
u = 0
65-
v = 0
66-
w = 0
67-
68-
6971
if TYPE_CHECKING:
7072
# make IDEs and flake8 happy
7173
from xrlint.rule import RuleConfig
@@ -122,13 +124,29 @@ def test_assumptions(self):
122124
class JsonSerializableTest(TestCase):
123125
def test_simple_ok(self):
124126
self.assertEqual(
125-
{"a": None, "b": False, "c": 0, "d": 0.0, "e": "abc", "f": "int"},
127+
{
128+
"a": None,
129+
"b": False,
130+
"c": 0,
131+
"d": 0.0,
132+
"e": "abc",
133+
"f": "<class 'int'>",
134+
"g": 0,
135+
},
126136
SimpleTypesContainer().to_json(),
127137
)
128138
self.assertEqual(
129-
{"a": "?", "b": True, "c": 12, "d": 34.56, "e": "uvw", "f": "bool"},
139+
{
140+
"a": "?",
141+
"b": True,
142+
"c": 12,
143+
"d": 34.56,
144+
"e": "uvw",
145+
"f": "<class 'bool'>",
146+
"g": "off",
147+
},
130148
SimpleTypesContainer(
131-
a="?", b=True, c=12, d=34.56, e="uvw", f=bool
149+
a="?", b=True, c=12, d=34.56, e="uvw", f=bool, g="off"
132150
).to_json(),
133151
)
134152

@@ -144,7 +162,15 @@ def test_complex_ok(self):
144162
)
145163
self.assertEqual(
146164
{
147-
"p": {"a": None, "b": False, "c": 0, "d": 0.0, "e": "abc", "f": "int"},
165+
"p": {
166+
"a": None,
167+
"b": False,
168+
"c": 0,
169+
"d": 0.0,
170+
"e": "abc",
171+
"f": "<class 'int'>",
172+
"g": 0,
173+
},
148174
"q": {"p": True, "q": False},
149175
"r": {
150176
"u": {
@@ -153,27 +179,38 @@ def test_complex_ok(self):
153179
"c": 0,
154180
"d": 0.0,
155181
"e": "abc",
156-
"f": "int",
182+
"f": "<class 'int'>",
183+
"g": 0,
157184
},
158185
"v": {
159186
"a": None,
160187
"b": False,
161188
"c": 0,
162189
"d": 0.0,
163190
"e": "abc",
164-
"f": "int",
191+
"f": "<class 'int'>",
192+
"g": 0,
165193
},
166194
},
167195
"s": [1, 2, 3],
168196
"t": [
169-
{"a": None, "b": False, "c": 5, "d": 6.7, "e": "abc", "f": "int"},
197+
{
198+
"a": None,
199+
"b": False,
200+
"c": 5,
201+
"d": 6.7,
202+
"e": "abc",
203+
"f": "<class 'int'>",
204+
"g": 0,
205+
},
170206
{
171207
"a": None,
172208
"b": False,
173209
"c": 8,
174210
"d": 9.1,
175211
"e": "abc",
176-
"f": "SimpleTypesContainer",
212+
"f": "<class 'tests.util.test_codec.SimpleTypesContainer'>",
213+
"g": 0,
177214
},
178215
],
179216
"u": None,
@@ -193,7 +230,7 @@ class Problematic(JsonSerializable):
193230
" None|bool|int|float|str|dict|list|tuple, but got object"
194231
),
195232
):
196-
Problematic(data=object()).to_json(name="problematic")
233+
Problematic(data=object()).to_json(value_name="problematic")
197234

198235

199236
class ValueConstructibleTest(TestCase):
@@ -263,11 +300,32 @@ def test_useless_fail(self):
263300
):
264301
UselessContainer.from_value(UselessContainer, value_name="utc")
265302

303+
def test_required_props_ok(self):
304+
rpc = RequiredPropsContainer.from_value({"x": 12.0, "y": 23.0, "z": 34.0})
305+
self.assertEqual(RequiredPropsContainer(x=12.0, y=23.0, z=34.0), rpc)
306+
307+
# noinspection PyMethodMayBeStatic
308+
def test_required_props_fail(self):
309+
with pytest.raises(
310+
TypeError,
311+
match=(
312+
r"missing value for required property rpc.y"
313+
r" of type RequiredPropsContainer \| dict\[str, Any\]"
314+
),
315+
):
316+
RequiredPropsContainer.from_value({"x": 12.0, "z": 34.0}, "rpc")
317+
318+
def test_no_types_ok(self):
319+
ntc = NoTypesContainer.from_value(dict(u=True, v=654, w="abc"))
320+
self.assertEqual(True, ntc.u)
321+
self.assertEqual(654, ntc.v)
322+
self.assertEqual("abc", ntc.w)
323+
266324

267325
class MappingConstructibleTest(TestCase):
268326

269327
def test_simple_ok(self):
270-
kwargs = dict(a="?", b=True, c=12, d=34.56, e="uvw", f=bytes)
328+
kwargs = dict(a="?", b=True, c=12, d=34.56, e="uvw", f=bytes, g="on")
271329
container = SimpleTypesContainer(**kwargs)
272330
self.assertEqual(container, SimpleTypesContainer.from_value(kwargs))
273331
self.assertIs(container, SimpleTypesContainer.from_value(container))
@@ -339,6 +397,12 @@ def test_simple_fail(self):
339397
):
340398
SimpleTypesContainer.from_value({"b": None}, value_name="stc")
341399

400+
with pytest.raises(
401+
TypeError,
402+
match=r"stc.g must be one of 0, 1, True, False, 'on', 'off', but got 74",
403+
):
404+
SimpleTypesContainer.from_value({"g": 74}, value_name="stc")
405+
342406
with pytest.raises(
343407
TypeError, match="x is not a member of stc of type SimpleTypesContainer"
344408
):
@@ -439,6 +503,7 @@ def test_resolves_types(self):
439503
"d",
440504
"e",
441505
"f",
506+
"g",
442507
"p",
443508
"q",
444509
"r",

tests/util/test_todict.py

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

xrlint/cli/engine.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def print_config_for_file(self, file_path: str) -> None:
136136
file_path: A file path.
137137
"""
138138
config = self.get_config_for_file(file_path)
139-
config_json_obj = config.to_dict() if config is not None else None
139+
config_json_obj = config.to_json() if config is not None else None
140140
click.echo(json.dumps(config_json_obj, indent=2))
141141

142142
def verify_datasets(self, files: Iterable[str]) -> Iterator[Result]:

0 commit comments

Comments
 (0)