ron.models

High-level utilities for traversing and manipulating Rusty Object Notation (RON) data.

This module features RonObject and semi-internal parser types (RonValue). Although it is possible to use primitive syntax types directly, you will likely prefer using RonObject's __getitem__ implementation, especially for nested lookups. It abstracts away specific key and container types, allowing you to use plain Python objects to access the data.

To access the raw syntax leaves, RonObject exposes a family of expect_ methods (e.g., expect_int, expect_map).

These methods assert that the value matches the expected type, raising a ValueError if it does not (which also makes type checkers happy).

  1"""
  2High-level utilities for traversing and manipulating Rusty Object Notation (RON)
  3data.
  4
  5This module features `RonObject` and semi-internal parser types (`RonValue`).
  6Although it is possible to use primitive syntax types directly, you will likely
  7prefer using `RonObject`'s `__getitem__` implementation, especially for nested
  8lookups. It abstracts away specific key and container types, allowing you to use
  9plain Python objects to access the data.
 10
 11To access the raw syntax leaves, `RonObject` exposes a family of `expect_`
 12methods (e.g., `expect_int`, `expect_map`).
 13
 14These methods assert that the value
 15matches the expected type, raising a `ValueError` if it does not (which also
 16makes type checkers happy).
 17"""
 18
 19from __future__ import annotations
 20
 21from dataclasses import dataclass
 22from typing import Any, Callable, Literal, TypeGuard, override
 23
 24from frozendict import frozendict
 25
 26type RonValue = (
 27    RonStruct
 28    | RonSeq
 29    | RonMap
 30    | RonOptional
 31    | RonChar
 32    | int
 33    | float
 34    | str
 35    | bool
 36)
 37"""
 38@public Union of all supported RON types, including primitives and containers.
 39"""
 40
 41
 42def is_ron_value(val: Any) -> TypeGuard[RonValue]:
 43    """
 44    Type-guard that checks that your value is indeed of `RonValue`
 45
 46    Use with typecheckers.
 47    """
 48    # simple top-level check
 49    if isinstance(
 50        val,
 51        (
 52            RonStruct,
 53            RonSeq,
 54            RonMap,
 55            RonOptional,
 56            RonChar,
 57            int,
 58            float,
 59            str,
 60            bool,
 61        ),
 62    ):
 63        return True
 64
 65    return False
 66
 67
 68@dataclass
 69class RonObject:
 70    """
 71    A wrapper around `RonValue` enabling convenient traversal.
 72
 73    It allows you to access nested fields using standard Python types as keys,
 74    automatically handling the necessary conversions to RON-specific types.
 75
 76    Read `RonObject.__getitem__`() docs for more.
 77    """
 78
 79    v: RonValue
 80    """@private field holding the internal value"""
 81
 82    def expect_map(self) -> RonMap:
 83        """Returns an underlying map or **raises** a `ValueError`"""
 84        if isinstance(self.v, RonMap):
 85            return self.v
 86        raise ValueError(f"Value '{self}' is not a map")
 87
 88    def expect_struct(self) -> RonStruct:
 89        """Returns an underlying struct or **raises** a `ValueError`"""
 90        if isinstance(self.v, RonStruct):
 91            return self.v
 92        raise ValueError(f"Value '{self}' is not a struct")
 93
 94    def expect_int(self) -> int:
 95        """Returns an underlying int or **raises** a `ValueError`"""
 96        if isinstance(self.v, int):
 97            return self.v
 98        raise ValueError(f"Value '{self}' is not an integer")
 99
