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:
- Strict Mapping: Deserializing RON directly into Python dataclasses and Enums.
- 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:
ron.models.RonStruct(includes span information for error reporting)ron.models.RonMapron.models.RonSeqron.models.RonOptional- Primitives (
int,float,str,bool,ron.models.RonChar)
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"]
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.
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
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
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