ron

Python implementation of Rusty Object Notation (RON).

See: https://docs.rs/ron/latest/ron/

This package allows working with RON data in two ways:

  1. Strict Mapping: Deserializing RON directly into Python dataclasses and Enums.
  2. Dynamic Access: Traversing the raw data structure using a ron.models.RonObject.

Well, and direct AST access, using ron.models.RonValue.

Quickstart

You'll most probably use a FromRonMixin mixin to bind RON data to typed Python definitions:

>>> from dataclasses import dataclass
>>> from ron import FromRonMixin
>>>
>>> # 1. Define Enums (variants) using inheritance
>>> class Difficulty: pass
>>>
>>> @dataclass
... class Easy(Difficulty): pass
>>> @dataclass
... class Hard(Difficulty): pass
>>> @dataclass
... class Custom(Difficulty):
...     factor: float
>>>
>>> # 2. Define schema
>>> @dataclass
... class GameConfig(FromRonMixin):
...     title: str
...     difficulty: Difficulty
>>>
>>> # 3. Load from RON
>>> ron_data = '''
...     GameConfig(
...         title: "Dungeon Crawler",
...         difficulty: Easy,
...     )
... '''
>>> config = GameConfig.from_ron(ron_data)
>>> config.title
'Dungeon Crawler'
>>> config.difficulty
Easy()

And yes, unlike traditional python enums, RON enums can have data attached to them.

>>> ron_data = '''
...     GameConfig(
...         title: "Dungeon Crawler",
...         difficulty: Custom(0.7),
...     )
... '''
>>> config = GameConfig.from_ron(ron_data)
>>> config.difficulty
Custom(factor=0.7)

Dynamic Access

If you do not have a strict schema, use parse_ron to get a RonObject wrapper. It abstracts away specific key types (allowing string/tuple lookups) and provides type-checked accessors.

>>> from ron import parse_ron
>>> obj = parse_ron('(config: (resolution: (1920, 1080)))')
>>> # Access nested fields using standard Python keys
>>> obj["config"]["resolution"][0].expect_int()
1920

Parser API

parse_ron produces an immutable tree (wrapped into ron.models.RonObject) of:

Known limitations

Extensions are not implemented yet.

Reference

 1"""
 2Python implementation of Rusty Object Notation (RON).
 3
 4See: <https://docs.rs/ron/latest/ron/>
 5
 6This package allows working with RON data in two ways:
 71. Strict Mapping: Deserializing RON directly into Python dataclasses and Enums.
 82. Dynamic Access: Traversing the raw data structure using a
 9`ron.models.RonObject`.
10
11Well, and direct AST access, using `ron.models.RonValue`.
12
13Quickstart
14---
15
16You'll most probably use a `FromRonMixin` mixin to bind RON data to typed
17Python definitions:
18
19>>> from dataclasses import dataclass
20>>> from ron import FromRonMixin
21>>>
22>>> # 1. Define Enums (variants) using inheritance
23>>> class Difficulty: pass
24>>>
25>>> @dataclass
26... class Easy(Difficulty): pass
27>>> @dataclass
28... class Hard(Difficulty): pass
29>>> @dataclass
30... class Custom(Difficulty):
31...     factor: float
32>>>
33>>> # 2. Define schema
34>>> @dataclass
35... class GameConfig(FromRonMixin):
36...     title: str
37...     difficulty: Difficulty
38>>>
39>>> # 3. Load from RON
40>>> ron_data = '''
41...     GameConfig(
42...         title: "Dungeon Crawler",
43...         difficulty: Easy,
44...     )
45... '''
46>>> config = GameConfig.from_ron(ron_data)
47>>> config.title
48'Dungeon Crawler'
49>>> config.difficulty
50Easy()
51
52And yes, unlike traditional python enums, RON enums can have data attached to
53them.
54>>> ron_data = '''
55...     GameConfig(
56...         title: "Dungeon Crawler",
57...         difficulty: Custom(0.7),
58...     )
59... '''
60>>> config = GameConfig.from_ron(ron_data)
61>>> config.difficulty
62Custom(factor=0.7)
63
64Dynamic Access
65---
66
67If you do not have a strict schema, use `parse_ron` to get a `RonObject` wrapper.
68It abstracts away specific key types (allowing string/tuple lookups) and provides
69type-checked accessors.
70
71>>> from ron import parse_ron
72>>> obj = parse_ron('(config: (resolution: (1920, 1080)))')
73>>> # Access nested fields using standard Python keys
74>>> obj["config"]["resolution"][0].expect_int()
751920
76
77Parser API
78---
79`parse_ron` produces an immutable tree (wrapped into `ron.models.RonObject`)
80of:
81* `ron.models.RonStruct` (includes span information for error reporting)
82* `ron.models.RonMap`
83* `ron.models.RonSeq`
84* `ron.models.RonOptional`
85* Primitives (`int`, `float`, `str`, `bool`, `ron.models.RonChar`)
86
87Known limitations
88---
89Extensions are not implemented yet.
90
91Reference
92---
93"""
94
95from ron.mapper import FromRonMixin, from_ron
96from ron.parser import parse_ron
97
98__all__ = ["models", "parse_ron", "from_ron", "FromRonMixin"]
def parse_ron(src_text: str, *, with_spans: bool = False) -> ron.models.RonObject:
25def parse_ron(src_text: str, *, with_spans: bool = False) -> RonObject:
26    """
27    Parses the string and returns a `ron.models.RonObject`.
28    """
29    input_stream = InputStream(src_text)
30    lexer = RonLexer(input_stream)
31    lexer.removeErrorListeners()
32    lexer.addErrorListener(RonErrorListener())
33    stream = CommonTokenStream(lexer)
34    # stream.fill()
35    #
36    # for token in stream.tokens:
37    #     print(f"Token: {token.type} -> '{token.text}'")
38    parser = RonParser(stream)
39    parser.removeErrorListeners()
40    parser.addErrorListener(RonErrorListener())
41
42    tree = parser.root()
43    if parser.getNumberOfSyntaxErrors() > 0:
44        raise RonSyntaxError("Failed to parse RON data: Syntax Error")
45
46    line_index = [0] + [
47        i + 1 for i, char in enumerate(src_text) if char == "\n"
48    ]
49    visitor = RonConverter(with_spans=with_spans, line_index=line_index)
50    val = visitor.visit(tree)  # type: ignore
51    assert is_ron_value(val), f"visitor returned {val} :/"
52
53    return RonObject(val)