100    def expect_float(self) -> float:
101        """Returns an underlying float or **raises** a `ValueError`"""
102        if isinstance(self.v, float):
103            return self.v
104        raise ValueError(f"Value '{self}' is not a float")
105
106    def expect_str(self) -> str:
107        """Returns an underlying str or **raises** a `ValueError`"""
108        if isinstance(self.v, str):
109            return self.v
110        raise ValueError(f"Value '{self}' is not a string")
111
112    def expect_bool(self) -> bool:
113        """Returns an underlying bool or **raises** a `ValueError`"""
114        if isinstance(self.v, bool):
115            return self.v
116        raise ValueError(f"Value '{self}' is not a boolean")
117
118    def expect_tuple(self) -> RonSeq:
119        """Returns an underlying tuple or **raises** a `ValueError`"""
120        if isinstance(self.v, RonSeq) and self.v.kind == "tuple":
121            return self.v
122        raise ValueError(f"Value '{self}' is not a ron tuple")
123
124    def expect_list(self) -> RonSeq:
125        """Returns an underlying list or **raises** a `ValueError`"""
126        if isinstance(self.v, RonSeq) and self.v.kind == "list":
127            return self.v
128        raise ValueError(f"Value '{self}' is not a ron list")
129
130    def maybe(self) -> RonObject | None:
131        """
132        Transposes a RON optional into Python `Optional`.
133
134        # Returns
135        `RonObject` | `None`: The inner value, if present.
136
137        # Raises
138        - `ValueError`: If the value is not an option.
139
140        Examples:
141        >>> from ron import parse_ron
142        >>> obj = parse_ron('Some(42)')
143        >>> result = obj.maybe()
144        >>> assert result is not None
145        >>> result.expect_int()
146        42
147
148        >>> obj = parse_ron('None')
149        >>> obj.maybe() is None
150        True
151
152        >>> # Raises error if called on non-option types
153        >>> obj = parse_ron('42')
154        >>> obj.maybe()
155        Traceback (most recent call last):
156            ...
157        ValueError: Value ... is not an option
158        """
159        if isinstance(self.v, RonOptional):
160            return RonObject(self.v.value) if self.v.value is not None else None
161        raise ValueError(f"Value '{self}' is not an option")
162
163    def __getitem__(
164        self,
165        item: RonValue
166        | tuple[RonValue, ...]
167        | list[RonValue]
168        | dict[Any, Any]
169        | None
170        | RonObject,
171    ) -> RonObject:
172        """
173        @public Escape hatch to traverse the RON object.
174
175        This method inspects the underlying container (Map, Struct, or Sequence)
176        and (if needed) automatically converts the provided `item` into the
177        correct RON type required for the lookup.
178
179        For example, passing a `str` to a struct will look up that field name,
180        and passing a `tuple` to a map will convert it to a `RonSeq` key.
181
182        # Returns
183        `RonObject`: A new wrapper around the retrieved value, enabling
184        chained access.
185
186        # Raises
187        - `TypeError`: If the value is not a container, or if the key type
188            is invalid for the current container.
189        - `KeyError`: If the item is missing (for Maps and Structs).
190        - `IndexError`: If the index is out of bounds (for Sequences).
191
192        Examples:
193        >>> from ron import parse_ron
194
195        >>> # Struct access: use string keys for field names
196        >>> obj = parse_ron('( id: 42, name: "foo" )')
197        >>> obj["id"].expect_int()
198        42
199
200        >>> # Map access: use standard Python literals
201        >>> obj = parse_ron('{ "key": "value" }')
202        >>> obj["key"].expect_str()
203        'value'
204
205        >>> # Sequence access: use integer indices
206        >>> obj = parse_ron('[10, 20, 30]')
207        >>> obj[1].expect_int()
208        20
209
210        >>> # Complex keys: Python tuples are automatically coerced to RON tuples
211        >>> obj = parse_ron('{ (1, "a"): "found" }')
212        >>> obj[(1, "a")].expect_str()
213        'found'
214
215        >>> # Nested chaining
216        >>> obj = parse_ron('( config: (version: 1) )')
217        >>> obj["config"]["version"].expect_int()
218        1
219
220        >>> # Unit structs
221        >>> obj = parse_ron('{ King: "Crown" }')
222        >>> obj["King"].expect_str()
223        'Crown'
224
225        >>> # And even optionals
226        >>> obj = parse_ron('{ Some("King"): "Crown", None: "Hat" }')
227        >>> obj["King"].expect_str()
228        'Crown'
229        >>> obj[None].expect_str()
230        'Hat'
231
232        >>> # Nested coercion doesn't work for now
233        >>> obj = parse_ron('{ Some(King): "Crown", None: "Hat" }')
234        >>> obj["King"].expect_str() # doctest: +IGNORE_EXCEPTION_DETAIL
235        Traceback (most recent call last):
236            ...
237        KeyError: ... coerced as Some("King"), not Some(King)
238
239        >>> # If you need nested keys, parse them
240        >>> obj[parse_ron("Some(King)")].expect_str()
241        'Crown'
242        """
243
244        val = self.v
245        if isinstance(item, RonObject):
246            item = item.v
247
248        # 1. Pick a proper container (and its key type)
249        key = None
250        if isinstance(val, RonStruct):
251            container = val._fields
252            if type(container) is frozendict:
253                key = container.key(0)
254        elif isinstance(val, RonMap):
255            container = val.entries
256            key = container.key(0)
257        elif isinstance(val, RonSeq):
258            container = val.elements
259        elif isinstance(val, tuple):
260            container = val
261        else:
262            raise TypeError(f"{self}[{item}]")
263
264        # 2. Convert index to a proper ron type, if needed
265        if isinstance(item, tuple):
266            match key:
267                case RonSeq():
268                    item = RonSeq(elements=item, kind="tuple")
269                case RonStruct(name):
270                    item = RonStruct(name, _fields=item, spans=None)
271                case _:
272                    raise TypeError(f"{self}[{item}]: Unexpected index type")
273        if isinstance(item, list):
274            item = RonSeq(elements=tuple(item), kind="list")
275        if isinstance(item, dict):
276            match key:
277                case RonMap():
278                    item = RonMap(entries=frozendict(item))
279                case RonStruct(name):
280                    item = RonStruct(name, _fields=frozendict(item), spans=None)
281                case _:
282                    raise TypeError(f"{self}[{item}]: Unexpected index type")
283
284        # Handle optionals and unit structs
285        if type(key) is RonOptional and type(item) is not RonOptional:
286            item = RonOptional(item)
287        elif type(key) is RonStruct and type(item) is str:
288            item = RonStruct(item, _fields=tuple(), spans=None)
289        elif item is None:
290            item = RonOptional(None)
291
292        # 3. Do the index magic and wrap in RonObject to prolong the chain
293        if isinstance(container, frozendict):
294            try:
295                result = container[item]
296            except (KeyError, TypeError):
297                raise KeyError(
298                    f"container={container}, key={item}, heuristic_key={key}"
299                )
300            return RonObject(result)
301        elif isinstance(container, tuple):
302            if not isinstance(item, int):
303                raise TypeError(
304                    f"List indices must be integers, got {type(item).__name__}"
305                )
306            result = container[item]
307            return RonObject(result)
308        else:
309            raise TypeError(
310                f"Value of type {type(val).__name__} is not subscriptable"
311            )
312
313
314@dataclass(frozen=True)
315class SpanPoint:
316    """
317    Represents span point, the beginning or the end of the element.
318    """
319
320    ch: int
321    """
322    Character index in a stream
323    """
324    line: int
325    """
326    Line number, starts at 1
327    """
328    column: int
329    """
330    Column number, starts at 1
331    """
332
333
334type Span = tuple[SpanPoint, SpanPoint]
335"""
336@private
337"""
338
339
340@dataclass(frozen=True)
341class RonStruct:
342    """
343    Represents RON structures.
344
345    Includes:
346    - named structs like `Weapon(str: 5)`
347    - anon structs like `(str: 5)`
348    - structs with named fields (see above)
349    - structs with unnamed fields like `Weapon(5)`
350    - unit structs like ... `Weapon`?
351    - enums like `RBG(r: 255, g: 0, b: 0)` (yes, same syntax as structs)
352    - unit enums like `Red` (more useful than unit structs)
353
354    Use `RonStruct.as_dict` or `RonStruct.as_tuple` to get fields.
355
356    **Careful**, they will panic if you try to get the wrong thing.
357
358    And if you, for some reason, need a name, there's `RonStruct.name`
359    (might be `None`).
360
361
362    Oh, and it has spans. See `RonStruct.spans`.
363    """
364
365    name: str | None
366    """
367    Struct's name, if present. Simple as that.
368    """
369    _fields: frozendict[RonValue, RonValue] | tuple[RonValue, ...]
370    """
371    @private
372    """
373
374    spans: frozendict[RonValue, Span] | tuple[Span, ...] | None
375    """
376    Represents spans for struct field *values* as (start, end).
377    See `SpanPoint`.
378    """
379
380    @property
381    def as_dict(self) -> frozendict[RonValue, RonValue]:
382        """Returns the underlying dictionary for structs with named fields.
383
384        **Raises** a `ValueError` otherwise.
385        """
386        if isinstance(self._fields, frozendict):
387            return self._fields
388        raise ValueError(
389            f"Struct '{self.name}' is a Tuple-Struct, not a Named-Struct"
390        )
391
392    @property
393    def as_tuple(self) -> tuple[RonValue, ...]:
394        """Returns the underlying tuple for structs with unnamed fields.
395
396        **Raises** a ValueError otherwise.
397        """
398        if isinstance(self._fields, tuple):
399            return self._fields
400        raise ValueError(
401            f"Struct '{self.name}' is a Named-Struct, not a Tuple-Struct"
402        )
403
404
405@dataclass(frozen=True)
406class RonSeq:
407    """
408    Represents all sort of sequences: tuple `()`, lists `[]`, sets `[]`.
409
410    Use `RonSeq.as_tuple` to get said sequence as a `tuple`.
411
412    If you need such info, for some reason, you can use `RonSeq.kind` to
413    find out whether it was parsed from `()` or `[]`.
414    """
415
416    elements: tuple[RonValue, ...]
417    """
418    @private
419    """
420
421    kind: Literal["list", "tuple"]
422
423    @property
424    def as_tuple(self) -> tuple[RonValue, ...]:
425        """Returns the underlying sequence as a tuple."""
426        return self.elements
427
428
429@dataclass(frozen=True)
430class RonMap:
431    """
432    Represents a RON map, you can think of it as python's dict pretty much.
433
434    Use `RonMap.as_dict` to get said dict as `frozendict`
435    """
436
437    entries: frozendict[RonValue, RonValue]
438    """
439    @private
440    """
441
442    @property
443    def as_dict(self) -> frozendict[RonValue, RonValue]:
444        """Returns the underlying mapping as a `frozendict`."""
445        return self.entries
446
447
448@dataclass(frozen=True)
449class RonOptional:
450    """
451    Represents things like `Some("value")` or None.
452
453    Unlike in Python, that's a real object, but feel free to snatch it
454    with `RonOptional.value`.
455
456    Or use fancy methods to work with the object like `RonOptional.map`.
457    """
458
459    value: RonValue | None
460
461    def unwrap(self) -> RonValue:
462        """
463        Asserts that value is present, and returns it.
464
465        **Raises** a `ValueError` otherwise.
466        """
467        if self.value is None:
468            raise ValueError("tried to call unwrap() on None value")
469        return self.value
470
471    def unwrap_or(self, o: RonValue) -> RonValue:
472        """
473        Return a value if present, or return a fallback.
474        """
475        if self.value is None:
476            return o
477        return self.value
478
479    def map(self, f: Callable[[RonValue], RonValue]):
480        """
481        Apply a function to the underlying value, if present.
482
483        If not, nothing happens.
484        """
485        if self.value is None:
486            return None
487        return f(self.value)
488
489
490@dataclass(frozen=True)
491class RonChar:
492    """
493    Represents characters, like 'a'.
494    """
495
496    value: str
497
498    @override
499    def __str__(self) -> str:
500        return self.value
type RonValue = RonStruct | RonSeq | RonMap | RonOptional | RonChar | int | float | str | bool

