0
# Custom YAML Objects
1
2
Base classes and utilities for creating custom YAML-serializable Python objects with automatic tag registration. These classes enable seamless integration between Python objects and YAML serialization.
3
4
## Capabilities
5
6
### YAMLObject Base Class
7
8
Base class for creating Python objects that can be automatically serialized to and from YAML with custom tags.
9
10
```python { .api }
11
class YAMLObjectMetaclass(type):
12
"""
13
Metaclass for YAML objects that automatically registers constructors and representers.
14
15
When a class uses this metaclass, it automatically registers itself
16
with the specified loaders and dumpers for seamless YAML integration.
17
"""
18
19
class YAMLObject(metaclass=YAMLObjectMetaclass):
20
"""
21
Base class for custom YAML-serializable objects.
22
23
Subclasses should define class attributes to control YAML behavior:
24
25
Class Attributes:
26
- yaml_loader: Loader class to register constructor with (default: None)
27
- yaml_dumper: Dumper class to register representer with (default: None)
28
- yaml_tag: YAML tag for this object type (required)
29
- yaml_flow_style: Use flow style for serialization (default: None)
30
"""
31
32
yaml_loader: Any = None
33
yaml_dumper: Any = None
34
yaml_tag: str = None
35
yaml_flow_style: bool | None = None
36
37
@classmethod
38
def from_yaml(cls, loader, node):
39
"""
40
Construct object from YAML node.
41
42
Parameters:
43
- loader: YAML loader instance
44
- node: YAML node to construct from
45
46
Returns:
47
- Instance of this class
48
49
Override this method to customize object construction from YAML.
50
"""
51
52
@classmethod
53
def to_yaml(cls, dumper, data):
54
"""
55
Represent object as YAML node.
56
57
Parameters:
58
- dumper: YAML dumper instance
59
- data: Object instance to represent
60
61
Returns:
62
- YAML node representing this object
63
64
Override this method to customize object representation to YAML.
65
"""
66
```
67
68
## Usage Examples
69
70
### Basic Custom Object
71
72
```python
73
import yaml
74
75
class Person(yaml.YAMLObject):
76
"""Custom person object with YAML serialization."""
77
78
yaml_tag = '!Person'
79
yaml_loader = yaml.SafeLoader
80
yaml_dumper = yaml.SafeDumper
81
82
def __init__(self, name, age, email=None):
83
self.name = name
84
self.age = age
85
self.email = email
86
87
def __repr__(self):
88
return f"Person(name='{self.name}', age={self.age}, email='{self.email}')"
89
90
def __eq__(self, other):
91
if not isinstance(other, Person):
92
return False
93
return (self.name == other.name and
94
self.age == other.age and
95
self.email == other.email)
96
97
# Create and serialize object
98
person = Person("John Doe", 30, "john@example.com")
99
100
# Dump to YAML
101
yaml_output = yaml.dump(person, Dumper=yaml.SafeDumper)
102
print(yaml_output)
103
# Output:
104
# !Person
105
# age: 30
106
# email: john@example.com
107
# name: John Doe
108
109
# Load from YAML
110
loaded_person = yaml.load(yaml_output, Loader=yaml.SafeLoader)
111
print(loaded_person) # Person(name='John Doe', age=30, email='john@example.com')
112
print(person == loaded_person) # True
113
```
114
115
### Custom Constructor and Representer
116
117
```python
118
import yaml
119
120
class Point(yaml.YAMLObject):
121
"""2D point with custom YAML representation."""
122
123
yaml_tag = '!Point'
124
yaml_loader = yaml.SafeLoader
125
yaml_dumper = yaml.SafeDumper
126
yaml_flow_style = True # Use flow style: !Point [x, y]
127
128
def __init__(self, x, y):
129
self.x = x
130
self.y = y
131
132
def __repr__(self):
133
return f"Point({self.x}, {self.y})"
134
135
@classmethod
136
def from_yaml(cls, loader, node):
137
"""Construct Point from YAML sequence [x, y]."""
138
if isinstance(node, yaml.SequenceNode):
139
x, y = loader.construct_sequence(node)
140
return cls(x, y)
141
elif isinstance(node, yaml.MappingNode):
142
data = loader.construct_mapping(node)
143
return cls(data['x'], data['y'])
144
else:
145
raise yaml.constructor.ConstructorError(
146
None, None, f"Expected sequence or mapping, got {node.id}", node.start_mark
147
)
148
149
@classmethod
150
def to_yaml(cls, dumper, data):
151
"""Represent Point as YAML sequence [x, y]."""
152
return dumper.represent_sequence(cls.yaml_tag, [data.x, data.y])
153
154
# Test custom Point serialization
155
points = [Point(1, 2), Point(3.5, -1.0), Point(0, 0)]
156
157
yaml_output = yaml.dump(points, Dumper=yaml.SafeDumper)
158
print(yaml_output)
159
# Output:
160
# - !Point [1, 2]
161
# - !Point [3.5, -1.0]
162
# - !Point [0, 0]
163
164
loaded_points = yaml.load(yaml_output, Loader=yaml.SafeLoader)
165
print(loaded_points) # [Point(1, 2), Point(3.5, -1.0), Point(0, 0)]
166
```
167
168
### Complex Custom Object
169
170
```python
171
import yaml
172
from datetime import datetime
173
174
class Task(yaml.YAMLObject):
175
"""Task object with complex nested data."""
176
177
yaml_tag = '!Task'
178
yaml_loader = yaml.SafeLoader
179
yaml_dumper = yaml.SafeDumper
180
181
def __init__(self, title, description="", priority="medium",
182
due_date=None, completed=False, tags=None):
183
self.title = title
184
self.description = description
185
self.priority = priority
186
self.due_date = due_date
187
self.completed = completed
188
self.tags = tags or []
189
self.created_at = datetime.now()
190
191
def __repr__(self):
192
return f"Task('{self.title}', priority='{self.priority}', completed={self.completed})"
193
194
@classmethod
195
def from_yaml(cls, loader, node):
196
"""Construct Task from YAML mapping."""
197
data = loader.construct_mapping(node, deep=True)
198
199
# Handle datetime string conversion
200
if 'due_date' in data and isinstance(data['due_date'], str):
201
data['due_date'] = datetime.fromisoformat(data['due_date'])
202
if 'created_at' in data and isinstance(data['created_at'], str):
203
data['created_at'] = datetime.fromisoformat(data['created_at'])
204
205
return cls(**data)
206
207
@classmethod
208
def to_yaml(cls, dumper, data):
209
"""Represent Task as YAML mapping."""
210
# Convert datetime objects to ISO strings for YAML compatibility
211
task_data = {
212
'title': data.title,
213
'description': data.description,
214
'priority': data.priority,
215
'completed': data.completed,
216
'tags': data.tags,
217
'created_at': data.created_at.isoformat()
218
}
219
220
if data.due_date:
221
task_data['due_date'] = data.due_date.isoformat()
222
223
return dumper.represent_mapping(cls.yaml_tag, task_data)
224
225
# Create task list
226
tasks = [
227
Task("Write documentation", "Complete API documentation", "high",
228
datetime(2024, 1, 15), tags=["docs", "api"]),
229
Task("Fix bug #123", "Memory leak in parser", "critical",
230
datetime(2024, 1, 10), tags=["bug", "parser"]),
231
Task("Add tests", "Unit tests for new features", "medium",
232
tags=["tests", "quality"])
233
]
234
235
# Serialize to YAML
236
yaml_output = yaml.dump(tasks, Dumper=yaml.SafeDumper, default_flow_style=False)
237
with open('tasks.yaml', 'w') as f:
238
f.write(yaml_output)
239
240
# Load back from YAML
241
with open('tasks.yaml', 'r') as f:
242
loaded_tasks = yaml.load(f, Loader=yaml.SafeLoader)
243
244
print(f"Loaded {len(loaded_tasks)} tasks")
245
for task in loaded_tasks:
246
print(f" {task}")
247
```
248
249
### Multiple Loaders and Dumpers
250
251
```python
252
import yaml
253
254
class Config(yaml.YAMLObject):
255
"""Configuration object that works with multiple loaders."""
256
257
yaml_tag = '!Config'
258
# Register with multiple loaders/dumpers
259
yaml_loader = [yaml.SafeLoader, yaml.FullLoader]
260
yaml_dumper = [yaml.SafeDumper, yaml.Dumper]
261
262
def __init__(self, **kwargs):
263
self.settings = kwargs
264
265
def __getitem__(self, key):
266
return self.settings[key]
267
268
def __setitem__(self, key, value):
269
self.settings[key] = value
270
271
def __repr__(self):
272
return f"Config({self.settings})"
273
274
@classmethod
275
def from_yaml(cls, loader, node):
276
"""Construct Config from YAML mapping."""
277
settings = loader.construct_mapping(node, deep=True)
278
return cls(**settings)
279
280
@classmethod
281
def to_yaml(cls, dumper, data):
282
"""Represent Config as YAML mapping."""
283
return dumper.represent_mapping(cls.yaml_tag, data.settings)
284
285
# Usage with different loaders
286
config_yaml = """
287
!Config
288
database:
289
host: localhost
290
port: 5432
291
name: myapp
292
features:
293
- authentication
294
- logging
295
- metrics
296
debug: true
297
"""
298
299
# Works with SafeLoader
300
config1 = yaml.load(config_yaml, Loader=yaml.SafeLoader)
301
print(f"SafeLoader: {config1}")
302
303
# Works with FullLoader
304
config2 = yaml.load(config_yaml, Loader=yaml.FullLoader)
305
print(f"FullLoader: {config2}")
306
307
# Both produce same result
308
print(f"Same config: {config1.settings == config2.settings}")
309
```
310
311
### Inheritance with YAML Objects
312
313
```python
314
import yaml
315
316
class Vehicle(yaml.YAMLObject):
317
"""Base vehicle class."""
318
319
yaml_tag = '!Vehicle'
320
yaml_loader = yaml.SafeLoader
321
yaml_dumper = yaml.SafeDumper
322
323
def __init__(self, make, model, year):
324
self.make = make
325
self.model = model
326
self.year = year
327
328
def __repr__(self):
329
return f"{self.__class__.__name__}({self.make} {self.model} {self.year})"
330
331
class Car(Vehicle):
332
"""Car subclass with additional properties."""
333
334
yaml_tag = '!Car'
335
336
def __init__(self, make, model, year, doors=4):
337
super().__init__(make, model, year)
338
self.doors = doors
339
340
class Motorcycle(Vehicle):
341
"""Motorcycle subclass with additional properties."""
342
343
yaml_tag = '!Motorcycle'
344
345
def __init__(self, make, model, year, engine_cc=None):
346
super().__init__(make, model, year)
347
self.engine_cc = engine_cc
348
349
# Create vehicle fleet
350
vehicles = [
351
Car("Toyota", "Camry", 2023, doors=4),
352
Motorcycle("Honda", "CBR600RR", 2022, engine_cc=600),
353
Vehicle("Generic", "Unknown", 2020)
354
]
355
356
# Serialize with specific tags
357
yaml_output = yaml.dump(vehicles, Dumper=yaml.SafeDumper)
358
print(yaml_output)
359
# Output:
360
# - !Car
361
# doors: 4
362
# make: Toyota
363
# model: Camry
364
# year: 2023
365
# - !Motorcycle
366
# engine_cc: 600
367
# make: Honda
368
# model: CBR600RR
369
# year: 2022
370
# - !Vehicle
371
# make: Generic
372
# model: Unknown
373
# year: 2020
374
375
# Load back with correct types
376
loaded_vehicles = yaml.load(yaml_output, Loader=yaml.SafeLoader)
377
for vehicle in loaded_vehicles:
378
print(f"{type(vehicle).__name__}: {vehicle}")
379
```