Skip to content

Commit 3f2aaf6

Browse files
committed
fix: typing closer to native python
1 parent bed8ba6 commit 3f2aaf6

8 files changed

Lines changed: 931 additions & 3 deletions

File tree

‎.github/workflows/test.yml‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,22 @@ jobs:
1919
run: |
2020
uv run --group dev ruff check .
2121
22+
type-check:
23+
name: Type checking
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
28+
- name: Install uv
29+
uses: astral-sh/setup-uv@v5
30+
31+
- name: Run type checking
32+
run: |
33+
make type-check
34+
2235
test:
2336
name: Run tests
24-
needs: [lint]
37+
needs: [lint, type-check]
2538
runs-on: ubuntu-latest
2639
strategy:
2740
matrix:

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ wheels/
1414
htmlcov/
1515
.ruff_cache/
1616
.pytest_cache/
17+
.mypy_cache/
1718

1819
# Experimental notebooks
1920
**_test.ipynb

‎Makefile‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1+
fix:
2+
uv run ruff format && uv run ruff check --fix-only --unsafe-fixes && uv run ruff check
3+
14
test:
25
uv run --group dev pytest --cov=yamlium .
36

47
test-report: test
58
coverage html && \
6-
open htmlcov/index.html
9+
open htmlcov/index.html
10+
11+
type-check:
12+
uv run --group dev mypy tests/test_type_checking.py --check-untyped-defs
13+
14+
test-all: test type-check