Union of all supported RON types, including primitives and containers.

def is_ron_value(val: Any) -> TypeGuard[RonValue]:
43def is_ron_value(val: Any) -> TypeGuard[RonValue]:
44    """
45    Type-guard that checks that your value is indeed of `RonValue`
46
47    Use with typecheckers.
48    """
49    # simple top-level check
50    if isinstance(
51        val,
52        (
53            RonStruct,
54            RonSeq,
55            RonMap,
56            RonOptional,
57            RonChar,
58            int,
59            float,
60            str,
61            bool,
62        ),
63    ):
64        return True
65
66    return False

Type-guard that checks that your value is indeed of RonValue

Use with typecheckers.

@dataclass
class RonObject:
 69@dataclass
 70class RonObject:
 71    """
 72    A wrapper around `RonValue` enabling convenient traversal.
 73
 74    It allows you to access nested fields using standard Python types as keys,
 75    automatically handling the necessary conversions to RON-specific types.
 76
 77    Read `RonObject.__getitem__`() docs for more.
 78    """
 79
 80    v: RonValue
 81    """@private field holding the internal value"""
 82
 83    def expect_map(self) -> RonMap:
 84        """Returns an underlying map or **raises** a `ValueError`"""
 85        if isinstance(self.v, RonMap):
 86            return self.v
 87        raise ValueError(f"Value '{self}' is not a map")
 88
 89    def expect_struct(self) -> RonStruct:
 90        """Returns an underlying struct or **raises** a `ValueError`"""
 91        if isinstance(self.v, RonStruct):
 92            return self.v
 93        raise ValueError(f"Value '{self}' is not a struct")
 94
 95    def expect_int(self) -> int:
 96        """Returns an underlying int or **raises** a `ValueError`"""
 97        if isinstance(self.v, int):
 98            return self.v
 99        raise ValueError(f"Value '{self}' is not an integer")
