Skip to content

Commit b5ae85e

Browse files
committed
#37 support Optional / Union types in provides_nodes.
Restore assert_asts_are_equal erroneously removed in the previous commit
1 parent 724f05c commit b5ae85e

5 files changed

Lines changed: 104 additions & 9 deletions

File tree

.github/workflows/pythonapp.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ jobs:
66
build:
77
strategy:
88
matrix:
9-
python: [ '3.7', '3.8', '3.9', '3.10', '3.11', '3.12' ]
9+
python: [ 3.8', '3.9', '3.10', '3.11', '3.12' ]
1010
runs-on: ubuntu-latest
1111
steps:
1212
- uses: actions/checkout@v4

pylasu/model/model.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import inspect
22
from abc import ABC, abstractmethod, ABCMeta
33
from dataclasses import Field, MISSING, dataclass, field
4-
from typing import Optional, Callable, List
4+
from typing import Optional, Callable, List, Union
55

66
from .position import Position, Source
77
from .reflection import Multiplicity, PropertyDescription
88
from ..reflection import getannotations, get_type_arguments, is_sequence_type
9+
from ..reflection.reflection import get_type_origin
910

1011

1112
class internal_property(property):
@@ -79,7 +80,19 @@ def is_internal_property_or_method(value):
7980

8081

8182
def provides_nodes(decl_type):
82-
return isinstance(decl_type, type) and issubclass(decl_type, Node)
83+
if get_type_origin(decl_type) is Union:
84+
provides = None
85+
for tp in get_type_arguments(decl_type):
86+
if tp is type(None):
87+
continue
88+
arg_provides = provides_nodes(tp)
89+
if provides is None:
90+
provides = arg_provides
91+
elif provides != arg_provides:
92+
raise Exception(f"Type {decl_type} mixes nodes and non-nodes")
93+
return provides
94+
else:
95+
return isinstance(decl_type, type) and issubclass(decl_type, Node)
8396

8497

8598
class Concept(ABCMeta):

pylasu/model/traversing.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def walk_descendants(self: Node, walker=walk, restrict_to=Node):
6060

6161
T = TypeVar("T")
6262

63+
6364
@extension_method(Node)
6465
def find_ancestor_of_type(self: Node, target: Type[T]) -> T:
6566
"""Returns the nearest ancestor of this node that is an instance of the target type.

pylasu/testing/testing.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import unittest
2+
3+
from pylasu.model import Node
4+
5+
6+
def assert_asts_are_equal(
7+
case: unittest.TestCase,
8+
expected: Node, actual: Node,
9+
context: str = "<root>", consider_position: bool = False
10+
):
11+
if expected.node_type != actual.node_type:
12+
case.fail(f"{context}: expected node of type {expected.node_type}, "
13+
f"but found {actual.node_type}")
14+
if consider_position:
15+
case.assertEqual(expected.position, actual.position, f"{context}.position")
16+
for expected_property in expected.properties:
17+
try:
18+
actual_property = next(filter(lambda p: p.name == expected_property.name, actual.properties))
19+
except StopIteration:
20+
case.fail(f"No property {expected_property.name} found at {context}")
21+
actual_prop_value = actual_property.value
22+
expected_prop_value = expected_property.value
23+
if expected_property.provides_nodes:
24+
if expected_property.multiple:
25+
assert_multi_properties_are_equal(
26+
case, expected_property, expected_prop_value, actual_prop_value, context, consider_position)
27+
else:
28+
assert_single_properties_are_equal(case, expected_property, expected_prop_value, actual_prop_value,
29+
context, consider_position)
30+
# TODO not yet supported elif expected_property.property_type == PropertyType.REFERENCE:
31+
else:
32+
case.assertEqual(
33+
expected_prop_value, actual_prop_value,
34+
f"{context}, comparing property {expected_property.name} of {expected.node_type}")
35+
36+
37+
def assert_single_properties_are_equal(case, expected_property, expected_prop_value, actual_prop_value, context,
38+
consider_position):
39+
if expected_prop_value is None and actual_prop_value is not None:
40+
case.assertEqual(expected_prop_value, actual_prop_value,
41+
f"{context}.{expected_property.name}")
42+
elif expected_prop_value is not None and actual_prop_value is None:
43+
case.assertEqual(expected_prop_value, actual_prop_value,
44+
f"{context}.{expected_property.name}")
45+
elif expected_prop_value is None and actual_prop_value is None:
46+
# that is ok
47+
pass
48+
else:
49+
case.assertIsInstance(actual_prop_value, Node)
50+
assert_asts_are_equal(
51+
case, expected_prop_value, actual_prop_value,
52+
context=f"{context}.{expected_property.name}",
53+
consider_position=consider_position)
54+
55+
56+
def assert_multi_properties_are_equal(case, expected_property, expected_prop_value, actual_prop_value, context,
57+
consider_position):
58+
# TODO IgnoreChildren
59+
case.assertEqual(actual_prop_value is None, expected_prop_value is None,
60+
f"{context}.{expected_property.name} nullness")
61+
if actual_prop_value is not None and expected_prop_value is not None:
62+
case.assertEqual(len(actual_prop_value), len(expected_prop_value),
63+
f"{context}.{expected_property.name} length")
64+
for expected_it, actual_it, i in \
65+
zip(expected_prop_value, actual_prop_value, range(len(expected_prop_value))):
66+
assert_asts_are_equal(case, expected_it, actual_it, f"{context}[{i}]",
67+
consider_position=consider_position)

tests/model/test_model.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import dataclasses
22
import unittest
3-
from typing import List
3+
from typing import List, Optional, Union
44

55
from pylasu.model import Node, Position, Point
66
from pylasu.model.reflection import Multiplicity
@@ -15,6 +15,7 @@ class SomeNode(Node, Named):
1515
__private__ = 4
1616
ref: Node = None
1717
multiple: List[Node] = dataclasses.field(default_factory=list)
18+
multiple_opt: List[Optional[Node]] = dataclasses.field(default_factory=list)
1819

1920
def __post_init__(self):
2021
self.bar = 5
@@ -30,6 +31,14 @@ class AnotherSymbol(Symbol):
3031
index: int = dataclasses.field(default=None)
3132

3233

34+
@dataclasses.dataclass
35+
class InvalidNode(Node):
36+
attr: int
37+
child: SomeNode
38+
invalid_prop: Union[Node, str] = None
39+
another_child: Node = None
40+
41+
3342
class ModelTest(unittest.TestCase):
3443

3544
def test_reference_by_name_unsolved_str(self):
@@ -150,15 +159,20 @@ def frob_node(_: Node):
150159
pass
151160

152161
pds = [pd for pd in sorted(SomeNode.node_properties, key=lambda x: x.name)]
153-
self.assertEqual(5, len(pds), f"{pds} should be 5")
162+
self.assertEqual(6, len(pds), f"{pds} should be 6")
154163
self.assertEqual("bar", pds[0].name)
155164
self.assertFalse(pds[0].provides_nodes)
156165
self.assertEqual("foo", pds[1].name)
157166
self.assertFalse(pds[1].provides_nodes)
158167
self.assertEqual("multiple", pds[2].name)
159168
self.assertTrue(pds[2].provides_nodes)
160169
self.assertEqual(Multiplicity.MANY, pds[2].multiplicity)
161-
self.assertEqual("name", pds[3].name)
162-
self.assertFalse(pds[3].provides_nodes)
163-
self.assertEqual("ref", pds[4].name)
164-
self.assertTrue(pds[4].provides_nodes)
170+
self.assertEqual("multiple_opt", pds[3].name)
171+
self.assertTrue(pds[3].provides_nodes)
172+
self.assertEqual(Multiplicity.MANY, pds[3].multiplicity)
173+
self.assertEqual("name", pds[4].name)
174+
self.assertFalse(pds[4].provides_nodes)
175+
self.assertEqual("ref", pds[5].name)
176+
self.assertTrue(pds[5].provides_nodes)
177+
178+
self.assertRaises(Exception, lambda: [x for x in InvalidNode.node_properties])

0 commit comments

Comments
 (0)