Parses the string and returns a ron.models.RonObject.

def from_ron(ron_val: ron.models.RonObject | RonValue, target_type: Type[T]) -> T:
 20def from_ron[T](
 21    ron_val: RonObject | RonValue, target_type: typing.Type[T]
 22) -> T:
 23    """
 24    Function you can use to convert your `ron.models.RonObject` or
 25    `ron.models.RonValue` into your specific dataclass.
 26
 27    >>> from dataclasses import dataclass
 28    >>> from ron import from_ron, parse_ron
 29    >>> @dataclass
 30    ... class Point:
 31    ...     x: int
 32    ...     y: int
 33    ...
 34    >>> obj = parse_ron("(x: 5, y: 13)")
 35    >>> point = from_ron(obj, Point)
 36    >>> point.x
 37    5
 38    >>> point.y
 39    13
 40    """
 41    if isinstance(ron_val, RonObject):
 42        ron_val = ron_val.v
 43
 44    target_origin = typing.get_origin(target_type)
 45    target_args = typing.get_args(target_type)
 46
 47    # 1) Turn RonOptional into Python's Optional[]
 48    if target_origin is typing.Union or (
 49        hasattr(typing, "UnionType")
 50        and isinstance(target_type, typing.UnionType)
 51    ):
 52        if isinstance(ron_val, RonOptional):
 53            match ron_val.value:
 54                case None:
 55                    return typing.cast(T, None)
 56                case v:
 57                    inner_type = next(
 58                        t for t in target_args if t is not type(None)
 59                    )
 60                    return from_ron(v, inner_type)
 61
 62    # 2) Turn RonStruct into target's subclass
 63    #
 64    # Basically, enum handling
 65    if isinstance(ron_val, RonStruct) and not is_dataclass(target_type):
 66        for subclass in target_type.__subclasses__():
 67            if subclass.__name__ == ron_val.name:
 68                return from_ron(ron_val, subclass)
 69        raise ValueError(
 70            f"No subclass found for {ron_val.name} in {target_type}"
 71        )
 72
 73    # 3) Turn RonStruct into target class
 74    #
 75    # Target type is supposed to be a dataclass
 76    if is_dataclass(target_type):
 77        if not isinstance(ron_val, RonStruct):
 78            raise TypeError(
 79                f"Expected RonStruct for {target_type}, got {type(ron_val)}"
 80            )
 81
 82        if ron_val.name is not None and ron_val.name != target_type.__name__:
 83            raise ValueError(
 84                f"Name mismatch: RON '{ron_val.name}' vs Class '{target_type.__name__}'"
 85            )
 86
 87        field_hints = typing.get_type_hints(target_type)
 88        kwargs = {}
 89
 90        if isinstance(ron_val._fields, frozendict):
 91            # if it's a struct with named fields, map every target's field to
 92            # struct's field
 93            for field in dataclasses.fields(target_type):
 94                if field.name in ron_val._fields:
 95                    kwargs[field.name] = from_ron(
 96                        ron_val._fields[field.name],
 97                        field_hints[field.name],
 98                    )
 99            return target_type(**kwargs)