100
101    def expect_float(self) -> float:
102        """Returns an underlying float or **raises** a `ValueError`"""
103        if isinstance(self.v, float):
104            return self.v
105        raise ValueError(f"Value '{self}' is not a float")
106
107    def expect_str(self) -> str:
108        """Returns an underlying str or **raises** a `ValueError`"""
109        if isinstance(self.v, str):
110            return self.v
111        raise ValueError(f"Value '{self}' is not a string")
112
113    def expect_bool(self) -> bool:
114        """Returns an underlying bool or **raises** a `ValueError`"""
115        if isinstance(self.v, bool):
116            return self.v
117        raise ValueError(f"Value '{self}' is not a boolean")
118
119    def expect_tuple(self) -> RonSeq:
120        """Returns an underlying tuple or **raises** a `ValueError`"""
121        if isinstance(self.v, RonSeq) and self.v.kind == "tuple":
122            return self.v
123        raise ValueError(f"Value '{self}' is not a ron tuple")
124
125    def expect_list(self) -> RonSeq:
126        """Returns an underlying list or **raises** a `ValueError`"""
127        if isinstance(self.v, RonSeq) and self.v.kind == "list":
128            return self.v
129        raise ValueError(f"Value '{self}' is not a ron list")
130
131    def maybe(self) -> RonObject | None:
132        """
133        Transposes a RON optional into Python `Optional`.
134
135        # Returns
136        `RonObject` | `None`: The inner value, if present.
137
138        # Raises
139        - `ValueError`: If the value is not an option.
140
141        Examples:
142        >>> from ron import parse_ron
143        >>> obj = parse_ron('Some(42)')
144        >>> result = obj.maybe()
145        >>> assert result is not None
146        >>> result.expect_int()
147        42
148
149        >>> obj = parse_ron('None')
150        >>> obj.maybe() is None
151        True
152
153        >>> # Raises error if called on non-option types
154        >>> obj = parse_ron('42')
155        >>> obj.maybe()
156        Traceback (most recent call last):
157            ...
158        ValueError: Value ... is not an option
159        """
160        if isinstance(self.v, RonOptional):
161            return RonObject(self.v.value) if self.v.value is not None else None
162        raise ValueError(f"Value '{self}' is not an option")
163
164    def __getitem__(
165        self,
166        item: RonValue
167        | tuple[RonValue, ...]
168        | list[RonValue]
169        | dict[Any, Any]
170        | None
171        | RonObject,
172    ) -> RonObject:
173        """
174        @public Escape hatch to traverse the RON object.
175
176        This method inspects the underlying container (Map, Struct, or Sequence)
177        and (if needed) automatically converts the provided `item` into the
178        correct RON type required for the lookup.
179
180        For example, passing a `str` to a struct will look up that field name,
181        and passing a `tuple` to a map will convert it to a `RonSeq` key.
182
183        # Returns
184        `RonObject`: A new wrapper around the retrieved value, enabling
185        chained access.
186
187        # Raises
188        - `TypeError`: If the value is not a container, or if the key type
189            is invalid for the current container.
190        - `KeyError`: If the item is missing (for Maps and Structs).
191        - `IndexError`: If the index is out of bounds (for Sequences).
192
193        Examples:
194        >>> from ron import parse_ron
195
196        >>> # Struct access: use string keys for field names
197        >>> obj = parse_ron('( id: 42, name: "foo" )')
198        >>> obj["id"].expect_int()
199        42
200
201        >>> # Map access: use standard Python literals
202        >>> obj = parse_ron('{ "key": "value" }')
203        >>> obj["key"].expect_str()
204        'value'
205
206        >>> # Sequence access: use integer indices
207        >>> obj = parse_ron('[10, 20, 30]')
208        >>> obj[1].expect_int()
209        20
210
211        >>> # Complex keys: Python tuples are automatically coerced to RON tuples
212        >>> obj = parse_ron('{ (1, "a"): "found" }')
213        >>> obj[(1, "a")].expect_str()
214        'found'
215
216        >>> # Nested chaining
217        >>> obj = parse_ron('( config: (version: 1) )')
218        >>> obj["config"]["version"].expect_int()
219        1
220
221        >>> # Unit structs
222        >>> obj = parse_ron('{ King: "Crown" }')
223        >>> obj["King"].expect_str()
224        'Crown'
225
226        >>> # And even optionals
227        >>> obj = parse_ron('{ Some("King"): "Crown", None: "Hat" }')
228        >>> obj["King"].expect_str()
229        'Crown'
230        >>> obj[None].expect_str()
231        'Hat'
232
233        >>> # Nested coercion doesn't work for now
234        >>> obj = parse_ron('{ Some(King): "Crown", None: "Hat" }')
235        >>> obj["King"].expect_str() # doctest: +IGNORE_EXCEPTION_DETAIL
236        Traceback (most recent call last):
237            ...
238        KeyError: ... coerced as Some("King"), not Some(King)
239
240        >>> # If you need nested keys, parse them
241        >>> obj[parse_ron("Some(King)")].expect_str()
242        'Crown'
243        """
244
245        val = self.v
246        if isinstance(item, RonObject):
247            item = item.v
248
249        # 1. Pick a proper container (and its key type)
250        key = None
251        if isinstance(val, RonStruct):
252            container = val._fields
253            if type(container) is frozendict:
254                key = container.key(0)
255        elif isinstance(val, RonMap):
256            container = val.entries
257            key = container.key(0)
258        elif isinstance(val, RonSeq):
259            container = val.elements
260        elif isinstance(val, tuple):
261            container = val
262        else:
263            raise TypeError(f"{self}[{item}]")
264
265        # 2. Convert index to a proper ron type, if needed
266        if isinstance(item, tuple):
267            match key:
268                case RonSeq():
269                    item = RonSeq(elements=item, kind="tuple")
270                case RonStruct(name):
271                    item = RonStruct(name, _fields=item, spans=None)
272                case _:
273                    raise TypeError(f"{self}[{item}]: Unexpected index type")
274        if isinstance(item, list):
275            item = RonSeq(elements=tuple(item), kind="list")
276        if isinstance(item, dict):
277            match key:
278                case RonMap():
279                    item = RonMap(entries=frozendict(item))
280                case RonStruct(name):
281                    item = RonStruct(name, _fields=frozendict(item), spans=None)
282                case _:
283                    raise TypeError(f"{self}[{item}]: Unexpected index type")
284
285        # Handle optionals and unit structs
286        if type(key) is RonOptional and type(item) is not RonOptional:
287            item = RonOptional(item)
288        elif type(key) is RonStruct and type(item) is str:
289            item = RonStruct(item, _fields=tuple(), spans=None)
290        elif item is None:
291            item = RonOptional(None)
292
293        # 3. Do the index magic and wrap in RonObject to prolong the chain
294        if isinstance(container, frozendict):
295            try:
296                result = container[item]
297            except (KeyError, TypeError):
298                raise KeyError(
299                    f"container={container}, key={item}, heuristic_key={key}"
300                )
301            return RonObject(result)
302        elif isinstance(container, tuple):
303            if not isinstance(item, int):
304                raise TypeError(
305                    f"List indices must be integers, got {type(item).__name__}"
306                )
307            result = container[item]
308            return RonObject(result)
309        else:
310            raise TypeError(
311                f"Value of type {type(val).__name__} is not subscriptable"
312            )