‎pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ classifiers=[
1919
[dependency-groups]
2020
dev = [
2121
"ipykernel>=6.29.5",
22+
"mypy>=1.13.0",
2223
"pytest>=8.3.5",
2324
"pytest-cov>=6.1.1",
2425
"pyyaml>=6.0.2",

‎tests/test_type_checking.py‎

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
"""
2+
Static type checking test file for yamlium.
3+
4+
This file is used to verify that type checkers (mypy, pyright) correctly
5+
understand the types in yamlium. Run with:
6+
mypy tests/test_type_checking.py
7+
pyright tests/test_type_checking.py
8+
9+
This file should have NO type errors when checked.
10+
"""
11+
12+
from typing import Any
13+
from yamlium import parse
14+
from yamlium.nodes import Key, Mapping, Scalar, Sequence
15+
16+
17+
def test_mapping_basic_types() -> None:
18+
"""Test that Mapping accepts all basic Python types like dict does."""
19+
m: Mapping = Mapping()
20+
21+
# All these should be valid (like dict)
22+
m["string_key"] = "string value"
23+
m["int_key"] = 42
24+
m["float_key"] = 3.14
25+
m["bool_key"] = True
26+
m["none_key"] = None
27+
28+
# Nested structures
29+
m["dict_key"] = {"nested": "value"}
30+
m["list_key"] = [1, 2, 3]
31+
32+
# Complex nested structures
33+
m["complex"] = {
34+
"users": [
35+
{"name": "Alice", "age": 30},
36+
{"name": "Bob", "age": 25},
37+
],
38+
"count": 2,
39+
"active": True,
40+
}
41+
42+
43+
def test_mapping_with_key_object() -> None:
44+
"""Test that Mapping accepts both string and Key objects as keys."""
45+
m: Mapping = Mapping()
46+
key: Key = Key("mykey")
47+
48+
m[key] = "value"
49+
m["string_key"] = "value"
50+
51+
# Both should work for access
52+
_val1: Any = m[key]
53+
_val2: Any = m["string_key"]
54+
55+
56+
def test_mapping_update() -> None:
57+
"""Test that Mapping.update works like dict.update."""
58+
m: Mapping = Mapping()
59+
60+
# Update with dict of mixed types
61+
m.update(
62+
{
63+
"string": "text",
64+
"number": 42,
65+
"list": [1, 2, 3],
66+
"dict": {"nested": True},
67+
"bool": False,
68+
"none": None,
69+
}
70+
)
71+
72+
73+
def test_mapping_methods() -> None:
74+
"""Test that Mapping methods have correct signatures."""
75+
m: Mapping = Mapping()
76+
m["key"] = "value"
77+
78+
# get method
79+
_val1: Any = m.get("key")
80+
_val2: Any = m.get("nonexistent", "default")
81+
82+
# pop method
83+
_popped: Any = m.pop("key")
84+
85+
# keys, values, items
86+
_keys = m.keys()
87+
_values = m.values()
88+
_items = m.items()
89+
90+
# iteration
91+
for _k in m:
92+
pass
93+
94+
for _k, _v in m.items():
95+
pass
96+
97+
98+
def test_sequence_basic_types() -> None:
99+
"""Test that Sequence accepts all basic Python types like list does."""
100+
s: Sequence = Sequence()
101+
102+
# Setting items with different types
103+
s.append("string")
104+
s.append(42)
105+
s.append(3.14)
106+
s.append(True)
107+
s.append(None)
108+
s.append({"nested": "dict"})
109+
s.append([1, 2, 3])
110+
111+
# The issue that was reported: setting list values
112+
s[0] = "new string"
113+
s[1] = 123
114+
s[2] = ["a", "b", "c"] # This should NOT cause a type error
115+
s[3] = {"key": "value"}
116+
117+
# Complex nested structures
118+
s.append(
119+
{
120+
"users": [
121+
{"name": "Alice"},
122+
{"name": "Bob"},
123+
]
124+
}
125+
)
126+
127+
128+
def test_sequence_methods() -> None:
129+
"""Test that Sequence methods have correct signatures."""
130+
s: Sequence = Sequence()
131+
132+
# append
133+
s.append("item")
134+
s.append(42)
135+
s.append([1, 2])
136+
s.append({"key": "value"})
137+
138+
# extend
139+
s.extend([1, 2, 3])
140+
s.extend(["a", "b", "c"])
141+
s.extend([{"dict": True}, [1, 2]])
142+
143+
# iteration
144+
for _item in s:
145+
pass
146+
147+
# indexing
148+
_val1: Any = s[0]
149+
_val2: Any = s[-1]
150+
151+
# length
152+
_length: int = len(s)
153+
154+
155+
def test_parsed_yaml_typing() -> None:
156+
"""Test that parsed YAML has correct types."""
157+
yaml_str = """
158+
name: John
159+
age: 30
160+
active: true
161+
scores:
162+
- 90
163+
- 85
164+
- 92
165+
metadata:
166+
created: 2024-01-01
167+
tags:
168+
- python
169+
- yaml
170+
"""
171+
result: Mapping = parse(yaml_str)
172+
173+
# Access with type annotations
174+
_name: Any = result["name"]
175+
_age: Any = result["age"]
176+
_active: Any = result["active"]
177+
_scores: Any = result["scores"]
178+
_metadata: Any = result["metadata"]
179+
180+
# Nested access
181+
_created: Any = result["metadata"]["created"]
182+
_tags: Any = result["metadata"]["tags"]
183+
184+
185+
def test_node_preservation() -> None:
186+
"""Test that Node objects can be assigned."""
187+
m: Mapping = Mapping()
188+
s: Sequence = Sequence()
189+
190+
# Assigning Node objects should work
191+
scalar: Scalar = Scalar(_value="test")
192+
nested_map: Mapping = Mapping()
193+
nested_seq: Sequence = Sequence()
194+
195+
m["scalar"] = scalar
196+
m["map"] = nested_map
197+
m["seq"] = nested_seq
198+
199+
s.append(scalar)
200+
s.append(nested_map)
201+
s.append(nested_seq)
202+
203+
204+
def test_dict_like_behavior() -> None:
205+
"""Test that Mapping behaves like dict in type system."""
206+
m: Mapping = Mapping()
207+
208+
# Should accept same operations as dict
209+
m["key1"] = "value1"
210+
m["key2"] = 42
211+
m["key3"] = [1, 2, 3]
212+
m["key4"] = {"nested": True}
213+
214+
# Check membership
215+
_exists: bool = "key1" in m
216+
_not_exists: bool = "other" not in m
217+
218+
# Length
219+
_size: int = len(m)
220+
221+
222+
def test_list_like_behavior() -> None:
223+
"""Test that Sequence behaves like list in type system."""
224+
s: Sequence = Sequence()
225+
226+
# Should accept same operations as list
227+
s.append("item1")
228+
s.append(42)
229+
s.append([1, 2])
230+
s.append({"key": "value"})
231+
232+
s[0] = "new value"
233+
s[1] = 123
234+
s[2] = ["nested", "list"] # The reported bug - should work
235+
236+
# Extend
237+
s.extend([1, 2, 3])
238+
239+
# Length
240+
_size: int = len(s)
241+
242+
243+
def test_edge_cases() -> None:
244+
"""Test edge cases with types."""
245+
m: Mapping = Mapping()
246+
s: Sequence = Sequence()
247+
248+
# Empty containers
249+
m["empty_dict"] = {}
250+
m["empty_list"] = []
251+
252+
s.append({})
253+
s.append([])
254+
255+
# Deeply nested
256+
m["deep"] = {"l1": {"l2": {"l3": "value"}}}
257+
s.append([[1, 2], [3, 4]])
258+
259+
# Mixed nesting
260+
m["mixed"] = [
261+
{"name": "Alice", "scores": [90, 85]},
262+
{"name": "Bob", "scores": [88, 91]},
263+
]
264+
265+
266+
def test_any_value_assignment() -> None:
267+
"""Test that Any type values can be assigned."""
268+
269+
def get_dynamic_value() -> Any:
270+
return {"dynamic": "value"}
271+
272+
m: Mapping = Mapping()
273+
s: Sequence = Sequence()
274+
275+
dynamic_val: Any = get_dynamic_value()
276+
277+
# Should accept Any typed values
278+
m["dynamic"] = dynamic_val
279+
s.append(dynamic_val)
280+
s[0] = dynamic_val
281+
282+
283+
# This file should pass mypy and pyright checks with no errors

0 commit comments

Comments
 (0)