100        elif isinstance(ron_val._fields, tuple):
101            # if it's a struct with unnamed fields, just rely on order
102            cls_fields = dataclasses.fields(target_type)
103            for i, val in enumerate(ron_val._fields):
104                f_name = cls_fields[i].name
105                kwargs[f_name] = from_ron(val, field_hints[f_name])
106            return target_type(**kwargs)
107
108    # 4) Turn RonTuple/tuple into sequence target class
109    if target_origin in (list, tuple, typing.Sequence):
110        item_type = target_args[0] if target_args else typing.Any
111
112        match ron_val:
113            case RonSeq(elements):
114                source_data = elements
115            case _:
116                raise RuntimeError(f"can't convert {ron_val} to sequence")
117
118        return target_origin(from_ron(item, item_type) for item in source_data)
119
120    # 5) Turn RonMap into dict target class
121    if target_origin in (dict, typing.Mapping):
122        key_type = target_args[0]
123        item_type = target_args[1]
124
125        assert isinstance(ron_val, RonMap), "can't convert {ron_val} to mapping"
126        source_map = ron_val.entries.items()
127
128        return target_origin(
129            (from_ron(key, key_type), from_ron(item, item_type))
130            for (key, item) in source_map
131        )
132
133    # 6) Turn primitives
134    match ron_val:
135        case RonChar(value):
136            assert target_type is str, (
137                f"tried to convert {ron_val!r} to {target_type}"
138            )
139            return typing.cast(T, value)
140        case bool() | int() | float() | str():
141            assert target_type in (int, float, str, bool), (
142                f"tried to convert {ron_val!r} to {target_type}"
143            )
144            return typing.cast(T, ron_val)
145        case other_val:
146            raise RuntimeError(f"unexpected {other_val} to {target_type}")

Function you can use to convert your ron.models.RonObject or ron.models.RonValue into your specific dataclass.

>>> from dataclasses import dataclass
>>> from ron import from_ron, parse_ron
>>> @dataclass
... class Point:
...     x: int
...     y: int
...
>>> obj = parse_ron("(x: 5, y: 13)")
>>> point = from_ron(obj, Point)
>>> point.x
5
>>> point.y
13
class FromRonMixin:
149class FromRonMixin:
150    """
151    Mixin that adds a capability to load a class from RON string
152    """
153
154    @classmethod
155    def from_ron(cls, ron_string: str) -> typing.Self:
156        """
157        Load your type from a string.
158        >>> from dataclasses import dataclass
159        >>> from ron import FromRonMixin
160        >>>
161        >>> @dataclass
162        ... class Point(FromRonMixin):
163        ...     x: int
164        ...     y: int
165        ...
166        >>> point = Point.from_ron("(x: 5, y: 13)")
167        >>> point.x
168        5
169        >>> point.y
170        13
171        """
172        parsed = parse_ron(ron_string)
173        res = from_ron(parsed, cls)
174        return res

Mixin that adds a capability to load a class from RON string

@classmethod
def from_ron(cls, ron_string: str) -> Self:
154    @classmethod
155    def from_ron(cls, ron_string: str) -> typing.Self:
156        """
157        Load your type from a string.
158        >>> from dataclasses import dataclass
159        >>> from ron import FromRonMixin
160        >>>
161        >>> @dataclass
162        ... class Point(FromRonMixin):
163        ...     x: int
164        ...     y: int
165        ...
166        >>> point = Point.from_ron("(x: 5, y: 13)")
167        >>> point.x
168        5
169        >>> point.y
170        13
171        """
172        parsed = parse_ron(ron_string)
173        res = from_ron(parsed, cls)
174        return res

Load your type from a string.

>>> from dataclasses import dataclass
>>> from ron import FromRonMixin
>>>
>>> @dataclass
... class Point(FromRonMixin):
...     x: int
...     y: int
...
>>> point = Point.from_ron("(x: 5, y: 13)")
>>> point.x
5
>>> point.y
13