A wrapper around RonValue enabling convenient traversal.

It allows you to access nested fields using standard Python types as keys, automatically handling the necessary conversions to RON-specific types.

Read RonObject.__getitem__() docs for more.

RonObject(v: RonValue)
def expect_map(self) -> RonMap:
83    def expect_map(self) -> RonMap:
84        """Returns an underlying map or **raises** a `ValueError`"""
85        if isinstance(self.v, RonMap):
86            return self.v
87        raise ValueError(f"Value '{self}' is not a map")

Returns an underlying map or raises a ValueError

def expect_struct(self) -> RonStruct:
89    def expect_struct(self) -> RonStruct:
90        """Returns an underlying struct or **raises** a `ValueError`"""
91        if isinstance(self.v, RonStruct):
92            return self.v
93        raise ValueError(f"Value '{self}' is not a struct")

Returns an underlying struct or raises a ValueError

def expect_int(self) -> int:
95    def expect_int(self) -> int:
96        """Returns an underlying int or **raises** a `ValueError`"""
97        if isinstance(self.v, int):
98            return self.v
99        raise ValueError(f"Value '{self}' is not an integer")

Returns an underlying int or raises a ValueError

def expect_float(self) -> float:
101    def expect_float(self) -> float:
102        """Returns an underlying float or **raises** a `ValueError`"""
103        if isinstance(self.v, float):
104            return self.v
105        raise ValueError(f"Value '{self}' is not a float")

Returns an underlying float or raises a ValueError

def expect_str(self) -> str:
107    def expect_str(self) -> str:
108        """Returns an underlying str or **raises** a `ValueError`"""
109        if isinstance(self.v, str):
110            return self.v
111        raise ValueError(f"Value '{self}' is not a string")

Returns an underlying str or raises a ValueError

def expect_bool(self) -> bool:
113    def expect_bool(self) -> bool:
114        """Returns an underlying bool or **raises** a `ValueError`"""
115        if isinstance(self.v, bool):
116            return self.v
117        raise ValueError(f"Value '{self}' is not a boolean")

Returns an underlying bool or raises a ValueError

def expect_tuple(self) -> RonSeq:
119    def expect_tuple(self) -> RonSeq:
120        """Returns an underlying tuple or **raises** a `ValueError`"""
121        if isinstance(self.v, RonSeq) and self.v.kind == "tuple":
122            return self.v
123        raise ValueError(f"Value '{self}' is not a ron tuple")

Returns an underlying tuple or raises a ValueError

def expect_list(self) -> RonSeq:
125    def expect_list(self) -> RonSeq:
126        """Returns an underlying list or **raises** a `ValueError`"""
127        if isinstance(self.v, RonSeq) and self.v.kind == "list":
128            return self.v
129        raise ValueError(f"Value '{self}' is not a ron list")

Returns an underlying list or raises a ValueError

def maybe(self) -> RonObject | None:
131    def maybe(self) -> RonObject | None:
132        """
133        Transposes a RON optional into Python `Optional`.
134
135        # Returns
136        `RonObject` | `None`: The inner value, if present.
137
138        # Raises
139        - `ValueError`: If the value is not an option.
140
141        Examples:
142        >>> from ron import parse_ron
143        >>> obj = parse_ron('Some(42)')
144        >>> result = obj.maybe()
145        >>> assert result is not None
146        >>> result.expect_int()
147        42
148
149        >>> obj = parse_ron('None')
150        >>> obj.maybe() is None
151        True
152
153        >>> # Raises error if called on non-option types
154        >>> obj = parse_ron('42')
155        >>> obj.maybe()
156        Traceback (most recent call last):
157            ...
158        ValueError: Value ... is not an option
159        """
160        if isinstance(self.v, RonOptional):
161            return RonObject(self.v.value) if self.v.value is not None else None
162        raise ValueError(f"Value '{self}' is not an option")

Transposes a RON optional into Python Optional.

Returns

RonObject | None: The inner value, if present.

Raises

  • ValueError: If the value is not an option.

Examples:

