hocon-parser 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hocon/__init__.py +68 -0
- hocon/_internal/__init__.py +4 -0
- hocon/_internal/lexer/__init__.py +1 -0
- hocon/_internal/lexer/lexer.py +500 -0
- hocon/_internal/lexer/token.py +65 -0
- hocon/_internal/parser/__init__.py +1 -0
- hocon/_internal/parser/ast.py +120 -0
- hocon/_internal/parser/empty_check.py +29 -0
- hocon/_internal/parser/parser.py +533 -0
- hocon/_internal/properties/__init__.py +4 -0
- hocon/_internal/properties/properties.py +84 -0
- hocon/_internal/resolver/__init__.py +1 -0
- hocon/_internal/resolver/fold_self_ref.py +282 -0
- hocon/_internal/resolver/include_loader.py +252 -0
- hocon/_internal/resolver/resolver.py +95 -0
- hocon/_internal/resolver/structure_builder.py +184 -0
- hocon/_internal/resolver/substitution_resolver.py +407 -0
- hocon/_internal/resolver/types.py +162 -0
- hocon/_internal/resolver/utils.py +144 -0
- hocon/coerce.py +257 -0
- hocon/config.py +524 -0
- hocon/errors.py +71 -0
- hocon/numeric_array.py +51 -0
- hocon/parse.py +122 -0
- hocon/py.typed +0 -0
- hocon/value.py +111 -0
- hocon/value_factory.py +71 -0
- hocon_parser-1.8.0.dist-info/METADATA +493 -0
- hocon_parser-1.8.0.dist-info/RECORD +31 -0
- hocon_parser-1.8.0.dist-info/WHEEL +4 -0
- hocon_parser-1.8.0.dist-info/licenses/LICENSE +202 -0
hocon/config.py
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
"""``Config`` — the resolved-configuration handle returned by the parse entry points.
|
|
2
|
+
|
|
3
|
+
Mirrors ts.hocon ``src/config.ts``. Accessor naming follows rs.hocon's
|
|
4
|
+
snake_case surface (``get_string`` / ``get_number`` / ``get_duration`` …), which
|
|
5
|
+
is already idiomatic Python. Deferred-resolution state (``resolve`` /
|
|
6
|
+
``resolve_with`` / ``with_fallback``, NotResolvedError semantics) follows E12.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import math
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, cast
|
|
15
|
+
|
|
16
|
+
from ._internal.resolver.resolver import (
|
|
17
|
+
build_partial_hocon_from_res_obj,
|
|
18
|
+
contains_placeholders,
|
|
19
|
+
hocon_value_to_res_obj,
|
|
20
|
+
resolve_tree,
|
|
21
|
+
val_contains_placeholders,
|
|
22
|
+
)
|
|
23
|
+
from ._internal.resolver.types import (
|
|
24
|
+
ConcatPlaceholder,
|
|
25
|
+
ResObj,
|
|
26
|
+
ResolveOptions,
|
|
27
|
+
SubstPlaceholder,
|
|
28
|
+
is_concat,
|
|
29
|
+
is_res_obj,
|
|
30
|
+
is_subst,
|
|
31
|
+
merge_unresolved,
|
|
32
|
+
)
|
|
33
|
+
from .coerce import (
|
|
34
|
+
ByteUnit,
|
|
35
|
+
DurationUnit,
|
|
36
|
+
coerce_boolean,
|
|
37
|
+
coerce_number,
|
|
38
|
+
parse_bytes,
|
|
39
|
+
parse_duration,
|
|
40
|
+
parse_period,
|
|
41
|
+
)
|
|
42
|
+
from .errors import ConfigError, NotResolvedError
|
|
43
|
+
from .numeric_array import numeric_object_to_array
|
|
44
|
+
from .value import HoconArray, HoconObject, HoconScalar, HoconValue, ScalarValueType
|
|
45
|
+
|
|
46
|
+
__all__ = ["Config", "Period"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class Period:
|
|
51
|
+
"""A calendar period with year, month, and day components.
|
|
52
|
+
|
|
53
|
+
Returned by :meth:`Config.get_period`. All fields are plain ``int``s;
|
|
54
|
+
negative periods are permitted (matches Lightbend behaviour). New fields
|
|
55
|
+
(e.g. weeks, hours) may be added in a future minor version — mirrors
|
|
56
|
+
rs.hocon's ``#[non_exhaustive]`` ``Period`` struct.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
years: int
|
|
60
|
+
months: int
|
|
61
|
+
days: int
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Config:
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
root: HoconObject,
|
|
68
|
+
*,
|
|
69
|
+
resolved: bool = True,
|
|
70
|
+
parse_base_dir: str | None = None,
|
|
71
|
+
origin_description: str | None = None,
|
|
72
|
+
res_obj_root: ResObj | None = None,
|
|
73
|
+
resolve_opts: ResolveOptions | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
self._root = root
|
|
76
|
+
self._resolved = resolved
|
|
77
|
+
self._parse_base_dir = parse_base_dir
|
|
78
|
+
self._origin_description = origin_description
|
|
79
|
+
self._res_obj_root = res_obj_root
|
|
80
|
+
self._resolve_opts = resolve_opts
|
|
81
|
+
|
|
82
|
+
# ─── construction ─────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
def is_resolved(self) -> bool:
|
|
85
|
+
"""True when the value tree contains no unresolved substitution
|
|
86
|
+
placeholders (whole-config granularity, E12 decision 11)."""
|
|
87
|
+
return self._resolved
|
|
88
|
+
|
|
89
|
+
@staticmethod
|
|
90
|
+
def _from_resolved_value(
|
|
91
|
+
root: HoconObject, origin_description: str | None = None
|
|
92
|
+
) -> Config:
|
|
93
|
+
return Config(root, resolved=True, origin_description=origin_description)
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def _from_unresolved_res_obj(
|
|
97
|
+
tree: ResObj,
|
|
98
|
+
*,
|
|
99
|
+
parse_base_dir: str | None,
|
|
100
|
+
origin_description: str | None,
|
|
101
|
+
resolved: bool,
|
|
102
|
+
resolve_opts: ResolveOptions,
|
|
103
|
+
) -> Config:
|
|
104
|
+
partial_root = build_partial_hocon_from_res_obj(tree)
|
|
105
|
+
return Config(
|
|
106
|
+
partial_root,
|
|
107
|
+
resolved=resolved,
|
|
108
|
+
parse_base_dir=parse_base_dir,
|
|
109
|
+
origin_description=origin_description,
|
|
110
|
+
res_obj_root=tree,
|
|
111
|
+
resolve_opts=resolve_opts,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# ─── accessors ────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
def get(self, path: str) -> Any:
|
|
117
|
+
v = self._lookup_node(path)
|
|
118
|
+
if v is None:
|
|
119
|
+
return None
|
|
120
|
+
return _hocon_to_py(v)
|
|
121
|
+
|
|
122
|
+
def get_value(self, path: str) -> HoconValue | None:
|
|
123
|
+
"""Raw HoconValue node at ``path`` for structural introspection, or None
|
|
124
|
+
if absent. Raises NotResolvedError when the node/subtree is unresolved."""
|
|
125
|
+
v = self._lookup_node(path)
|
|
126
|
+
if v is None:
|
|
127
|
+
if not self._resolved and self._subtree_has_placeholders(path):
|
|
128
|
+
raise NotResolvedError(path)
|
|
129
|
+
return None
|
|
130
|
+
if (
|
|
131
|
+
not isinstance(v, HoconScalar)
|
|
132
|
+
and not self._resolved
|
|
133
|
+
and self._subtree_has_placeholders(path)
|
|
134
|
+
):
|
|
135
|
+
raise NotResolvedError(path)
|
|
136
|
+
return v
|
|
137
|
+
|
|
138
|
+
def get_string(self, path: str) -> str:
|
|
139
|
+
v = self._require_scalar(path)
|
|
140
|
+
# S17.6 (HOCON.md L1252): null → any non-null type is an error.
|
|
141
|
+
if v.value_type == "null":
|
|
142
|
+
raise ConfigError(f"expected string at {path}, got {v.value_type}", path)
|
|
143
|
+
return v.raw
|
|
144
|
+
|
|
145
|
+
def get_number(self, path: str) -> float | int:
|
|
146
|
+
v = self._require_scalar(path)
|
|
147
|
+
coerced = coerce_number(v.raw)
|
|
148
|
+
if coerced is not None:
|
|
149
|
+
return coerced
|
|
150
|
+
raise ConfigError(f"expected number at {path}, got {v.value_type}", path)
|
|
151
|
+
|
|
152
|
+
def get_int(self, path: str) -> int:
|
|
153
|
+
"""Convenience: :meth:`get_number` truncated to an ``int``."""
|
|
154
|
+
return int(self.get_number(path))
|
|
155
|
+
|
|
156
|
+
def get_float(self, path: str) -> float:
|
|
157
|
+
"""Convenience: :meth:`get_number` as a ``float``."""
|
|
158
|
+
return float(self.get_number(path))
|
|
159
|
+
|
|
160
|
+
def get_boolean(self, path: str) -> bool:
|
|
161
|
+
v = self._require_scalar(path)
|
|
162
|
+
coerced = coerce_boolean(v.raw)
|
|
163
|
+
if coerced is not None:
|
|
164
|
+
return coerced
|
|
165
|
+
raise ConfigError(f"expected boolean at {path}, got {v.value_type}", path)
|
|
166
|
+
|
|
167
|
+
def get_duration(self, path: str, unit: DurationUnit | None = None) -> float:
|
|
168
|
+
v = self._require_scalar(path)
|
|
169
|
+
if v.value_type != "string" and v.value_type != "number":
|
|
170
|
+
raise ConfigError(f"expected duration at {path}, got {v.value_type}", path)
|
|
171
|
+
result = parse_duration(v.raw, unit)
|
|
172
|
+
if math.isnan(result):
|
|
173
|
+
raise ConfigError(f"invalid duration at {path}: {v.raw!r}", path)
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
def get_bytes(self, path: str, unit: ByteUnit | None = None) -> float:
|
|
177
|
+
v = self._require_scalar(path)
|
|
178
|
+
if v.value_type != "string" and v.value_type != "number":
|
|
179
|
+
raise ConfigError(f"expected byte size at {path}, got {v.value_type}", path)
|
|
180
|
+
result = parse_bytes(v.raw, unit)
|
|
181
|
+
if math.isnan(result):
|
|
182
|
+
raise ConfigError(f"invalid byte size at {path}: {v.raw!r}", path)
|
|
183
|
+
if result < 0:
|
|
184
|
+
raise ConfigError(f"byte size must be non-negative at {path}: {v.raw!r}", path)
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
def get_period(self, path: str) -> Period:
|
|
188
|
+
"""Calendar :class:`Period` at ``path`` (S20.1–S20.4). Accepts
|
|
189
|
+
``"7d"`` / ``"2w"`` / ``"3m"`` / ``"1y"`` style strings or a bare
|
|
190
|
+
integer, which is taken as days (HOCON.md L1321). Period is
|
|
191
|
+
integer-only (Lightbend ``Integer.parseInt``): fractional values
|
|
192
|
+
raise, unlike :meth:`get_duration` / :meth:`get_bytes`."""
|
|
193
|
+
v = self._require_scalar(path)
|
|
194
|
+
if v.value_type != "string" and v.value_type != "number":
|
|
195
|
+
raise ConfigError(f"expected period at {path}, got {v.value_type}", path)
|
|
196
|
+
parsed = parse_period(v.raw)
|
|
197
|
+
if parsed is None:
|
|
198
|
+
raise ConfigError(f"invalid period at {path}: {v.raw!r}", path)
|
|
199
|
+
return Period(*parsed)
|
|
200
|
+
|
|
201
|
+
def get_config(self, path: str) -> Config:
|
|
202
|
+
v = self._lookup_node(path)
|
|
203
|
+
if v is None:
|
|
204
|
+
if not self._resolved and self._subtree_has_placeholders(path):
|
|
205
|
+
raise NotResolvedError(path)
|
|
206
|
+
raise ConfigError(f"path not found: {path}", path)
|
|
207
|
+
if not isinstance(v, HoconObject):
|
|
208
|
+
raise ConfigError(f"expected object at {path}", path)
|
|
209
|
+
if not self._resolved and self._subtree_has_placeholders(path):
|
|
210
|
+
raise NotResolvedError(path)
|
|
211
|
+
return Config(v, resolved=self._resolved)
|
|
212
|
+
|
|
213
|
+
def get_list(self, path: str) -> list[Any]:
|
|
214
|
+
v = self._lookup_node(path)
|
|
215
|
+
if v is None:
|
|
216
|
+
if not self._resolved and self._subtree_has_placeholders(path):
|
|
217
|
+
raise NotResolvedError(path)
|
|
218
|
+
raise ConfigError(f"path not found: {path}", path)
|
|
219
|
+
if not self._resolved and self._subtree_has_placeholders(path):
|
|
220
|
+
raise NotResolvedError(path)
|
|
221
|
+
# S15: numerically-keyed object → array before the type check.
|
|
222
|
+
if isinstance(v, HoconObject):
|
|
223
|
+
converted = numeric_object_to_array(v)
|
|
224
|
+
if converted is not None:
|
|
225
|
+
return [_hocon_to_py(x) for x in converted]
|
|
226
|
+
if not isinstance(v, HoconArray):
|
|
227
|
+
raise ConfigError(f"expected array at {path}", path)
|
|
228
|
+
return [_hocon_to_py(x) for x in v.items]
|
|
229
|
+
|
|
230
|
+
def has(self, path: str) -> bool:
|
|
231
|
+
return self._lookup_node(path) is not None
|
|
232
|
+
|
|
233
|
+
def keys(self) -> list[str]:
|
|
234
|
+
return list(self._root.fields.keys())
|
|
235
|
+
|
|
236
|
+
def with_fallback(self, fallback: Config | None) -> Config:
|
|
237
|
+
if fallback is None:
|
|
238
|
+
return self
|
|
239
|
+
self_tree = self._res_obj_root or hocon_value_to_res_obj(self._root)
|
|
240
|
+
fb_tree = fallback._res_obj_root or hocon_value_to_res_obj(fallback._root)
|
|
241
|
+
merged = merge_unresolved(self_tree, fb_tree)
|
|
242
|
+
has_placeholders = contains_placeholders(merged)
|
|
243
|
+
partial_root = build_partial_hocon_from_res_obj(merged)
|
|
244
|
+
return Config(
|
|
245
|
+
partial_root,
|
|
246
|
+
resolved=not has_placeholders,
|
|
247
|
+
parse_base_dir=self._parse_base_dir,
|
|
248
|
+
origin_description=self._origin_description,
|
|
249
|
+
res_obj_root=merged,
|
|
250
|
+
resolve_opts=self._resolve_opts or fallback._resolve_opts,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def resolve(
|
|
254
|
+
self, *, allow_unresolved: bool = False, use_system_environment: bool = True
|
|
255
|
+
) -> Config:
|
|
256
|
+
"""Run substitution resolution (phase 2) on the stored unresolved tree.
|
|
257
|
+
Idempotent on an already-resolved Config (E12 decision 3)."""
|
|
258
|
+
if self._resolved:
|
|
259
|
+
return Config(
|
|
260
|
+
self._root,
|
|
261
|
+
resolved=True,
|
|
262
|
+
parse_base_dir=self._parse_base_dir,
|
|
263
|
+
origin_description=self._origin_description,
|
|
264
|
+
)
|
|
265
|
+
tree = self._res_obj_root
|
|
266
|
+
if tree is None:
|
|
267
|
+
return Config(self._root, resolved=True)
|
|
268
|
+
|
|
269
|
+
resolve_opts = self._effective_resolve_opts(allow_unresolved, use_system_environment)
|
|
270
|
+
resolved = resolve_tree(tree, resolve_opts)
|
|
271
|
+
if not isinstance(resolved, HoconObject):
|
|
272
|
+
raise RuntimeError("resolve: expected object root")
|
|
273
|
+
|
|
274
|
+
if allow_unresolved:
|
|
275
|
+
stripped, had_placeholders = _strip_placeholder_fields(resolved)
|
|
276
|
+
return Config(
|
|
277
|
+
stripped,
|
|
278
|
+
resolved=not had_placeholders,
|
|
279
|
+
parse_base_dir=self._parse_base_dir,
|
|
280
|
+
origin_description=self._origin_description,
|
|
281
|
+
res_obj_root=tree if had_placeholders else None,
|
|
282
|
+
resolve_opts=resolve_opts if had_placeholders else None,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
return Config(
|
|
286
|
+
resolved,
|
|
287
|
+
resolved=True,
|
|
288
|
+
parse_base_dir=self._parse_base_dir,
|
|
289
|
+
origin_description=self._origin_description,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def resolve_with(
|
|
293
|
+
self,
|
|
294
|
+
source: Config,
|
|
295
|
+
*,
|
|
296
|
+
allow_unresolved: bool = False,
|
|
297
|
+
use_system_environment: bool = True,
|
|
298
|
+
) -> Config:
|
|
299
|
+
"""Resolve receiver substitutions using ``source`` as lookup context;
|
|
300
|
+
source's keys are NOT merged into the result (E12 decisions 9, 10)."""
|
|
301
|
+
if not source.is_resolved():
|
|
302
|
+
raise NotResolvedError("source")
|
|
303
|
+
if self._resolved:
|
|
304
|
+
return Config(
|
|
305
|
+
self._root,
|
|
306
|
+
resolved=True,
|
|
307
|
+
parse_base_dir=self._parse_base_dir,
|
|
308
|
+
origin_description=self._origin_description,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
receiver_tree = self._res_obj_root or hocon_value_to_res_obj(self._root)
|
|
312
|
+
src_tree = hocon_value_to_res_obj(source._root)
|
|
313
|
+
merged = merge_unresolved(receiver_tree, src_tree)
|
|
314
|
+
|
|
315
|
+
resolve_opts = self._effective_resolve_opts(allow_unresolved, use_system_environment)
|
|
316
|
+
resolved = resolve_tree(merged, resolve_opts)
|
|
317
|
+
if not isinstance(resolved, HoconObject):
|
|
318
|
+
raise RuntimeError("resolve_with: expected object root")
|
|
319
|
+
|
|
320
|
+
receiver_shape = _res_obj_to_key_shape(receiver_tree)
|
|
321
|
+
filtered = _filter_by_receiver_shape(resolved, receiver_shape)
|
|
322
|
+
|
|
323
|
+
if allow_unresolved:
|
|
324
|
+
stripped, had_placeholders = _strip_placeholder_fields(filtered)
|
|
325
|
+
return Config(
|
|
326
|
+
stripped,
|
|
327
|
+
resolved=not had_placeholders,
|
|
328
|
+
parse_base_dir=self._parse_base_dir,
|
|
329
|
+
origin_description=self._origin_description,
|
|
330
|
+
res_obj_root=receiver_tree if had_placeholders else None,
|
|
331
|
+
resolve_opts=resolve_opts if had_placeholders else None,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
return Config(
|
|
335
|
+
filtered,
|
|
336
|
+
resolved=True,
|
|
337
|
+
parse_base_dir=self._parse_base_dir,
|
|
338
|
+
origin_description=self._origin_description,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def to_object(self) -> Any:
|
|
342
|
+
return _hocon_to_py(self._root)
|
|
343
|
+
|
|
344
|
+
def _render_json_for_test(self) -> str:
|
|
345
|
+
"""Test-only: render this resolved Config as canonical JSON (sorted keys,
|
|
346
|
+
no whitespace). Used by the conformance harness. Raises on placeholders."""
|
|
347
|
+
return _render_hocon_as_json(self._root)
|
|
348
|
+
|
|
349
|
+
# ─── internals ────────────────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
def _effective_resolve_opts(
|
|
352
|
+
self, allow_unresolved: bool, use_system_environment: bool
|
|
353
|
+
) -> ResolveOptions:
|
|
354
|
+
import dataclasses
|
|
355
|
+
|
|
356
|
+
base = self._resolve_opts or ResolveOptions()
|
|
357
|
+
return dataclasses.replace(
|
|
358
|
+
base,
|
|
359
|
+
allow_unresolved=allow_unresolved,
|
|
360
|
+
use_system_environment=use_system_environment,
|
|
361
|
+
origin_description=self._origin_description,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
def _lookup_node(self, path: str) -> HoconValue | None:
|
|
365
|
+
segments = _split_config_path(path)
|
|
366
|
+
current: HoconValue = self._root
|
|
367
|
+
for seg in segments:
|
|
368
|
+
if not isinstance(current, HoconObject):
|
|
369
|
+
return None
|
|
370
|
+
nxt = current.fields.get(seg)
|
|
371
|
+
if nxt is None:
|
|
372
|
+
return None
|
|
373
|
+
current = nxt
|
|
374
|
+
return current
|
|
375
|
+
|
|
376
|
+
def _subtree_has_placeholders(self, path: str) -> bool:
|
|
377
|
+
tree = self._res_obj_root
|
|
378
|
+
if tree is None:
|
|
379
|
+
return False
|
|
380
|
+
segments = _split_config_path(path)
|
|
381
|
+
cur: ResObj = tree
|
|
382
|
+
for seg in segments:
|
|
383
|
+
val = cur.fields.get(seg)
|
|
384
|
+
if val is None:
|
|
385
|
+
return False
|
|
386
|
+
if is_res_obj(val):
|
|
387
|
+
cur = val
|
|
388
|
+
else:
|
|
389
|
+
return val_contains_placeholders(val)
|
|
390
|
+
return contains_placeholders(cur)
|
|
391
|
+
|
|
392
|
+
def _require_scalar(self, path: str) -> HoconScalar:
|
|
393
|
+
v = self._lookup_node(path)
|
|
394
|
+
if v is None:
|
|
395
|
+
if not self._resolved and self._subtree_has_placeholders(path):
|
|
396
|
+
raise NotResolvedError(path)
|
|
397
|
+
raise ConfigError(f"path not found: {path}", path)
|
|
398
|
+
if not isinstance(v, HoconScalar):
|
|
399
|
+
raise ConfigError(f"expected scalar at {path}, got {v.kind}", path)
|
|
400
|
+
return v
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _split_config_path(path: str) -> list[str]:
|
|
404
|
+
segments: list[str] = []
|
|
405
|
+
i = 0
|
|
406
|
+
while i < len(path):
|
|
407
|
+
if path[i] == '"':
|
|
408
|
+
i += 1
|
|
409
|
+
segment = ""
|
|
410
|
+
closed = False
|
|
411
|
+
while i < len(path):
|
|
412
|
+
ch = path[i]
|
|
413
|
+
if ch == "\\" and i + 1 < len(path):
|
|
414
|
+
segment += path[i + 1]
|
|
415
|
+
i += 2
|
|
416
|
+
continue
|
|
417
|
+
if ch == '"':
|
|
418
|
+
closed = True
|
|
419
|
+
i += 1
|
|
420
|
+
break
|
|
421
|
+
segment += ch
|
|
422
|
+
i += 1
|
|
423
|
+
if not closed:
|
|
424
|
+
raise ConfigError(f"unterminated quoted path segment: {path}", path)
|
|
425
|
+
segments.append(segment)
|
|
426
|
+
if i < len(path) and path[i] == ".":
|
|
427
|
+
i += 1
|
|
428
|
+
else:
|
|
429
|
+
dot = path.find(".", i)
|
|
430
|
+
if dot == -1:
|
|
431
|
+
segments.append(path[i:])
|
|
432
|
+
break
|
|
433
|
+
segments.append(path[i:dot])
|
|
434
|
+
i = dot + 1
|
|
435
|
+
return segments
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _scalar_to_py(raw: str, value_type: ScalarValueType) -> Any:
|
|
439
|
+
if value_type == "null":
|
|
440
|
+
return None
|
|
441
|
+
if value_type == "boolean":
|
|
442
|
+
return raw == "true"
|
|
443
|
+
if value_type == "number":
|
|
444
|
+
coerced = coerce_number(raw)
|
|
445
|
+
return coerced if coerced is not None else raw
|
|
446
|
+
return raw
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _hocon_to_py(v: HoconValue) -> Any:
|
|
450
|
+
if isinstance(v, HoconScalar):
|
|
451
|
+
return _scalar_to_py(v.raw, v.value_type)
|
|
452
|
+
if isinstance(v, HoconArray):
|
|
453
|
+
return [_hocon_to_py(item) for item in v.items]
|
|
454
|
+
return {k: _hocon_to_py(val) for k, val in v.fields.items()}
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _res_obj_to_key_shape(tree: ResObj) -> HoconObject:
|
|
458
|
+
fields: dict[str, HoconValue] = {}
|
|
459
|
+
for k, v in tree.fields.items():
|
|
460
|
+
if is_subst(v) or is_concat(v):
|
|
461
|
+
fields[k] = HoconScalar("", "null")
|
|
462
|
+
elif is_res_obj(v):
|
|
463
|
+
fields[k] = _res_obj_to_key_shape(v)
|
|
464
|
+
else:
|
|
465
|
+
# Neither placeholder nor ResObj → a plain HoconValue.
|
|
466
|
+
fields[k] = cast("HoconValue", v)
|
|
467
|
+
return HoconObject(fields)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _strip_placeholder_fields(v: HoconObject) -> tuple[HoconObject, bool]:
|
|
471
|
+
fields: dict[str, HoconValue] = {}
|
|
472
|
+
had_placeholders = False
|
|
473
|
+
for k, val in v.fields.items():
|
|
474
|
+
if isinstance(val, (SubstPlaceholder, ConcatPlaceholder)):
|
|
475
|
+
had_placeholders = True
|
|
476
|
+
elif isinstance(val, HoconObject):
|
|
477
|
+
inner, inner_had = _strip_placeholder_fields(val)
|
|
478
|
+
if inner_had:
|
|
479
|
+
had_placeholders = True
|
|
480
|
+
fields[k] = inner
|
|
481
|
+
else:
|
|
482
|
+
fields[k] = val
|
|
483
|
+
return HoconObject(fields), had_placeholders
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _filter_by_receiver_shape(resolved: HoconObject, receiver_shape: HoconObject) -> HoconObject:
|
|
487
|
+
fields: dict[str, HoconValue] = {}
|
|
488
|
+
for k, rv in resolved.fields.items():
|
|
489
|
+
if k not in receiver_shape.fields:
|
|
490
|
+
continue
|
|
491
|
+
receiver_val = receiver_shape.fields[k]
|
|
492
|
+
if isinstance(rv, HoconObject) and isinstance(receiver_val, HoconObject):
|
|
493
|
+
fields[k] = _filter_by_receiver_shape(rv, receiver_val)
|
|
494
|
+
else:
|
|
495
|
+
fields[k] = rv
|
|
496
|
+
return HoconObject(fields)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
# RFC 8259 JSON number grammar — gates the numeric render fast-path.
|
|
500
|
+
_JSON_NUMBER_RE = re.compile(r"-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?")
|
|
501
|
+
_LEADING_ZERO_RE = re.compile(r"^([+-]?)0+(\d)")
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _normalize_leading_zero_number(raw: str) -> str:
|
|
505
|
+
return _LEADING_ZERO_RE.sub(r"\1\2", raw)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def _render_hocon_as_json(v: HoconValue) -> str:
|
|
509
|
+
import json
|
|
510
|
+
|
|
511
|
+
if isinstance(v, HoconScalar):
|
|
512
|
+
if v.value_type == "null":
|
|
513
|
+
return "null"
|
|
514
|
+
if v.value_type == "boolean":
|
|
515
|
+
return v.raw
|
|
516
|
+
if v.value_type == "number":
|
|
517
|
+
norm = _normalize_leading_zero_number(v.raw)
|
|
518
|
+
return norm if _JSON_NUMBER_RE.fullmatch(norm) else json.dumps(v.raw)
|
|
519
|
+
return json.dumps(v.raw)
|
|
520
|
+
if isinstance(v, HoconArray):
|
|
521
|
+
return "[" + ",".join(_render_hocon_as_json(item) for item in v.items) + "]"
|
|
522
|
+
keys = sorted(v.fields.keys())
|
|
523
|
+
pairs = [f"{json.dumps(k)}:{_render_hocon_as_json(v.fields[k])}" for k in keys]
|
|
524
|
+
return "{" + ",".join(pairs) + "}"
|
hocon/errors.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Error types raised by the parser, resolver, and Config accessors.
|
|
2
|
+
|
|
3
|
+
Mirrors ts.hocon ``src/errors.ts``. The hierarchy is intentionally split in
|
|
4
|
+
two: ``ParseError`` / ``ResolveError`` belong to the parse/resolve pipeline,
|
|
5
|
+
while ``ConfigError`` covers accessor-time failures on a ``Config``. They do
|
|
6
|
+
not share a common base beyond ``Exception``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ConfigError",
|
|
13
|
+
"NotResolvedError",
|
|
14
|
+
"PackageLookupError",
|
|
15
|
+
"ParseError",
|
|
16
|
+
"ResolveError",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ParseError(Exception):
|
|
21
|
+
def __init__(self, message: str, line: int, col: int, file: str | None = None) -> None:
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.line = line
|
|
24
|
+
self.col = col
|
|
25
|
+
self.file = file
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ResolveError(Exception):
|
|
29
|
+
def __init__(
|
|
30
|
+
self, message: str, path: str, line: int, col: int, file: str | None = None
|
|
31
|
+
) -> None:
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
self.path = path
|
|
34
|
+
self.line = line
|
|
35
|
+
self.col = col
|
|
36
|
+
self.file = file
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PackageLookupError(ResolveError):
|
|
40
|
+
"""Raised when ``include package("id", "file")`` resolution fails because the
|
|
41
|
+
package cannot be located.
|
|
42
|
+
|
|
43
|
+
Extends ``ResolveError`` so callers who catch ``ResolveError`` still handle
|
|
44
|
+
it, while callers who need to distinguish a missing-package error can use
|
|
45
|
+
``isinstance(err, PackageLookupError)``.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self, message: str, identifier: str, package_file: str, line: int, col: int
|
|
50
|
+
) -> None:
|
|
51
|
+
super().__init__(message, f"{identifier}/{package_file}", line, col)
|
|
52
|
+
self.identifier = identifier
|
|
53
|
+
self.package_file = package_file
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ConfigError(Exception):
|
|
57
|
+
def __init__(self, message: str, path: str) -> None:
|
|
58
|
+
super().__init__(message)
|
|
59
|
+
self.path = path
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class NotResolvedError(ConfigError):
|
|
63
|
+
"""Raised when a getter is called on a Config whose value (or any transitive
|
|
64
|
+
parent) contains an unresolved substitution placeholder. E12 decision 12.
|
|
65
|
+
|
|
66
|
+
Detect with ``isinstance(err, NotResolvedError)``; it also passes
|
|
67
|
+
``isinstance(err, ConfigError)``.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, path: str) -> None:
|
|
71
|
+
super().__init__(f'value at path "{path}" is not resolved (call resolve() first)', path)
|
hocon/numeric_array.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""S15 — numerically-indexed object → array conversion helper.
|
|
2
|
+
|
|
3
|
+
Mirrors ts.hocon ``src/value/numeric-array.ts``. Private helper shared between
|
|
4
|
+
the accessor call site (``Config.get_list``) and the resolver concat call site
|
|
5
|
+
(``SubstitutionResolver._resolve_concat``); not part of the public API.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from .value import HoconObject, HoconValue
|
|
13
|
+
|
|
14
|
+
__all__ = ["numeric_object_to_array"]
|
|
15
|
+
|
|
16
|
+
# Canonical decimal non-negative integers only.
|
|
17
|
+
# Rejects: "+1", "-0", "-1", "00", "01", " 1", "1 ", "0x1", "1e2", "1.0", "".
|
|
18
|
+
_CANONICAL_INT = re.compile(r"(?:0|[1-9][0-9]*)")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def numeric_object_to_array(value: HoconValue) -> list[HoconValue] | None:
|
|
22
|
+
"""Attempt to convert a numerically-keyed HOCON object to an array.
|
|
23
|
+
|
|
24
|
+
- not an object → None
|
|
25
|
+
- empty object → None (S15.4: empty NOT converted)
|
|
26
|
+
- no key parses as non-neg int → None
|
|
27
|
+
- otherwise → values sorted by parsed key (ascending)
|
|
28
|
+
|
|
29
|
+
Conversion is non-recursive: only top-level keys are examined. A new list is
|
|
30
|
+
returned; the original object is not mutated.
|
|
31
|
+
"""
|
|
32
|
+
if not isinstance(value, HoconObject):
|
|
33
|
+
return None
|
|
34
|
+
if len(value.fields) == 0:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
eligible: list[tuple[int, HoconValue]] = []
|
|
38
|
+
for key, val in value.fields.items():
|
|
39
|
+
if not _CANONICAL_INT.fullmatch(key):
|
|
40
|
+
continue
|
|
41
|
+
n = int(key)
|
|
42
|
+
# i32 max: 2147483647
|
|
43
|
+
if n > 2_147_483_647:
|
|
44
|
+
continue
|
|
45
|
+
eligible.append((n, val))
|
|
46
|
+
|
|
47
|
+
if not eligible:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
eligible.sort(key=lambda e: e[0])
|
|
51
|
+
return [e[1] for e in eligible]
|