4
4
from collections import OrderedDict
5
5
from collections .abc import Iterator
6
6
import fnmatch
7
+ from io import StringIO
7
8
import logging
8
9
import os
9
10
from pathlib import Path
10
11
from typing import Any , TextIO , TypeVar , Union , overload
11
12
12
13
import yaml
13
14
15
+ try :
16
+ from yaml import CSafeLoader as FastestAvailableSafeLoader
17
+
18
+ HAS_C_LOADER = True
19
+ except ImportError :
20
+ HAS_C_LOADER = False
21
+ from yaml import SafeLoader as FastestAvailableSafeLoader # type: ignore[misc]
22
+
14
23
from homeassistant .exceptions import HomeAssistantError
15
24
16
25
from .const import SECRET_YAML
@@ -88,6 +97,30 @@ def _load_secret_yaml(self, secret_dir: Path) -> dict[str, str]:
88
97
return secrets
89
98
90
99
100
+ class SafeLoader (FastestAvailableSafeLoader ):
101
+ """The fastest available safe loader."""
102
+
103
+ def __init__ (self , stream : Any , secrets : Secrets | None = None ) -> None :
104
+ """Initialize a safe line loader."""
105
+ self .stream = stream
106
+ if isinstance (stream , str ):
107
+ self .name = "<unicode string>"
108
+ elif isinstance (stream , bytes ):
109
+ self .name = "<byte string>"
110
+ else :
111
+ self .name = getattr (stream , "name" , "<file>" )
112
+ super ().__init__ (stream )
113
+ self .secrets = secrets
114
+
115
+ def get_name (self ) -> str :
116
+ """Get the name of the loader."""
117
+ return self .name
118
+
119
+ def get_stream_name (self ) -> str :
120
+ """Get the name of the stream."""
121
+ return self .stream .name or ""
122
+
123
+
91
124
class SafeLineLoader (yaml .SafeLoader ):
92
125
"""Loader class that keeps track of line numbers."""
93
126
@@ -103,6 +136,17 @@ def compose_node(self, parent: yaml.nodes.Node, index: int) -> yaml.nodes.Node:
103
136
node .__line__ = last_line + 1 # type: ignore[attr-defined]
104
137
return node
105
138
139
+ def get_name (self ) -> str :
140
+ """Get the name of the loader."""
141
+ return self .name
142
+
143
+ def get_stream_name (self ) -> str :
144
+ """Get the name of the stream."""
145
+ return self .stream .name or ""
146
+
147
+
148
+ LoaderType = Union [SafeLineLoader , SafeLoader ]
149
+
106
150
107
151
def load_yaml (fname : str , secrets : Secrets | None = None ) -> JSON_TYPE :
108
152
"""Load a YAML file."""
@@ -114,60 +158,90 @@ def load_yaml(fname: str, secrets: Secrets | None = None) -> JSON_TYPE:
114
158
raise HomeAssistantError (exc ) from exc
115
159
116
160
117
- def parse_yaml (content : str | TextIO , secrets : Secrets | None = None ) -> JSON_TYPE :
118
- """Load a YAML file."""
161
+ def parse_yaml (
162
+ content : str | TextIO | StringIO , secrets : Secrets | None = None
163
+ ) -> JSON_TYPE :
164
+ """Parse YAML with the fastest available loader."""
165
+ if not HAS_C_LOADER :
166
+ return _parse_yaml_pure_python (content , secrets )
167
+ try :
168
+ return _parse_yaml (SafeLoader , content , secrets )
169
+ except yaml .YAMLError :
170
+ # Loading failed, so we now load with the slow line loader
171
+ # since the C one will not give us line numbers
172
+ if isinstance (content , (StringIO , TextIO )):
173
+ # Rewind the stream so we can try again
174
+ content .seek (0 , 0 )
175
+ return _parse_yaml_pure_python (content , secrets )
176
+
177
+
178
+ def _parse_yaml_pure_python (
179
+ content : str | TextIO | StringIO , secrets : Secrets | None = None
180
+ ) -> JSON_TYPE :
181
+ """Parse YAML with the pure python loader (this is very slow)."""
119
182
try :
120
- # If configuration file is empty YAML returns None
121
- # We convert that to an empty dict
122
- return (
123
- yaml .load (content , Loader = lambda stream : SafeLineLoader (stream , secrets ))
124
- or OrderedDict ()
125
- )
183
+ return _parse_yaml (SafeLineLoader , content , secrets )
126
184
except yaml .YAMLError as exc :
127
185
_LOGGER .error (str (exc ))
128
186
raise HomeAssistantError (exc ) from exc
129
187
130
188
189
+ def _parse_yaml (
190
+ loader : type [SafeLoader ] | type [SafeLineLoader ],
191
+ content : str | TextIO ,
192
+ secrets : Secrets | None = None ,
193
+ ) -> JSON_TYPE :
194
+ """Load a YAML file."""
195
+ # If configuration file is empty YAML returns None
196
+ # We convert that to an empty dict
197
+ return (
198
+ yaml .load (content , Loader = lambda stream : loader (stream , secrets ))
199
+ or OrderedDict ()
200
+ )
201
+
202
+
131
203
@overload
132
204
def _add_reference (
133
- obj : list | NodeListClass , loader : SafeLineLoader , node : yaml .nodes .Node
205
+ obj : list | NodeListClass ,
206
+ loader : LoaderType ,
207
+ node : yaml .nodes .Node ,
134
208
) -> NodeListClass :
135
209
...
136
210
137
211
138
212
@overload
139
213
def _add_reference (
140
- obj : str | NodeStrClass , loader : SafeLineLoader , node : yaml .nodes .Node
214
+ obj : str | NodeStrClass ,
215
+ loader : LoaderType ,
216
+ node : yaml .nodes .Node ,
141
217
) -> NodeStrClass :
142
218
...
143
219
144
220
145
221
@overload
146
- def _add_reference (
147
- obj : _DictT , loader : SafeLineLoader , node : yaml .nodes .Node
148
- ) -> _DictT :
222
+ def _add_reference (obj : _DictT , loader : LoaderType , node : yaml .nodes .Node ) -> _DictT :
149
223
...
150
224
151
225
152
- def _add_reference (obj , loader : SafeLineLoader , node : yaml .nodes .Node ): # type: ignore[no-untyped-def]
226
+ def _add_reference (obj , loader : LoaderType , node : yaml .nodes .Node ): # type: ignore[no-untyped-def]
153
227
"""Add file reference information to an object."""
154
228
if isinstance (obj , list ):
155
229
obj = NodeListClass (obj )
156
230
if isinstance (obj , str ):
157
231
obj = NodeStrClass (obj )
158
- setattr (obj , "__config_file__" , loader .name )
232
+ setattr (obj , "__config_file__" , loader .get_name () )
159
233
setattr (obj , "__line__" , node .start_mark .line )
160
234
return obj
161
235
162
236
163
- def _include_yaml (loader : SafeLineLoader , node : yaml .nodes .Node ) -> JSON_TYPE :
237
+ def _include_yaml (loader : LoaderType , node : yaml .nodes .Node ) -> JSON_TYPE :
164
238
"""Load another YAML file and embeds it using the !include tag.
165
239
166
240
Example:
167
241
device_tracker: !include device_tracker.yaml
168
242
169
243
"""
170
- fname = os .path .join (os .path .dirname (loader .name ), node .value )
244
+ fname = os .path .join (os .path .dirname (loader .get_name () ), node .value )
171
245
try :
172
246
return _add_reference (load_yaml (fname , loader .secrets ), loader , node )
173
247
except FileNotFoundError as exc :
@@ -191,12 +265,10 @@ def _find_files(directory: str, pattern: str) -> Iterator[str]:
191
265
yield filename
192
266
193
267
194
- def _include_dir_named_yaml (
195
- loader : SafeLineLoader , node : yaml .nodes .Node
196
- ) -> OrderedDict :
268
+ def _include_dir_named_yaml (loader : LoaderType , node : yaml .nodes .Node ) -> OrderedDict :
197
269
"""Load multiple files from directory as a dictionary."""
198
270
mapping : OrderedDict = OrderedDict ()
199
- loc = os .path .join (os .path .dirname (loader .name ), node .value )
271
+ loc = os .path .join (os .path .dirname (loader .get_name () ), node .value )
200
272
for fname in _find_files (loc , "*.yaml" ):
201
273
filename = os .path .splitext (os .path .basename (fname ))[0 ]
202
274
if os .path .basename (fname ) == SECRET_YAML :
@@ -206,11 +278,11 @@ def _include_dir_named_yaml(
206
278
207
279
208
280
def _include_dir_merge_named_yaml (
209
- loader : SafeLineLoader , node : yaml .nodes .Node
281
+ loader : LoaderType , node : yaml .nodes .Node
210
282
) -> OrderedDict :
211
283
"""Load multiple files from directory as a merged dictionary."""
212
284
mapping : OrderedDict = OrderedDict ()
213
- loc = os .path .join (os .path .dirname (loader .name ), node .value )
285
+ loc = os .path .join (os .path .dirname (loader .get_name () ), node .value )
214
286
for fname in _find_files (loc , "*.yaml" ):
215
287
if os .path .basename (fname ) == SECRET_YAML :
216
288
continue
@@ -221,10 +293,10 @@ def _include_dir_merge_named_yaml(
221
293
222
294
223
295
def _include_dir_list_yaml (
224
- loader : SafeLineLoader , node : yaml .nodes .Node
296
+ loader : LoaderType , node : yaml .nodes .Node
225
297
) -> list [JSON_TYPE ]:
226
298
"""Load multiple files from directory as a list."""
227
- loc = os .path .join (os .path .dirname (loader .name ), node .value )
299
+ loc = os .path .join (os .path .dirname (loader .get_name () ), node .value )
228
300
return [
229
301
load_yaml (f , loader .secrets )
230
302
for f in _find_files (loc , "*.yaml" )
@@ -233,10 +305,10 @@ def _include_dir_list_yaml(
233
305
234
306
235
307
def _include_dir_merge_list_yaml (
236
- loader : SafeLineLoader , node : yaml .nodes .Node
308
+ loader : LoaderType , node : yaml .nodes .Node
237
309
) -> JSON_TYPE :
238
310
"""Load multiple files from directory as a merged list."""
239
- loc : str = os .path .join (os .path .dirname (loader .name ), node .value )
311
+ loc : str = os .path .join (os .path .dirname (loader .get_name () ), node .value )
240
312
merged_list : list [JSON_TYPE ] = []
241
313
for fname in _find_files (loc , "*.yaml" ):
242
314
if os .path .basename (fname ) == SECRET_YAML :
@@ -247,7 +319,7 @@ def _include_dir_merge_list_yaml(
247
319
return _add_reference (merged_list , loader , node )
248
320
249
321
250
- def _ordered_dict (loader : SafeLineLoader , node : yaml .nodes .MappingNode ) -> OrderedDict :
322
+ def _ordered_dict (loader : LoaderType , node : yaml .nodes .MappingNode ) -> OrderedDict :
251
323
"""Load YAML mappings into an ordered dictionary to preserve key order."""
252
324
loader .flatten_mapping (node )
253
325
nodes = loader .construct_pairs (node )
@@ -259,14 +331,14 @@ def _ordered_dict(loader: SafeLineLoader, node: yaml.nodes.MappingNode) -> Order
259
331
try :
260
332
hash (key )
261
333
except TypeError as exc :
262
- fname = getattr ( loader .stream , "name" , "" )
334
+ fname = loader .get_stream_name ( )
263
335
raise yaml .MarkedYAMLError (
264
336
context = f'invalid key: "{ key } "' ,
265
337
context_mark = yaml .Mark (fname , 0 , line , - 1 , None , None ), # type: ignore[arg-type]
266
338
) from exc
267
339
268
340
if key in seen :
269
- fname = getattr ( loader .stream , "name" , "" )
341
+ fname = loader .get_stream_name ( )
270
342
_LOGGER .warning (
271
343
'YAML file %s contains duplicate key "%s". Check lines %d and %d' ,
272
344
fname ,
@@ -279,13 +351,13 @@ def _ordered_dict(loader: SafeLineLoader, node: yaml.nodes.MappingNode) -> Order
279
351
return _add_reference (OrderedDict (nodes ), loader , node )
280
352
281
353
282
- def _construct_seq (loader : SafeLineLoader , node : yaml .nodes .Node ) -> JSON_TYPE :
354
+ def _construct_seq (loader : LoaderType , node : yaml .nodes .Node ) -> JSON_TYPE :
283
355
"""Add line number and file name to Load YAML sequence."""
284
356
(obj ,) = loader .construct_yaml_seq (node )
285
357
return _add_reference (obj , loader , node )
286
358
287
359
288
- def _env_var_yaml (loader : SafeLineLoader , node : yaml .nodes .Node ) -> str :
360
+ def _env_var_yaml (loader : LoaderType , node : yaml .nodes .Node ) -> str :
289
361
"""Load environment variables and embed it into the configuration YAML."""
290
362
args = node .value .split ()
291
363
@@ -298,27 +370,27 @@ def _env_var_yaml(loader: SafeLineLoader, node: yaml.nodes.Node) -> str:
298
370
raise HomeAssistantError (node .value )
299
371
300
372
301
- def secret_yaml (loader : SafeLineLoader , node : yaml .nodes .Node ) -> JSON_TYPE :
373
+ def secret_yaml (loader : LoaderType , node : yaml .nodes .Node ) -> JSON_TYPE :
302
374
"""Load secrets and embed it into the configuration YAML."""
303
375
if loader .secrets is None :
304
376
raise HomeAssistantError ("Secrets not supported in this YAML file" )
305
377
306
- return loader .secrets .get (loader .name , node .value )
307
-
308
-
309
- SafeLineLoader . add_constructor ("!include" , _include_yaml )
310
- SafeLineLoader . add_constructor (
311
- yaml . resolver . BaseResolver . DEFAULT_MAPPING_TAG , _ordered_dict
312
- )
313
- SafeLineLoader . add_constructor (
314
- yaml . resolver . BaseResolver . DEFAULT_SEQUENCE_TAG , _construct_seq
315
- )
316
- SafeLineLoader . add_constructor ("!env_var" , _env_var_yaml )
317
- SafeLineLoader . add_constructor ("!secret" , secret_yaml )
318
- SafeLineLoader . add_constructor ("!include_dir_list " , _include_dir_list_yaml )
319
- SafeLineLoader . add_constructor ("!include_dir_merge_list " , _include_dir_merge_list_yaml )
320
- SafeLineLoader . add_constructor ("!include_dir_named " , _include_dir_named_yaml )
321
- SafeLineLoader . add_constructor (
322
- "!include_dir_merge_named " , _include_dir_merge_named_yaml
323
- )
324
- SafeLineLoader . add_constructor ("!input" , Input .from_node )
378
+ return loader .secrets .get (loader .get_name () , node .value )
379
+
380
+
381
+ def add_constructor (tag : Any , constructor : Any ) -> None :
382
+ """Add to constructor to all loaders."""
383
+ for yaml_loader in ( SafeLoader , SafeLineLoader ):
384
+ yaml_loader . add_constructor ( tag , constructor )
385
+
386
+
387
+ add_constructor ( "!include" , _include_yaml )
388
+ add_constructor (yaml . resolver . BaseResolver . DEFAULT_MAPPING_TAG , _ordered_dict )
389
+ add_constructor (yaml . resolver . BaseResolver . DEFAULT_SEQUENCE_TAG , _construct_seq )
390
+ add_constructor ("!env_var " , _env_var_yaml )
391
+ add_constructor ("!secret " , secret_yaml )
392
+ add_constructor ("!include_dir_list " , _include_dir_list_yaml )
393
+ add_constructor ("!include_dir_merge_list" , _include_dir_merge_list_yaml )
394
+ add_constructor ( "!include_dir_named " , _include_dir_named_yaml )
395
+ add_constructor ( "!include_dir_merge_named" , _include_dir_merge_named_yaml )
396
+ add_constructor ("!input" , Input .from_node )
0 commit comments