>>> from ron import parse_ron
>>> obj = parse_ron('Some(42)')
>>> result = obj.maybe()
>>> assert result is not None
>>> result.expect_int()
42
>>> obj = parse_ron('None')
>>> obj.maybe() is None
True
>>> # Raises error if called on non-option types
>>> obj = parse_ron('42')
>>> obj.maybe()
Traceback (most recent call last):
    ...
ValueError: Value ... is not an option
def __getitem__( self, item: RonValue | tuple[RonValue, ...] | list[RonValue] | dict[Any, Any] | None | RonObject) -> RonObject:
164    def __getitem__(
165        self,
166        item: RonValue
167        | tuple[RonValue, ...]
168        | list[RonValue]
169        | dict[Any, Any]
170        | None
171        | RonObject,
172    ) -> RonObject:
173        """
174        @public Escape hatch to traverse the RON object.
175
176        This method inspects the underlying container (Map, Struct, or Sequence)
177        and (if needed) automatically converts the provided `item` into the
178        correct RON type required for the lookup.
179
180        For example, passing a `str` to a struct will look up that field name,
181        and passing a `tuple` to a map will convert it to a `RonSeq` key.
182
183        # Returns
184        `RonObject`: A new wrapper around the retrieved value, enabling
185        chained access.
186
187        # Raises
188        - `TypeError`: If the value is not a container, or if the key type
189            is invalid for the current container.
190        - `KeyError`: If the item is missing (for Maps and Structs).
191        - `IndexError`: If the index is out of bounds (for Sequences).
192
193        Examples:
194        >>> from ron import parse_ron
195
196        >>> # Struct access: use string keys for field names
197        >>> obj = parse_ron('( id: 42, name: "foo" )')
198        >>> obj["id"].expect_int()
199        42
200
201        >>> # Map access: use standard Python literals
202        >>> obj = parse_ron('{ "key": "value" }')
203        >>> obj["key"].expect_str()
204        'value'
205
206        >>> # Sequence access: use integer indices
207        >>> obj = parse_ron('[10, 20, 30]')
208        >>> obj[1].expect_int()
209        20
210
211        >>> # Complex keys: Python tuples are automatically coerced to RON tuples
212        >>> obj = parse_ron('{ (1, "a"): "found" }')
213        >>> obj[(1, "a")].expect_str()
214        'found'
215
216        >>> # Nested chaining
217        >>> obj = parse_ron('( config: (version: 1) )')
218        >>> obj["config"]["version"].expect_int()
219        1
220
221        >>> # Unit structs
222        >>> obj = parse_ron('{ King: "Crown" }')
223        >>> obj["King"].expect_str()
224        'Crown'
225
226        >>> # And even optionals
227        >>> obj = parse_ron('{ Some("King"): "Crown", None: "Hat" }')
228        >>> obj["King"].expect_str()
229        'Crown'
230        >>> obj[None].expect_str()
231        'Hat'
232
233        >>> # Nested coercion doesn't work for now
234        >>> obj = parse_ron('{ Some(King): "Crown", None: "Hat" }')
235        >>> obj["King"].expect_str() # doctest: +IGNORE_EXCEPTION_DETAIL
236        Traceback (most recent call last):
237            ...
238        KeyError: ... coerced as Some("King"), not Some(King)
239
240        >>> # If you need nested keys, parse them
241        >>> obj[parse_ron("Some(King)")].expect_str()
242        'Crown'
243        """
244
245        val = self.v
246        if isinstance(item, RonObject):
247            item = item.v
248
249        # 1. Pick a proper container (and its key type)
250        key = None
251        if isinstance(val, RonStruct):
252            container = val._fields
253            if type(container) is frozendict:
254                key = container.key(0)
255        elif isinstance(val, RonMap):
256            container = val.entries
257            key = container.key(0)
258        elif isinstance(val, RonSeq):
259            container = val.elements
260        elif isinstance(val, tuple):
261            container = val
262        else:
263            raise TypeError(f"{self}[{item}]")
264
265        # 2. Convert index to a proper ron type, if needed
266        if isinstance(item, tuple):
267            match key:
268                case RonSeq():
269                    item = RonSeq(elements=item, kind="tuple")
270                case RonStruct(name):
271                    item = RonStruct(name, _fields=item, spans=None)
272                case _:
273                    raise TypeError(f"{self}[{item}]: Unexpected index type")
274        if isinstance(item, list):
275            item = RonSeq(elements=tuple(item), kind="list")
276        if isinstance(item, dict):
277            match key:
278                case RonMap():
279                    item = RonMap(entries=frozendict(item))
280                case RonStruct(name):
281                    item = RonStruct(name, _fields=frozendict(item), spans=None)
282                case _:
283                    raise TypeError(f"{self}[{item}]: Unexpected index type")
284
285        # Handle optionals and unit structs
286        if type(key) is RonOptional and type(item) is not RonOptional:
287            item = RonOptional(item)
288        elif type(key) is RonStruct and type(item) is str:
289            item = RonStruct(item, _fields=tuple(), spans=None)
290        elif item is None:
291            item = RonOptional(None)
292
293        # 3. Do the index magic and wrap in RonObject to prolong the chain
294        if isinstance(container, frozendict):
295            try:
296                result = container[item]
297            except (KeyError, TypeError):
298                raise KeyError(
299                    f"container={container}, key={item}, heuristic_key={key}"
300                )
301            return RonObject(result)
302        elif isinstance(container, tuple):
303            if not isinstance(item, int):
304                raise TypeError(
305                    f"List indices must be integers, got {type(item).__name__}"
306                )
307            result = container[item]
308            return RonObject(result)
309        else:
310            raise TypeError(
311                f"Value of type {type(val).__name__} is not subscriptable"
312            )

Escape hatch to traverse the RON object.

This method inspects the underlying container (Map, Struct, or Sequence) and (if needed) automatically converts the provided item into the correct RON type required for the lookup.

