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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Resolution driver. Mirrors ts.hocon ``src/internal/resolver/resolver.ts``.
|
|
2
|
+
|
|
3
|
+
Two-phase: :func:`build_tree` (phase 1 — placeholders left in place, includes
|
|
4
|
+
expanded) then :func:`resolve_tree` (phase 2 — placeholders replaced). Sync path
|
|
5
|
+
only (py.hocon has no async parse variant).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import dataclasses
|
|
11
|
+
from typing import cast
|
|
12
|
+
|
|
13
|
+
from ...value import HoconArray, HoconObject, HoconValue
|
|
14
|
+
from ..parser.ast import AstNode
|
|
15
|
+
from .structure_builder import StructureBuilder
|
|
16
|
+
from .substitution_resolver import SubstitutionResolver
|
|
17
|
+
from .types import (
|
|
18
|
+
ResObj,
|
|
19
|
+
ResolveOptions,
|
|
20
|
+
ResolverValue,
|
|
21
|
+
is_concat,
|
|
22
|
+
is_res_obj,
|
|
23
|
+
is_subst,
|
|
24
|
+
make_res_obj,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"build_partial_hocon_from_res_obj",
|
|
29
|
+
"build_tree",
|
|
30
|
+
"contains_placeholders",
|
|
31
|
+
"hocon_value_to_res_obj",
|
|
32
|
+
"resolve",
|
|
33
|
+
"resolve_tree",
|
|
34
|
+
"val_contains_placeholders",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_tree(ast: AstNode, opts: ResolveOptions) -> ResObj:
|
|
39
|
+
"""Phase 1: builds a ResObj tree with substitution/concat placeholders
|
|
40
|
+
unresolved. Includes are fully expanded."""
|
|
41
|
+
return StructureBuilder(opts).build(ast)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def resolve_tree(tree: ResObj, opts: ResolveOptions) -> HoconValue:
|
|
45
|
+
"""Phase 2: replaces placeholders in a ResObj tree → fully-resolved value."""
|
|
46
|
+
return SubstitutionResolver(tree, opts).resolve()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def contains_placeholders(tree: ResObj) -> bool:
|
|
50
|
+
return any(val_contains_placeholders(val) for val in tree.fields.values())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def val_contains_placeholders(v: ResolverValue) -> bool:
|
|
54
|
+
if is_subst(v) or is_concat(v):
|
|
55
|
+
return True
|
|
56
|
+
if is_res_obj(v):
|
|
57
|
+
return contains_placeholders(v)
|
|
58
|
+
if isinstance(v, HoconArray):
|
|
59
|
+
return any(val_contains_placeholders(item) for item in v.items)
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def build_partial_hocon_from_res_obj(tree: ResObj) -> HoconObject:
|
|
64
|
+
"""Extract resolved (non-placeholder) fields from a ResObj into a plain
|
|
65
|
+
HoconValue object. Placeholder-valued fields are omitted so ``Config``
|
|
66
|
+
accessors return absent → NotResolvedError."""
|
|
67
|
+
fields: dict[str, HoconValue] = {}
|
|
68
|
+
for k, v in tree.fields.items():
|
|
69
|
+
if not is_subst(v) and not is_concat(v):
|
|
70
|
+
if is_res_obj(v):
|
|
71
|
+
fields[k] = build_partial_hocon_from_res_obj(v)
|
|
72
|
+
elif not val_contains_placeholders(v):
|
|
73
|
+
# Not a placeholder and placeholder-free → a plain HoconValue.
|
|
74
|
+
fields[k] = cast("HoconValue", v)
|
|
75
|
+
return HoconObject(fields)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def hocon_value_to_res_obj(value: HoconObject) -> ResObj:
|
|
79
|
+
"""Convert a fully-resolved HoconValue object into a ResObj (for withFallback)."""
|
|
80
|
+
obj = make_res_obj()
|
|
81
|
+
for k, v in value.fields.items():
|
|
82
|
+
if isinstance(v, HoconObject):
|
|
83
|
+
obj.fields[k] = hocon_value_to_res_obj(v)
|
|
84
|
+
else:
|
|
85
|
+
obj.fields[k] = v
|
|
86
|
+
return obj
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def resolve(ast: AstNode, opts: ResolveOptions) -> HoconValue:
|
|
90
|
+
"""Fused phase 1 + phase 2 (default parse behaviour)."""
|
|
91
|
+
tree = build_tree(ast, opts)
|
|
92
|
+
return resolve_tree(
|
|
93
|
+
tree,
|
|
94
|
+
dataclasses.replace(opts, use_system_environment=True, allow_unresolved=False),
|
|
95
|
+
)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Pass 1: builds a ResObj tree from AST nodes.
|
|
2
|
+
|
|
3
|
+
Mirrors ts.hocon ``src/internal/resolver/structure-builder.ts`` (sync path only;
|
|
4
|
+
py.hocon has no async parse variant). Encapsulates structure building, include
|
|
5
|
+
loading, and substitution-path relativization for nested includes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from ...errors import ResolveError
|
|
11
|
+
from ...value import HoconArray, HoconScalar
|
|
12
|
+
from ..lexer.token import Segment
|
|
13
|
+
from ..parser.ast import (
|
|
14
|
+
AstArray,
|
|
15
|
+
AstConcat,
|
|
16
|
+
AstField,
|
|
17
|
+
AstInclude,
|
|
18
|
+
AstNode,
|
|
19
|
+
AstObject,
|
|
20
|
+
AstScalar,
|
|
21
|
+
AstSubst,
|
|
22
|
+
)
|
|
23
|
+
from .fold_self_ref import (
|
|
24
|
+
contains_self_ref,
|
|
25
|
+
fold_nested_self_refs,
|
|
26
|
+
fold_or_skip_prior,
|
|
27
|
+
string_segments_to_key,
|
|
28
|
+
)
|
|
29
|
+
from .include_loader import IncludeLoader
|
|
30
|
+
from .types import (
|
|
31
|
+
ConcatPlaceholder,
|
|
32
|
+
ResObj,
|
|
33
|
+
ResolveOptions,
|
|
34
|
+
ResolverValue,
|
|
35
|
+
SubstPlaceholder,
|
|
36
|
+
is_concat,
|
|
37
|
+
is_res_obj,
|
|
38
|
+
is_subst,
|
|
39
|
+
make_res_obj,
|
|
40
|
+
separator_values,
|
|
41
|
+
)
|
|
42
|
+
from .utils import deep_merge_res_obj_into
|
|
43
|
+
|
|
44
|
+
__all__ = ["StructureBuilder"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class StructureBuilder:
|
|
48
|
+
def __init__(self, opts: ResolveOptions) -> None:
|
|
49
|
+
self.loader = IncludeLoader(opts)
|
|
50
|
+
self.loader.on_build_res_obj = lambda a, o: StructureBuilder(o).build(a)
|
|
51
|
+
|
|
52
|
+
def build(self, ast: AstNode, path_prefix: list[str] | None = None) -> ResObj:
|
|
53
|
+
if path_prefix is None:
|
|
54
|
+
path_prefix = []
|
|
55
|
+
if not isinstance(ast, AstObject):
|
|
56
|
+
raise ResolveError("root AST must be an object", "", ast.pos.line, ast.pos.col)
|
|
57
|
+
obj = make_res_obj()
|
|
58
|
+
for fld in ast.fields:
|
|
59
|
+
self._apply_field(obj, fld, path_prefix)
|
|
60
|
+
return obj
|
|
61
|
+
|
|
62
|
+
def _apply_field(self, obj: ResObj, field: AstField, path_prefix: list[str]) -> None:
|
|
63
|
+
# include directive: key empty, value is include node.
|
|
64
|
+
if len(field.key) == 0 and isinstance(field.value, AstInclude):
|
|
65
|
+
qualifier = field.value.qualifier
|
|
66
|
+
path = field.value.path
|
|
67
|
+
required = field.value.required
|
|
68
|
+
if qualifier.kind == "package":
|
|
69
|
+
included = self.loader.load_package(qualifier.identifier, path, required)
|
|
70
|
+
else:
|
|
71
|
+
included = self.loader.load(path, required, qualifier.kind == "file")
|
|
72
|
+
if len(path_prefix) > 0:
|
|
73
|
+
self._relativize_res_obj(included, path_prefix)
|
|
74
|
+
deep_merge_res_obj_into(obj, included, path_prefix)
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
if len(field.key) == 0:
|
|
78
|
+
return
|
|
79
|
+
head, tail = field.key[0], field.key[1:]
|
|
80
|
+
|
|
81
|
+
if len(tail) > 0:
|
|
82
|
+
# Nested key: server.host = "x" → synthetic object AST.
|
|
83
|
+
synthetic_ast = AstObject(
|
|
84
|
+
[AstField(tail, field.value, field.append, field.pos)], field.pos
|
|
85
|
+
)
|
|
86
|
+
self._apply_field(
|
|
87
|
+
obj, AstField([head], synthetic_ast, False, field.pos), path_prefix
|
|
88
|
+
)
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
child_prefix = [*path_prefix, head]
|
|
92
|
+
full_key = string_segments_to_key(child_prefix)
|
|
93
|
+
|
|
94
|
+
if field.append:
|
|
95
|
+
# S13b.2: `a += b` ≡ `a = ${?a} [b]` (HOCON.md L732). Desugar and
|
|
96
|
+
# re-dispatch so += flows through the chained-self-ref machinery.
|
|
97
|
+
synthetic = self._desugar_append(field, child_prefix)
|
|
98
|
+
self._apply_field(obj, synthetic, path_prefix)
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
existing = obj.fields.get(head)
|
|
102
|
+
new_val = self._ast_to_resolver_value(field.value, child_prefix)
|
|
103
|
+
|
|
104
|
+
# go.hocon#134: a non-self-referential assignment is a *reset*.
|
|
105
|
+
if not contains_self_ref(new_val, full_key):
|
|
106
|
+
obj.reset_keys.add(head)
|
|
107
|
+
|
|
108
|
+
# Save prior value for self-referential substitution resolution.
|
|
109
|
+
if existing is not None:
|
|
110
|
+
old_prior = obj.prior_values.get(head)
|
|
111
|
+
prior_input = (
|
|
112
|
+
fold_nested_self_refs(existing, child_prefix)
|
|
113
|
+
if is_res_obj(existing)
|
|
114
|
+
else existing
|
|
115
|
+
)
|
|
116
|
+
prior = fold_or_skip_prior(prior_input, full_key, old_prior)
|
|
117
|
+
if prior is not None:
|
|
118
|
+
obj.prior_values[head] = prior
|
|
119
|
+
|
|
120
|
+
# Deep merge if both are ResObj.
|
|
121
|
+
if existing is not None and is_res_obj(existing) and is_res_obj(new_val):
|
|
122
|
+
deep_merge_res_obj_into(existing, new_val, child_prefix)
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
obj.fields[head] = new_val
|
|
126
|
+
|
|
127
|
+
def _desugar_append(self, field: AstField, child_prefix: list[str]) -> AstField:
|
|
128
|
+
"""Build the ``key = ${?fullkey} [value]`` field that ``key += value``
|
|
129
|
+
desugars to (S13b.2, HOCON.md L732)."""
|
|
130
|
+
segments = [Segment(text, field.pos.line, field.pos.col) for text in child_prefix]
|
|
131
|
+
subst = AstSubst(segments, True, False, field.pos)
|
|
132
|
+
elem_array = AstArray([field.value], field.pos)
|
|
133
|
+
synthetic = AstConcat([subst, elem_array], field.pos)
|
|
134
|
+
return AstField(field.key, synthetic, False, field.pos)
|
|
135
|
+
|
|
136
|
+
def _ast_to_resolver_value(self, ast: AstNode, path_prefix: list[str]) -> ResolverValue:
|
|
137
|
+
if isinstance(ast, AstScalar):
|
|
138
|
+
sv = HoconScalar(ast.raw, ast.value_type)
|
|
139
|
+
if ast.separator:
|
|
140
|
+
separator_values.add(sv)
|
|
141
|
+
return sv
|
|
142
|
+
if isinstance(ast, AstArray):
|
|
143
|
+
return HoconArray(
|
|
144
|
+
[self._ast_to_resolver_value(i, path_prefix) for i in ast.items] # type: ignore[misc]
|
|
145
|
+
)
|
|
146
|
+
if isinstance(ast, AstObject):
|
|
147
|
+
return self.build(ast, path_prefix)
|
|
148
|
+
if isinstance(ast, AstSubst):
|
|
149
|
+
return SubstPlaceholder(
|
|
150
|
+
ast.segments, ast.optional, False, ast.list_suffix, ast.pos.line, ast.pos.col, 0
|
|
151
|
+
)
|
|
152
|
+
if isinstance(ast, AstConcat):
|
|
153
|
+
return ConcatPlaceholder(
|
|
154
|
+
[self._ast_to_resolver_value(n, path_prefix) for n in ast.nodes],
|
|
155
|
+
ast.pos.line,
|
|
156
|
+
ast.pos.col,
|
|
157
|
+
)
|
|
158
|
+
# AstInclude — handled by _apply_field; should not reach here.
|
|
159
|
+
return HoconScalar("null", "null")
|
|
160
|
+
|
|
161
|
+
# ---- Relativize substitution paths for nested includes ----
|
|
162
|
+
|
|
163
|
+
def _relativize_subst_paths(self, val: ResolverValue, prefix_segments: list[str]) -> None:
|
|
164
|
+
if is_subst(val):
|
|
165
|
+
prefix_as_segments = [Segment(text, 0, 0) for text in prefix_segments]
|
|
166
|
+
val.segments = [*prefix_as_segments, *val.segments]
|
|
167
|
+
val.prefix_len += len(prefix_segments)
|
|
168
|
+
return
|
|
169
|
+
if is_concat(val):
|
|
170
|
+
for node in val.nodes:
|
|
171
|
+
self._relativize_subst_paths(node, prefix_segments)
|
|
172
|
+
return
|
|
173
|
+
if is_res_obj(val):
|
|
174
|
+
self._relativize_res_obj(val, prefix_segments)
|
|
175
|
+
return
|
|
176
|
+
if isinstance(val, HoconArray):
|
|
177
|
+
for item in val.items:
|
|
178
|
+
self._relativize_subst_paths(item, prefix_segments)
|
|
179
|
+
|
|
180
|
+
def _relativize_res_obj(self, obj: ResObj, prefix_segments: list[str]) -> None:
|
|
181
|
+
for val in obj.fields.values():
|
|
182
|
+
self._relativize_subst_paths(val, prefix_segments)
|
|
183
|
+
for val in obj.prior_values.values():
|
|
184
|
+
self._relativize_subst_paths(val, prefix_segments)
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"""Pass 2: replaces substitution and concat placeholders with resolved values.
|
|
2
|
+
|
|
3
|
+
Mirrors ts.hocon ``src/internal/resolver/substitution-resolver.ts``. The
|
|
4
|
+
self-reference detection (owner gate + path walk), delayed object merge, S14c.2
|
|
5
|
+
original-path fallback, and S13c env-var-list expansion are ported line-for-line;
|
|
6
|
+
see the inline S-references for the governing spec items.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import weakref
|
|
12
|
+
from typing import cast
|
|
13
|
+
|
|
14
|
+
from ...errors import ResolveError
|
|
15
|
+
from ...numeric_array import numeric_object_to_array
|
|
16
|
+
from ...value import HoconArray, HoconObject, HoconScalar, HoconValue
|
|
17
|
+
from .fold_self_ref import contains_subst_by_path, string_segments_to_key
|
|
18
|
+
from .types import (
|
|
19
|
+
ConcatPlaceholder,
|
|
20
|
+
ResObj,
|
|
21
|
+
ResolveOptions,
|
|
22
|
+
ResolverValue,
|
|
23
|
+
SubstPlaceholder,
|
|
24
|
+
is_concat,
|
|
25
|
+
is_res_obj,
|
|
26
|
+
is_subst,
|
|
27
|
+
separator_values,
|
|
28
|
+
)
|
|
29
|
+
from .utils import (
|
|
30
|
+
deep_merge_hocon_values,
|
|
31
|
+
lookup_path,
|
|
32
|
+
lookup_res_obj,
|
|
33
|
+
segments_to_key,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = ["SubstitutionResolver"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _null_scalar() -> HoconScalar:
|
|
40
|
+
return HoconScalar("null", "null")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SubstitutionResolver:
|
|
44
|
+
def __init__(self, root: ResObj, opts: ResolveOptions) -> None:
|
|
45
|
+
self.root = root
|
|
46
|
+
self.opts = opts
|
|
47
|
+
self.resolving: set[str] = set()
|
|
48
|
+
self.cache: dict[str, HoconValue] = {}
|
|
49
|
+
# ConcatPlaceholder nodes currently mid-iteration (identity membership).
|
|
50
|
+
self.resolving_concats: weakref.WeakSet[ConcatPlaceholder] = weakref.WeakSet()
|
|
51
|
+
# Full dotted path of the field currently being assigned (owner gate).
|
|
52
|
+
self.resolving_field_path: list[str] = []
|
|
53
|
+
|
|
54
|
+
def _origin_prefix(self) -> str:
|
|
55
|
+
return f"{self.opts.origin_description}: " if self.opts.origin_description else ""
|
|
56
|
+
|
|
57
|
+
def resolve(self) -> HoconValue:
|
|
58
|
+
return self._resolve_res_obj(self.root)
|
|
59
|
+
|
|
60
|
+
def _resolve_res_obj(self, obj: ResObj) -> HoconValue:
|
|
61
|
+
result: dict[str, HoconValue] = {}
|
|
62
|
+
for key, val in obj.fields.items():
|
|
63
|
+
self.resolving_field_path.append(key)
|
|
64
|
+
full_cache_key = string_segments_to_key(self.resolving_field_path)
|
|
65
|
+
self.cache.pop(full_cache_key, None)
|
|
66
|
+
try:
|
|
67
|
+
resolved = self._resolve_val(val, obj)
|
|
68
|
+
finally:
|
|
69
|
+
self.resolving_field_path.pop()
|
|
70
|
+
if resolved is not None:
|
|
71
|
+
# Delayed merge: if both current and prior resolve to objects, deep merge.
|
|
72
|
+
if isinstance(resolved, HoconObject):
|
|
73
|
+
prior = obj.prior_values.get(key)
|
|
74
|
+
if prior is not None:
|
|
75
|
+
prior_resolved = self._resolve_val(prior, obj)
|
|
76
|
+
if isinstance(prior_resolved, HoconObject):
|
|
77
|
+
final_value = deep_merge_hocon_values(prior_resolved, resolved)
|
|
78
|
+
result[key] = final_value
|
|
79
|
+
self.cache[full_cache_key] = final_value
|
|
80
|
+
self._cache_descendants(full_cache_key, final_value)
|
|
81
|
+
continue
|
|
82
|
+
result[key] = resolved
|
|
83
|
+
self.cache[full_cache_key] = resolved
|
|
84
|
+
self._cache_descendants(full_cache_key, resolved)
|
|
85
|
+
else:
|
|
86
|
+
# Unresolved optional substitution: fall back to prior value.
|
|
87
|
+
prior = obj.prior_values.get(key)
|
|
88
|
+
if prior is not None:
|
|
89
|
+
prior_resolved = self._resolve_val(prior, obj)
|
|
90
|
+
if prior_resolved is not None:
|
|
91
|
+
result[key] = prior_resolved
|
|
92
|
+
self.cache[full_cache_key] = prior_resolved
|
|
93
|
+
self._cache_descendants(full_cache_key, prior_resolved)
|
|
94
|
+
return HoconObject(result)
|
|
95
|
+
|
|
96
|
+
def _cache_descendants(self, prefix: str, value: HoconValue) -> None:
|
|
97
|
+
if not isinstance(value, HoconObject):
|
|
98
|
+
return
|
|
99
|
+
for key, child in value.fields.items():
|
|
100
|
+
child_key = f"{prefix}.{string_segments_to_key([key])}"
|
|
101
|
+
self.cache[child_key] = child
|
|
102
|
+
self._cache_descendants(child_key, child)
|
|
103
|
+
|
|
104
|
+
def _resolve_val(self, v: ResolverValue, scope: ResObj) -> HoconValue | None:
|
|
105
|
+
if is_subst(v):
|
|
106
|
+
return self._resolve_subst(v, scope)
|
|
107
|
+
if is_concat(v):
|
|
108
|
+
# Mark mid-iteration so _resolve_subst can detect a true self-ref.
|
|
109
|
+
self.resolving_concats.add(v)
|
|
110
|
+
try:
|
|
111
|
+
return self._resolve_concat(v.nodes, scope, v.line, v.col)
|
|
112
|
+
finally:
|
|
113
|
+
self.resolving_concats.discard(v)
|
|
114
|
+
if is_res_obj(v):
|
|
115
|
+
return self._resolve_res_obj(v)
|
|
116
|
+
if isinstance(v, HoconArray):
|
|
117
|
+
return HoconArray(
|
|
118
|
+
[
|
|
119
|
+
(self._resolve_val(item, scope) or _null_scalar())
|
|
120
|
+
for item in v.items
|
|
121
|
+
]
|
|
122
|
+
)
|
|
123
|
+
# Exhausted every placeholder/container variant above; v is a resolved
|
|
124
|
+
# HoconValue (scalar or object). TypeGuards don't narrow the negative
|
|
125
|
+
# branch in mypy, so cast to the resolved contract.
|
|
126
|
+
return cast("HoconValue", v)
|
|
127
|
+
|
|
128
|
+
def _resolve_subst(self, s: SubstPlaceholder, scope: ResObj) -> HoconValue | None:
|
|
129
|
+
if s.known_absent:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
# Cache key includes listSuffix so ${X} and ${X[]} never collide.
|
|
133
|
+
key = f"{segments_to_key(s.segments)}[]" if s.list_suffix else segments_to_key(s.segments)
|
|
134
|
+
|
|
135
|
+
if key in self.cache:
|
|
136
|
+
return self.cache[key]
|
|
137
|
+
|
|
138
|
+
if key in self.resolving:
|
|
139
|
+
# Cycle detected: try prior value for self-referential substitutions.
|
|
140
|
+
prior = self._lookup_prior(s, scope)
|
|
141
|
+
if prior is not None:
|
|
142
|
+
saved = self.resolving
|
|
143
|
+
self.resolving = set(saved)
|
|
144
|
+
try:
|
|
145
|
+
return self._resolve_val(prior, scope)
|
|
146
|
+
finally:
|
|
147
|
+
self.resolving = saved
|
|
148
|
+
if s.optional:
|
|
149
|
+
return None
|
|
150
|
+
raise ResolveError(
|
|
151
|
+
self._origin_prefix() + f"circular substitution: {key}", key, s.line, s.col
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
self.resolving.add(key)
|
|
155
|
+
try:
|
|
156
|
+
found = lookup_path(self.root, s.segments)
|
|
157
|
+
if found is not None:
|
|
158
|
+
# S13a.13 / spec L841: self-ref detection (owner gate + path walk).
|
|
159
|
+
rfp = self.resolving_field_path
|
|
160
|
+
is_owner = len(rfp) >= len(s.segments) and all(
|
|
161
|
+
rfp[i] == s.segments[i].text for i in range(len(s.segments))
|
|
162
|
+
)
|
|
163
|
+
is_self_ref = (
|
|
164
|
+
is_concat(found) and found in self.resolving_concats
|
|
165
|
+
) or (is_owner and contains_subst_by_path(found, s.segments))
|
|
166
|
+
if is_self_ref:
|
|
167
|
+
prior = self._lookup_prior(s, scope)
|
|
168
|
+
if prior is not None:
|
|
169
|
+
result = self._resolve_val(prior, scope)
|
|
170
|
+
if result is not None:
|
|
171
|
+
self.cache[key] = result
|
|
172
|
+
return result
|
|
173
|
+
if s.optional:
|
|
174
|
+
return None
|
|
175
|
+
raise ResolveError(
|
|
176
|
+
f"{self._origin_prefix()}could not resolve substitution: ${{{key}}}",
|
|
177
|
+
key,
|
|
178
|
+
s.line,
|
|
179
|
+
s.col,
|
|
180
|
+
)
|
|
181
|
+
result = self._resolve_val(found, scope)
|
|
182
|
+
# Delayed merge in substitution context.
|
|
183
|
+
effective_len = len(s.segments) - s.prefix_len
|
|
184
|
+
if effective_len == 1 and isinstance(result, HoconObject):
|
|
185
|
+
leaf_seg = s.segments[-1].text if s.segments else ""
|
|
186
|
+
if s.prefix_len > 0:
|
|
187
|
+
parent_scope = lookup_res_obj(self.root, s.segments[:-1])
|
|
188
|
+
prior = parent_scope.prior_values.get(leaf_seg) if parent_scope else None
|
|
189
|
+
else:
|
|
190
|
+
prior = scope.prior_values.get(leaf_seg) or self.root.prior_values.get(
|
|
191
|
+
leaf_seg
|
|
192
|
+
)
|
|
193
|
+
if prior is not None:
|
|
194
|
+
prior_resolved = self._resolve_val(prior, scope)
|
|
195
|
+
if isinstance(prior_resolved, HoconObject):
|
|
196
|
+
result = deep_merge_hocon_values(prior_resolved, result)
|
|
197
|
+
if result is not None:
|
|
198
|
+
self.cache[key] = result
|
|
199
|
+
return result
|
|
200
|
+
|
|
201
|
+
# S14c.2 (xx.hocon#22 C4): original-path config fallback for
|
|
202
|
+
# relativized substitutions. Tried after the relativized lookup
|
|
203
|
+
# misses, before env-var fallback.
|
|
204
|
+
if s.prefix_len > 0 and len(s.segments) > s.prefix_len:
|
|
205
|
+
original_segments = s.segments[s.prefix_len :]
|
|
206
|
+
fallback_found = lookup_path(self.root, original_segments)
|
|
207
|
+
if fallback_found is not None:
|
|
208
|
+
result = self._resolve_val(fallback_found, scope)
|
|
209
|
+
if len(original_segments) == 1 and isinstance(result, HoconObject):
|
|
210
|
+
root_seg = original_segments[0].text if original_segments else ""
|
|
211
|
+
prior = self.root.prior_values.get(root_seg)
|
|
212
|
+
if prior is not None:
|
|
213
|
+
prior_resolved = self._resolve_val(prior, scope)
|
|
214
|
+
if isinstance(prior_resolved, HoconObject):
|
|
215
|
+
result = deep_merge_hocon_values(prior_resolved, result)
|
|
216
|
+
if result is not None:
|
|
217
|
+
self.cache[key] = result
|
|
218
|
+
return result
|
|
219
|
+
|
|
220
|
+
# S13c: env-var list expansion — gated on useSystemEnvironment.
|
|
221
|
+
if s.list_suffix and self.opts.use_system_environment is not False:
|
|
222
|
+
env_result = self._resolve_env_list(s)
|
|
223
|
+
if env_result is not None:
|
|
224
|
+
self.cache[key] = env_result
|
|
225
|
+
return env_result
|
|
226
|
+
if s.optional:
|
|
227
|
+
return None
|
|
228
|
+
env_base = ".".join(seg.text for seg in s.segments)
|
|
229
|
+
raise ResolveError(
|
|
230
|
+
f"{self._origin_prefix()}could not resolve substitution: ${{{key}}} "
|
|
231
|
+
f"(no environment variables {env_base}_0, {env_base}_1, … set)",
|
|
232
|
+
key,
|
|
233
|
+
s.line,
|
|
234
|
+
s.col,
|
|
235
|
+
)
|
|
236
|
+
elif s.list_suffix:
|
|
237
|
+
if s.optional:
|
|
238
|
+
return None
|
|
239
|
+
if self.opts.allow_unresolved:
|
|
240
|
+
return cast(HoconValue, s)
|
|
241
|
+
raise ResolveError(
|
|
242
|
+
f"{self._origin_prefix()}could not resolve substitution: ${{{key}}}",
|
|
243
|
+
key,
|
|
244
|
+
s.line,
|
|
245
|
+
s.col,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Env var fallback — raw dot-join (no quoting), Lightbend-aligned.
|
|
249
|
+
if self.opts.use_system_environment is not False:
|
|
250
|
+
env_key = ".".join(seg.text for seg in s.segments)
|
|
251
|
+
env_val = self.opts.env.get(env_key)
|
|
252
|
+
if env_val is None and s.prefix_len > 0:
|
|
253
|
+
env_val = self.opts.env.get(
|
|
254
|
+
".".join(seg.text for seg in s.segments[s.prefix_len :])
|
|
255
|
+
)
|
|
256
|
+
if env_val is not None:
|
|
257
|
+
result_scalar = HoconScalar(env_val, "string")
|
|
258
|
+
self.cache[key] = result_scalar
|
|
259
|
+
return result_scalar
|
|
260
|
+
|
|
261
|
+
if s.optional:
|
|
262
|
+
return None
|
|
263
|
+
if self.opts.allow_unresolved:
|
|
264
|
+
return cast(HoconValue, s)
|
|
265
|
+
raise ResolveError(
|
|
266
|
+
f"{self._origin_prefix()}could not resolve substitution: ${{{key}}}",
|
|
267
|
+
key,
|
|
268
|
+
s.line,
|
|
269
|
+
s.col,
|
|
270
|
+
)
|
|
271
|
+
finally:
|
|
272
|
+
self.resolving.discard(key)
|
|
273
|
+
|
|
274
|
+
def _lookup_prior(self, s: SubstPlaceholder, scope: ResObj) -> ResolverValue | None:
|
|
275
|
+
"""Prior lookup shared by the cycle-guard and self-ref short-circuit
|
|
276
|
+
branches (dotted-path-at-root uses ``len(segments) > 1``)."""
|
|
277
|
+
if len(s.segments) > 1:
|
|
278
|
+
leaf_seg = s.segments[-1].text
|
|
279
|
+
parent_scope = lookup_res_obj(self.root, s.segments[:-1])
|
|
280
|
+
return parent_scope.prior_values.get(leaf_seg) if parent_scope else None
|
|
281
|
+
root_seg = s.segments[0].text if s.segments else ""
|
|
282
|
+
return scope.prior_values.get(root_seg) or self.root.prior_values.get(root_seg)
|
|
283
|
+
|
|
284
|
+
def _resolve_env_list(self, s: SubstPlaceholder) -> HoconValue | None:
|
|
285
|
+
"""S13c: scan environment for NAME_0, NAME_1, … → Array<Scalar(String)>.
|
|
286
|
+
First candidate base whose _0 is present wins entirely."""
|
|
287
|
+
for base in self._candidate_bases(s):
|
|
288
|
+
key0 = base + "_0"
|
|
289
|
+
if key0 not in self.opts.env:
|
|
290
|
+
continue
|
|
291
|
+
items: list[HoconValue] = []
|
|
292
|
+
i = 0
|
|
293
|
+
while True:
|
|
294
|
+
k = f"{base}_{i}"
|
|
295
|
+
if k not in self.opts.env:
|
|
296
|
+
break
|
|
297
|
+
items.append(HoconScalar(self.opts.env[k], "string"))
|
|
298
|
+
i += 1
|
|
299
|
+
return HoconArray(items)
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
def _candidate_bases(self, s: SubstPlaceholder) -> list[str]:
|
|
303
|
+
full = ".".join(seg.text for seg in s.segments)
|
|
304
|
+
if s.prefix_len == 0:
|
|
305
|
+
return [full]
|
|
306
|
+
bare = ".".join(seg.text for seg in s.segments[s.prefix_len :])
|
|
307
|
+
return [full, bare]
|
|
308
|
+
|
|
309
|
+
def _resolve_concat(
|
|
310
|
+
self, nodes: list[ResolverValue], scope: ResObj, line: int = 0, col: int = 0
|
|
311
|
+
) -> HoconValue | None:
|
|
312
|
+
raw_resolved = [self._resolve_val(n, scope) for n in nodes]
|
|
313
|
+
|
|
314
|
+
if self.opts.allow_unresolved:
|
|
315
|
+
for v in raw_resolved:
|
|
316
|
+
if v is not None and is_subst(cast(ResolverValue, v)):
|
|
317
|
+
return v
|
|
318
|
+
|
|
319
|
+
resolved = [v for v in raw_resolved if v is not None]
|
|
320
|
+
|
|
321
|
+
if len(resolved) == 0:
|
|
322
|
+
return None
|
|
323
|
+
if len(resolved) == 1:
|
|
324
|
+
return resolved[0]
|
|
325
|
+
|
|
326
|
+
# Filter parser-inserted separator whitespace (tracked via WeakSet), NOT
|
|
327
|
+
# user-authored '' / ' ' which should prevent object merging.
|
|
328
|
+
non_sep = [
|
|
329
|
+
v
|
|
330
|
+
for v in resolved
|
|
331
|
+
if not (isinstance(v, HoconScalar) and v in separator_values)
|
|
332
|
+
]
|
|
333
|
+
|
|
334
|
+
if len(non_sep) == 0:
|
|
335
|
+
s = "".join(
|
|
336
|
+
v.raw if isinstance(v, HoconScalar) else _json_dumps(v) for v in resolved
|
|
337
|
+
)
|
|
338
|
+
return HoconScalar(s, "string")
|
|
339
|
+
|
|
340
|
+
# True left-to-right pairwise fold (S10.4 / S10.13 / S10.19).
|
|
341
|
+
def join_pair(left: HoconValue, right: HoconValue) -> HoconValue:
|
|
342
|
+
if isinstance(left, HoconObject) and isinstance(right, HoconObject):
|
|
343
|
+
return deep_merge_hocon_values(left, right)
|
|
344
|
+
if isinstance(left, HoconArray) and isinstance(right, HoconObject):
|
|
345
|
+
converted = numeric_object_to_array(right)
|
|
346
|
+
if converted is not None:
|
|
347
|
+
return HoconArray([*left.items, *converted])
|
|
348
|
+
raise ResolveError(
|
|
349
|
+
"cannot concatenate array with object: value concatenation requires "
|
|
350
|
+
"same-kind operands (S10.4)",
|
|
351
|
+
"",
|
|
352
|
+
line,
|
|
353
|
+
col,
|
|
354
|
+
)
|
|
355
|
+
if isinstance(left, HoconObject) and isinstance(right, HoconArray):
|
|
356
|
+
converted = numeric_object_to_array(left)
|
|
357
|
+
if converted is not None:
|
|
358
|
+
return HoconArray([*converted, *right.items])
|
|
359
|
+
raise ResolveError(
|
|
360
|
+
"cannot concatenate object with array: value concatenation requires "
|
|
361
|
+
"same-kind operands (S10.4)",
|
|
362
|
+
"",
|
|
363
|
+
line,
|
|
364
|
+
col,
|
|
365
|
+
)
|
|
366
|
+
if isinstance(left, HoconArray) and isinstance(right, HoconArray):
|
|
367
|
+
return HoconArray([*left.items, *right.items])
|
|
368
|
+
if isinstance(left, HoconArray) or isinstance(right, HoconArray):
|
|
369
|
+
raise ResolveError(
|
|
370
|
+
f"cannot concatenate {left.kind} with {right.kind}: arrays and objects "
|
|
371
|
+
f"may not appear in string value concatenation (S10.13)",
|
|
372
|
+
"",
|
|
373
|
+
line,
|
|
374
|
+
col,
|
|
375
|
+
)
|
|
376
|
+
if isinstance(left, HoconObject) or isinstance(right, HoconObject):
|
|
377
|
+
raise ResolveError(
|
|
378
|
+
f"cannot concatenate {left.kind} with {right.kind}: arrays and objects "
|
|
379
|
+
f"may not appear in string value concatenation (S10.13)",
|
|
380
|
+
"",
|
|
381
|
+
line,
|
|
382
|
+
col,
|
|
383
|
+
)
|
|
384
|
+
# Scalar + Scalar — string concat (S10).
|
|
385
|
+
return HoconScalar(left.raw + right.raw, "string")
|
|
386
|
+
|
|
387
|
+
folded = non_sep[0]
|
|
388
|
+
for nxt in non_sep[1:]:
|
|
389
|
+
folded = join_pair(folded, nxt)
|
|
390
|
+
|
|
391
|
+
# If scalar and there were separator tokens, re-run as plain string concat
|
|
392
|
+
# over ALL resolved values so whitespace is preserved.
|
|
393
|
+
if isinstance(folded, HoconScalar):
|
|
394
|
+
s = "".join(
|
|
395
|
+
v.raw if isinstance(v, HoconScalar) else _json_dumps(v) for v in resolved
|
|
396
|
+
)
|
|
397
|
+
return HoconScalar(s, "string")
|
|
398
|
+
|
|
399
|
+
return folded
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _json_dumps(v: HoconValue) -> str:
|
|
403
|
+
"""Defensive-only stringification for a non-scalar value inside string
|
|
404
|
+
concat. Both call sites already special-case scalars, and ``join_pair``
|
|
405
|
+
rejects arrays/objects in string concatenation before this fold path is
|
|
406
|
+
taken (S10.13), so this is unreachable for valid input."""
|
|
407
|
+
return ""
|