For example, passing a str to a struct will look up that field name, and passing a tuple to a map will convert it to a RonSeq key.

Returns

RonObject: A new wrapper around the retrieved value, enabling chained access.

Raises

  • TypeError: If the value is not a container, or if the key type is invalid for the current container.
  • KeyError: If the item is missing (for Maps and Structs).
  • IndexError: If the index is out of bounds (for Sequences).

Examples:

>>> from ron import parse_ron
>>> # Struct access: use string keys for field names
>>> obj = parse_ron('( id: 42, name: "foo" )')
>>> obj["id"].expect_int()
42
>>> # Map access: use standard Python literals
>>> obj = parse_ron('{ "key": "value" }')
>>> obj["key"].expect_str()
'value'
>>> # Sequence access: use integer indices
>>> obj = parse_ron('[10, 20, 30]')
>>> obj[1].expect_int()
20
>>> # Complex keys: Python tuples are automatically coerced to RON tuples
>>> obj = parse_ron('{ (1, "a"): "found" }')
>>> obj[(1, "a")].expect_str()
'found'
>>> # Nested chaining
>>> obj = parse_ron('( config: (version: 1) )')
>>> obj["config"]["version"].expect_int()
1
>>> # Unit structs
>>> obj = parse_ron('{ King: "Crown" }')
>>> obj["King"].expect_str()
'Crown'
>>> # And even optionals
>>> obj = parse_ron('{ Some("King"): "Crown", None: "Hat" }')
>>> obj["King"].expect_str()
'Crown'
>>> obj[None].expect_str()
'Hat'
>>> # Nested coercion doesn't work for now
>>> obj = parse_ron('{ Some(King): "Crown", None: "Hat" }')
>>> obj["King"].expect_str() # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
    ...
KeyError: ... coerced as Some("King"), not Some(King)
>>> # If you need nested keys, parse them
>>> obj[parse_ron("Some(King)")].expect_str()
'Crown'
@dataclass(frozen=True)
class SpanPoint:
315@dataclass(frozen=True)
316class SpanPoint:
317    """
318    Represents span point, the beginning or the end of the element.
319    """
320
321    ch: int
322    """
323    Character index in a stream
324    """
325    line: int
326    """
327    Line number, starts at 1
328    """
329    column: int
330    """
331    Column number, starts at 1
332    """

Represents span point, the beginning or the end of the element.

SpanPoint(ch: int, line: int, column: int)
ch: int

Character index in a stream

line: int

Line number, starts at 1

column: int

Column number, starts at 1

@dataclass(frozen=True)
class RonStruct:
341@dataclass(frozen=True)
342class RonStruct:
343    """
344    Represents RON structures.
345
346    Includes:
347    - named structs like `Weapon(str: 5)`
348    - anon structs like `(str: 5)`
349    - structs with named fields (see above)
350    - structs with unnamed fields like `Weapon(5)`
351    - unit structs like ... `Weapon`?
352    - enums like `RBG(r: 255, g: 0, b: 0)` (yes, same syntax as structs)
353    - unit enums like `Red` (more useful than unit structs)
354
355    Use `RonStruct.as_dict` or `RonStruct.as_tuple` to get fields.
356
357    **Careful**, they will panic if you try to get the wrong thing.
358
359    And if you, for some reason, need a name, there's `RonStruct.name`
360    (might be `None`).
361
362
363    Oh, and it has spans. See `RonStruct.spans`.
364    """
365
366    name: str | None
367    """
368    Struct's name, if present. Simple as that.
369    """
370    _fields: frozendict[RonValue, RonValue] | tuple[RonValue, ...]
371    """
372    @private
373    """
374
375    spans: frozendict[RonValue, Span] | tuple[Span, ...] | None
376    """
377    Represents spans for struct field *values* as (start, end).
378    See `SpanPoint`.
379    """
380
381    @property
382    def as_dict(self) -> frozendict[RonValue, RonValue]:
383        """Returns the underlying dictionary for structs with named fields.
384
385        **Raises** a `ValueError` otherwise.
386        """
387        if isinstance(self._fields, frozendict):
388            return self._fields
389        raise ValueError(
390            f"Struct '{self.name}' is a Tuple-Struct, not a Named-Struct"
391        )
392
393    @property
394    def as_tuple(self) -> tuple[RonValue, ...]:
395        """Returns the underlying tuple for structs with unnamed fields.
396
397        **Raises** a ValueError otherwise.
398        """
399        if isinstance(self._fields, tuple):
400            return self._fields
401        raise ValueError(
402            f"Struct '{self.name}' is a Named-Struct, not a Tuple-Struct"
403        )

Represents RON structures.

Includes:

  • named structs like Weapon(str: 5)
  • anon structs like (str: 5)
  • structs with named fields (see above)
  • structs with unnamed fields like Weapon(5)
  • unit structs like ... Weapon?
  • enums like RBG(r: 255, g: 0, b: 0) (yes, same syntax as structs)
  • unit enums like Red (more useful than unit structs)

Use RonStruct.as_dict or RonStruct.as_tuple to get fields.

Careful, they will panic if you try to get the wrong thing.

And if you, for some reason, need a name, there's RonStruct.name (might be None).

Oh, and it has spans. See RonStruct.spans.

RonStruct( name: str | None, _fields: frozendict.frozendict[RonValue, RonValue] | tuple[RonValue, ...], spans: frozendict.frozendict[RonValue, Span] | tuple[Span, ...] | None)
name: str | None

Struct's name, if present. Simple as that.

spans: frozendict.frozendict[RonValue, Span] | tuple[Span, ...] | None

Represents spans for struct field values as (start, end). See SpanPoint.

as_dict: frozendict.frozendict[RonValue, RonValue]
381    @property
382    def as_dict(self) -> frozendict[RonValue, RonValue]:
383        """Returns the underlying dictionary for structs with named fields.
384
385        **Raises** a `ValueError` otherwise.
386        """
387        if isinstance(self._fields, frozendict):
388            return self._fields
389        raise ValueError(
390            f"Struct '{self.name}' is a Tuple-Struct, not a Named-Struct"
391        )

Returns the underlying dictionary for structs with named fields.

Raises a ValueError otherwise.

as_tuple: tuple[RonValue, ...]
393    @property
394    def as_tuple(self) -> tuple[RonValue, ...]:
395        """Returns the underlying tuple for structs with unnamed fields.
396
397        **Raises** a ValueError otherwise.
398        """
399        if isinstance(self._fields, tuple):
400            return self._fields
401        raise ValueError(
402            f"Struct '{self.name}' is a Named-Struct, not a Tuple-Struct"
403        )

Returns the underlying tuple for structs with unnamed fields.

Raises a ValueError otherwise.

@dataclass(frozen=True)
class RonSeq:
406@dataclass(frozen=True)
407class RonSeq:
408    """
409    Represents all sort of sequences: tuple `()`, lists `[]`, sets `[]`.
410
411    Use `RonSeq.as_tuple` to get said sequence as a `tuple`.
412
413    If you need such info, for some reason, you can use `RonSeq.kind` to
414    find out whether it was parsed from `()` or `[]`.
415    """
416
417    elements: tuple[RonValue, ...]
418    """
419    @private
420    """
421
422    kind: Literal["list", "tuple"]
423
424    @property
425    def as_tuple(self) -> tuple[RonValue, ...]:
426        """Returns the underlying sequence as a tuple."""
427        return self.elements

Represents all sort of sequences: tuple (), lists [], sets [].

Use RonSeq.as_tuple to get said sequence as a tuple.

If you need such info, for some reason, you can use RonSeq.kind to find out whether it was parsed from () or [].

RonSeq(elements: tuple[RonValue, ...], kind: Literal['list', 'tuple'])
kind: Literal['list', 'tuple']
as_tuple: tuple[RonValue, ...]
424    @property
425    def as_tuple(self) -> tuple[RonValue, ...]:
426        """Returns the underlying sequence as a tuple."""
427        return self.elements

Returns the underlying sequence as a tuple.

@dataclass(frozen=True)
class RonMap:
430@dataclass(frozen=True)
431class RonMap:
432    """
433    Represents a RON map, you can think of it as python's dict pretty much.
434
435    Use `RonMap.as_dict` to get said dict as `frozendict`
436    """
437
438    entries: frozendict[RonValue, RonValue]
439    """
440    @private
441    """
442
443    @property
444    def as_dict(self) -> frozendict[RonValue, RonValue]:
445        """Returns the underlying mapping as a `frozendict`."""
446        return self.entries

Represents a RON map, you can think of it as python's dict pretty much.

Use RonMap.as_dict to get said dict as frozendict

RonMap(entries: frozendict.frozendict[RonValue, RonValue])
as_dict: frozendict.frozendict[RonValue, RonValue]
443    @property
444    def as_dict(self) -> frozendict[RonValue, RonValue]:
445        """Returns the underlying mapping as a `frozendict`."""
446        return self.entries

Returns the underlying mapping as a frozendict.

@dataclass(frozen=True)
class RonOptional:
449@dataclass(frozen=True)
450class RonOptional:
451    """
452    Represents things like `Some("value")` or None.
453
454    Unlike in Python, that's a real object, but feel free to snatch it
455    with `RonOptional.value`.
456
457    Or use fancy methods to work with the object like `RonOptional.map`.
458    """
459
460    value: RonValue | None
461
462    def unwrap(self) -> RonValue:
463        """
464        Asserts that value is present, and returns it.
465
466        **Raises** a `ValueError` otherwise.
467        """
468        if self.value is None:
469            raise ValueError("tried to call unwrap() on None value")
470        return self.value
471
472    def unwrap_or(self, o: RonValue) -> RonValue:
473        """
474        Return a value if present, or return a fallback.
475        """
476        if self.value is None:
477            return o
478        return self.value
479
480    def map(self, f: Callable[[RonValue], RonValue]):
481        """
482        Apply a function to the underlying value, if present.
483
484        If not, nothing happens.
485        """
486        if self.value is None:
487            return None
488        return f(self.value)

Represents things like Some("value") or None.

Unlike in Python, that's a real object, but feel free to snatch it with RonOptional.value.

Or use fancy methods to work with the object like RonOptional.map.

RonOptional(value: RonValue | None)
value: RonValue | None
def unwrap(self) -> RonValue:
462    def unwrap(self) -> RonValue:
463        """
464        Asserts that value is present, and returns it.
465
466        **Raises** a `ValueError` otherwise.
467        """
468        if self.value is None:
469            raise ValueError("tried to call unwrap() on None value")
470        return self.value

Asserts that value is present, and returns it.

Raises a ValueError otherwise.

def unwrap_or(self, o: RonValue) -> RonValue:
472    def unwrap_or(self, o: RonValue) -> RonValue:
473        """
474        Return a value if present, or return a fallback.
475        """
476        if self.value is None:
477            return o
478        return self.value

Return a value if present, or return a fallback.

def map(self, f: Callable[[RonValue], RonValue]):
480    def map(self, f: Callable[[RonValue], RonValue]):
481        """
482        Apply a function to the underlying value, if present.
483
484        If not, nothing happens.
485        """
486        if self.value is None:
487            return None
488        return f(self.value)

Apply a function to the underlying value, if present.

If not, nothing happens.

@dataclass(frozen=True)
class RonChar:
491@dataclass(frozen=True)
492class RonChar:
493    """
494    Represents characters, like 'a'.
495    """
496
497    value: str
498
499    @override
500    def __str__(self) -> str:
501        return self.value

Represents characters, like 'a'.

RonChar(value: str)
value: str