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.
@@ -0,0 +1,162 @@
1
+ """Resolver-shared types: placeholder nodes, ``ResObj``, ``ResolveOptions``,
2
+ and the unresolved-tree merge helpers.
3
+
4
+ Mirrors ts.hocon ``src/internal/resolver/types.ts``. ``ResolverValue`` is the
5
+ union carried through phase 1 (build) before phase 2 (resolve) replaces the
6
+ placeholders.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import weakref
12
+ from collections.abc import Callable
13
+ from dataclasses import dataclass, field
14
+ from typing import TypeGuard
15
+
16
+ from ...value import HoconScalar, HoconValue
17
+ from ..lexer.token import Segment
18
+
19
+ __all__ = [
20
+ "ConcatPlaceholder",
21
+ "PackageResolver",
22
+ "ResObj",
23
+ "ResolveOptions",
24
+ "ResolverValue",
25
+ "SubstPlaceholder",
26
+ "get_prior",
27
+ "is_concat",
28
+ "is_res_obj",
29
+ "is_subst",
30
+ "make_res_obj",
31
+ "merge_unresolved",
32
+ "separator_values",
33
+ "set_prior",
34
+ ]
35
+
36
+
37
+ class SubstPlaceholder:
38
+ def __init__(
39
+ self,
40
+ segments: list[Segment],
41
+ optional: bool,
42
+ known_absent: bool,
43
+ list_suffix: bool,
44
+ line: int,
45
+ col: int,
46
+ prefix_len: int,
47
+ ) -> None:
48
+ self.segments = segments
49
+ self.optional = optional
50
+ # Internal sentinel for a folded optional self-ref with no prior value.
51
+ self.known_absent = known_absent
52
+ self.list_suffix = list_suffix
53
+ self.line = line
54
+ self.col = col
55
+ # 0 for normal, >0 for relativized (number of prefix segments).
56
+ self.prefix_len = prefix_len
57
+
58
+
59
+ class ConcatPlaceholder:
60
+ def __init__(self, nodes: list[ResolverValue], line: int, col: int) -> None:
61
+ self.nodes = nodes
62
+ self.line = line
63
+ self.col = col
64
+
65
+
66
+ class ResObj:
67
+ def __init__(self) -> None:
68
+ self.fields: dict[str, ResolverValue] = {}
69
+ self.prior_values: dict[str, ResolverValue] = {}
70
+ # Keys whose net value was established by an explicit non-self-referential
71
+ # assignment (a reset rather than a += / self-ref append). See go.hocon#134.
72
+ self.reset_keys: set[str] = set()
73
+
74
+
75
+ ResolverValue = HoconValue | SubstPlaceholder | ConcatPlaceholder | ResObj
76
+
77
+ PackageResolver = Callable[[str, str, str | None, str | None], str]
78
+
79
+
80
+ def _no_files(_path: str) -> str:
81
+ raise RuntimeError("no file reader configured")
82
+
83
+
84
+ @dataclass
85
+ class ResolveOptions:
86
+ env: dict[str, str] = field(default_factory=dict)
87
+ base_dir: str | None = None
88
+ read_file_sync: Callable[[str], str] = _no_files
89
+ include_stack: list[str] = field(default_factory=list)
90
+ # Override the starting dir(s) for the default package resolver.
91
+ resolve_from: str | list[str] | None = None
92
+ package_resolver: PackageResolver | None = None
93
+ # When False, env var lookups in SubstitutionResolver are suppressed.
94
+ use_system_environment: bool | None = None
95
+ # When True, leaves unresolved non-optional placeholders in place.
96
+ allow_unresolved: bool | None = None
97
+ # Human-readable origin for error messages.
98
+ origin_description: str | None = None
99
+
100
+
101
+ # Track parser-inserted separator whitespace values without leaking a flag into
102
+ # the public HoconValue type. WeakSet keys on identity, so values GC normally.
103
+ separator_values: weakref.WeakSet[HoconScalar] = weakref.WeakSet()
104
+
105
+
106
+ def is_subst(v: ResolverValue) -> TypeGuard[SubstPlaceholder]:
107
+ return isinstance(v, SubstPlaceholder)
108
+
109
+
110
+ def is_concat(v: ResolverValue) -> TypeGuard[ConcatPlaceholder]:
111
+ return isinstance(v, ConcatPlaceholder)
112
+
113
+
114
+ def is_res_obj(v: ResolverValue) -> TypeGuard[ResObj]:
115
+ return isinstance(v, ResObj)
116
+
117
+
118
+ def make_res_obj() -> ResObj:
119
+ return ResObj()
120
+
121
+
122
+ def set_prior(o: ResObj, key: str, v: ResolverValue) -> None:
123
+ o.prior_values[key] = v
124
+
125
+
126
+ def get_prior(o: ResObj, key: str) -> ResolverValue | None:
127
+ return o.prior_values.get(key)
128
+
129
+
130
+ def merge_unresolved(receiver: ResObj, fallback: ResObj) -> ResObj:
131
+ """E12 withFallback merge of two unresolved trees. Receiver's keys win; on a
132
+ non-object collision the fallback's value is recorded as a prior on the
133
+ result for cross-layer self-reference lookback in phase 2."""
134
+ # Function-local import breaks the types ↔ fold_self_ref cycle (both only
135
+ # touch the other's bindings inside function bodies, as ts.hocon notes).
136
+ from .fold_self_ref import fold_or_skip_prior
137
+
138
+ result = make_res_obj()
139
+ for k, v in fallback.fields.items():
140
+ result.fields[k] = v
141
+ for k, v in fallback.prior_values.items():
142
+ result.prior_values[k] = v
143
+ for k, rv in receiver.fields.items():
144
+ existing = result.fields.get(k)
145
+ if existing is not None:
146
+ rec_prior = receiver.prior_values.get(k)
147
+ if rec_prior is not None and not is_res_obj(rec_prior):
148
+ result.fields[k] = rv
149
+ continue
150
+ if is_res_obj(rv) and is_res_obj(existing):
151
+ result.fields[k] = merge_unresolved(rv, existing)
152
+ continue
153
+ # Non-object collision: receiver wins; capture the fallback's value
154
+ # (existing) as prior, folded self-ref-free first (go.hocon#134).
155
+ folded_prior = fold_or_skip_prior(existing, k, None)
156
+ if folded_prior is not None:
157
+ result.prior_values[k] = folded_prior
158
+ result.fields[k] = rv
159
+ for k, v in receiver.prior_values.items():
160
+ if k in receiver.fields:
161
+ result.prior_values[k] = v
162
+ return result
@@ -0,0 +1,144 @@
1
+ """Resolver helpers. Mirrors ts.hocon ``src/internal/resolver/utils.ts``.
2
+
3
+ Path lookup, deep-merge (both ``HoconValue`` and ``ResObj`` variants), and the
4
+ cross-include splice logic (go.hocon#134, S13b.2).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+
11
+ from ...value import HoconObject, HoconValue
12
+ from ..lexer.token import Segment
13
+ from .fold_self_ref import (
14
+ fold_known_absent_self_ref,
15
+ fold_or_skip_prior,
16
+ string_segments_to_key,
17
+ )
18
+ from .types import ResObj, ResolverValue, is_res_obj, make_res_obj
19
+
20
+ __all__ = [
21
+ "deep_merge_hocon_values",
22
+ "deep_merge_res_obj_into",
23
+ "hocon_value_to_res_obj",
24
+ "is_file_not_found_error",
25
+ "lookup_path",
26
+ "lookup_res_obj",
27
+ "segments_to_key",
28
+ ]
29
+
30
+ _NON_BARE_KEY = re.compile(r"[^a-zA-Z0-9\-_]")
31
+
32
+
33
+ def segments_to_key(segments: list[Segment]) -> str:
34
+ parts = []
35
+ for s in segments:
36
+ t = s.text
37
+ if t == "" or _NON_BARE_KEY.search(t):
38
+ parts.append('"' + t.replace("\\", "\\\\").replace('"', '\\"') + '"')
39
+ else:
40
+ parts.append(t)
41
+ return ".".join(parts)
42
+
43
+
44
+ def lookup_path(root: ResObj, segments: list[Segment]) -> ResolverValue | None:
45
+ if len(segments) == 0:
46
+ return None
47
+ head, tail = segments[0], segments[1:]
48
+ key = head.text
49
+ val = root.fields.get(key)
50
+ if val is None:
51
+ return None
52
+ if len(tail) == 0:
53
+ return val
54
+ if is_res_obj(val):
55
+ return lookup_path(val, tail)
56
+ return None
57
+
58
+
59
+ def lookup_res_obj(root: ResObj, segments: list[Segment]) -> ResObj | None:
60
+ cur: ResObj = root
61
+ for seg in segments:
62
+ val = cur.fields.get(seg.text)
63
+ if val is None or not is_res_obj(val):
64
+ return None
65
+ cur = val
66
+ return cur
67
+
68
+
69
+ def deep_merge_hocon_values(base: HoconObject, overlay: HoconObject) -> HoconObject:
70
+ merged: dict[str, HoconValue] = dict(base.fields)
71
+ for k, v in overlay.fields.items():
72
+ existing = merged.get(k)
73
+ if isinstance(existing, HoconObject) and isinstance(v, HoconObject):
74
+ merged[k] = deep_merge_hocon_values(existing, v)
75
+ else:
76
+ merged[k] = v
77
+ return HoconObject(merged)
78
+
79
+
80
+ def deep_merge_res_obj_into(
81
+ dst: ResObj, src: ResObj, path_prefix: list[str] | None = None
82
+ ) -> None:
83
+ if path_prefix is None:
84
+ path_prefix = []
85
+ for k, src_val in src.fields.items():
86
+ child_prefix = [*path_prefix, k]
87
+ full_key = string_segments_to_key(child_prefix)
88
+ dst_val = dst.fields.get(k)
89
+ if dst_val is not None and is_res_obj(dst_val) and is_res_obj(src_val):
90
+ # #120: save dst's pre-merge value as the prior at the OUTER level
91
+ # even when both sides are objects and we recurse.
92
+ prior_existing = dst.prior_values.get(k)
93
+ prior = fold_or_skip_prior(dst_val, full_key, prior_existing)
94
+ if prior is not None:
95
+ dst.prior_values[k] = prior
96
+ deep_merge_res_obj_into(dst_val, src_val, child_prefix)
97
+ else:
98
+ # Non-object collision: distinguish how src's value composes with
99
+ # dst's pre-merge value (go.hocon#134, S13b.2).
100
+ if dst_val is not None:
101
+ if k in src.reset_keys:
102
+ dst.prior_values.pop(k, None)
103
+ else:
104
+ dst_folded = fold_or_skip_prior(dst_val, full_key, dst.prior_values.get(k))
105
+ if dst_folded is not None:
106
+ src_prior = src.prior_values.get(k)
107
+ if src_prior is not None:
108
+ dst.prior_values[k] = fold_known_absent_self_ref(
109
+ src_prior, full_key, dst_folded
110
+ )
111
+ else:
112
+ dst.prior_values[k] = dst_folded
113
+ dst.fields[k] = src_val
114
+ # Carry over priors src has that dst lacks.
115
+ for k, src_prior in src.prior_values.items():
116
+ if k not in dst.prior_values:
117
+ dst.prior_values[k] = src_prior
118
+ # Propagate reset origin (union).
119
+ for k in src.reset_keys:
120
+ dst.reset_keys.add(k)
121
+
122
+
123
+ def hocon_value_to_res_obj(hv: HoconValue) -> ResObj:
124
+ obj = make_res_obj()
125
+ if not isinstance(hv, HoconObject):
126
+ return obj
127
+ for key, val in hv.fields.items():
128
+ if isinstance(val, HoconObject):
129
+ obj.fields[key] = hocon_value_to_res_obj(val)
130
+ else:
131
+ obj.fields[key] = val
132
+ return obj
133
+
134
+
135
+ def is_file_not_found_error(e: BaseException) -> bool:
136
+ if isinstance(e, FileNotFoundError):
137
+ return True
138
+ if isinstance(e, OSError):
139
+ import errno
140
+
141
+ if e.errno == errno.ENOENT:
142
+ return True
143
+ msg = str(e).lower()
144
+ return "not found" in msg or "no such file" in msg or "enoent" in msg
hocon/coerce.py ADDED
@@ -0,0 +1,257 @@
1
+ """Typed coercions for Config accessors: numbers, booleans, durations, byte sizes.
2
+
3
+ Mirrors ts.hocon ``src/coerce.ts``. Duration unit names are case-sensitive
4
+ (S19.8, HOCON.md L1304). Single-letter byte abbreviations are powers of two
5
+ (S21.4, HOCON.md L1385).
6
+
7
+ Standalone module: it must not import :mod:`hocon.value` (value imports this).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import math
13
+ import re
14
+
15
+ __all__ = [
16
+ "DECIMAL_NUMBER_RE",
17
+ "ByteUnit",
18
+ "DurationUnit",
19
+ "coerce_boolean",
20
+ "coerce_number",
21
+ "parse_bytes",
22
+ "parse_duration",
23
+ "parse_period",
24
+ ]
25
+
26
+ DECIMAL_NUMBER_RE = re.compile(r"-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?")
27
+
28
+ # HOCON_WS = Java Character.isWhitespace set
29
+ # ∪ { 0x00A0 NBSP, 0x2007 FIGURE SPACE, 0x202F NARROW NO-BREAK SPACE }
30
+ # ∪ { 0xFEFF BOM }
31
+ # Do NOT use str.strip() — it strips NEL (U+0085) and other Unicode space
32
+ # separators that HOCON does not classify as whitespace. Mirrors
33
+ # ``isHoconWhitespace`` in lexer.py byte-for-byte.
34
+
35
+
36
+ def _is_hocon_ws(cp: int) -> bool:
37
+ if cp in (0x09, 0x0A, 0x0B, 0x0C, 0x0D):
38
+ return True
39
+ if 0x1C <= cp <= 0x1F:
40
+ return True
41
+ if cp in (0x20, 0xA0, 0xFEFF):
42
+ return True
43
+ if cp == 0x1680:
44
+ return True
45
+ if 0x2000 <= cp <= 0x200A:
46
+ return True
47
+ if cp in (0x2028, 0x2029, 0x202F, 0x205F):
48
+ return True
49
+ if cp == 0x3000:
50
+ return True
51
+ return False
52
+
53
+
54
+ def _trim_hocon_ws(s: str) -> str:
55
+ start = 0
56
+ end = len(s)
57
+ while start < end and _is_hocon_ws(ord(s[start])):
58
+ start += 1
59
+ while end > start and _is_hocon_ws(ord(s[end - 1])):
60
+ end -= 1
61
+ return s[start:end]
62
+
63
+
64
+ _TRUTHY = frozenset(("true", "yes", "on"))
65
+ _FALSY = frozenset(("false", "no", "off"))
66
+
67
+
68
+ def coerce_boolean(value: str) -> bool | None:
69
+ lower = value.lower()
70
+ if lower in _TRUTHY:
71
+ return True
72
+ if lower in _FALSY:
73
+ return False
74
+ return None
75
+
76
+
77
+ def coerce_number(value: str) -> float | int | None:
78
+ if not DECIMAL_NUMBER_RE.fullmatch(value):
79
+ return None
80
+ if any(c in value for c in ".eE"):
81
+ return float(value)
82
+ return int(value)
83
+
84
+
85
+ DurationUnit = str # 'ns' | 'us' | 'ms' | 's' | 'm' | 'h' | 'd'
86
+
87
+ _DURATION_UNITS: dict[str, float] = {
88
+ "ns": 1e-6, "nanosecond": 1e-6, "nanoseconds": 1e-6,
89
+ "us": 1e-3, "microsecond": 1e-3, "microseconds": 1e-3,
90
+ "ms": 1, "millisecond": 1, "milliseconds": 1,
91
+ "s": 1_000, "second": 1_000, "seconds": 1_000,
92
+ "m": 60_000, "minute": 60_000, "minutes": 60_000,
93
+ "h": 3_600_000, "hour": 3_600_000, "hours": 3_600_000,
94
+ "d": 86_400_000, "day": 86_400_000, "days": 86_400_000,
95
+ }
96
+
97
+ _OUTPUT_DURATION_UNITS: dict[str, float] = {
98
+ "ns": 1e-6, "us": 1e-3, "ms": 1, "s": 1_000, "m": 60_000, "h": 3_600_000, "d": 86_400_000,
99
+ }
100
+
101
+
102
+ def _leading_numeric_prefix_len(trimmed: str) -> int:
103
+ i = 0
104
+ while i < len(trimmed):
105
+ ch = trimmed[i]
106
+ if i == 0 and (ch == "-" or ch == "+"):
107
+ i += 1
108
+ continue
109
+ if ch != "." and not ("0" <= ch <= "9"):
110
+ break
111
+ i += 1
112
+ return i
113
+
114
+
115
+ def parse_duration(value: str, output_unit: DurationUnit | None = None) -> float:
116
+ unit_out = output_unit if output_unit is not None else "ms"
117
+ trimmed = _trim_hocon_ws(value)
118
+ i = _leading_numeric_prefix_len(trimmed)
119
+ if i == 0:
120
+ return math.nan
121
+ try:
122
+ num = float(trimmed[:i])
123
+ except ValueError:
124
+ return math.nan
125
+ if math.isnan(num):
126
+ return math.nan
127
+ # S19.8 — unit match is case-sensitive per HOCON.md L1304 (lowercase only).
128
+ unit = _trim_hocon_ws(trimmed[i:])
129
+ divisor = _OUTPUT_DURATION_UNITS.get(unit_out)
130
+ if divisor is None:
131
+ return math.nan
132
+ # S18.1 + S18.4: bare number (no unit) → treat as default unit (ms).
133
+ if unit == "":
134
+ return num / divisor
135
+ mult = _DURATION_UNITS.get(unit)
136
+ if mult is None:
137
+ return math.nan
138
+ ms = num * mult
139
+ return ms / divisor
140
+
141
+
142
+ # S20 — Period format. Values are bounded to i32 like Lightbend
143
+ # (`Integer.parseInt`) and rs.hocon's `Period { i32, i32, i32 }`, so a period
144
+ # that would overflow there is invalid here too, not silently widened.
145
+ _I32_MIN = -(2**31)
146
+ _I32_MAX = 2**31 - 1
147
+
148
+
149
+ def parse_period(value: str) -> tuple[int, int, int] | None:
150
+ """Parse a HOCON period string into a ``(years, months, days)`` tuple.
151
+
152
+ Accepts ``[ws] integer [ws] [unit] [ws]``; a bare number is taken as days
153
+ (S20.1, HOCON.md L1321). Unit names are case-sensitive lowercase (the
154
+ S19.8 duration rule applies to periods too, HOCON.md L1304). Period is
155
+ integer-only per Lightbend ``Integer.parseInt`` — fractional strings like
156
+ ``"7.5"`` return None (up03), unlike duration/bytes which accept them.
157
+ """
158
+ trimmed = _trim_hocon_ws(value)
159
+ if not trimmed:
160
+ return None
161
+ i = 0
162
+ while i < len(trimmed) and (("0" <= trimmed[i] <= "9") or trimmed[i] in "+-"):
163
+ i += 1
164
+ num_str = trimmed[:i]
165
+ unit = _trim_hocon_ws(trimmed[i:])
166
+ if not num_str:
167
+ return None
168
+ try:
169
+ n = int(num_str)
170
+ except ValueError:
171
+ return None
172
+ if not _I32_MIN <= n <= _I32_MAX:
173
+ return None
174
+ if unit == "" or unit in ("d", "day", "days"):
175
+ return (0, 0, n)
176
+ if unit in ("w", "week", "weeks"):
177
+ days = n * 7
178
+ if not _I32_MIN <= days <= _I32_MAX:
179
+ return None
180
+ return (0, 0, days)
181
+ if unit in ("m", "mo", "month", "months"):
182
+ return (0, n, 0)
183
+ if unit in ("y", "year", "years"):
184
+ return (n, 0, 0)
185
+ return None
186
+
187
+
188
+ ByteUnit = str # 'B' | 'KB' | 'KiB' | 'MB' | 'MiB' | 'GB' | 'GiB' | 'TB' | 'TiB'
189
+
190
+ _BYTE_UNITS: dict[str, float] = {
191
+ "B": 1, "byte": 1, "bytes": 1,
192
+ "KB": 1_000, "kilobyte": 1_000, "kilobytes": 1_000,
193
+ "KiB": 1_024, "kibibyte": 1_024, "kibibytes": 1_024,
194
+ "MB": 1_000_000, "megabyte": 1_000_000, "megabytes": 1_000_000,
195
+ "MiB": 1_048_576, "mebibyte": 1_048_576, "mebibytes": 1_048_576,
196
+ "GB": 1_000_000_000, "gigabyte": 1_000_000_000, "gigabytes": 1_000_000_000,
197
+ "GiB": 1_073_741_824, "gibibyte": 1_073_741_824, "gibibytes": 1_073_741_824,
198
+ "TB": 1_000_000_000_000, "terabyte": 1_000_000_000_000, "terabytes": 1_000_000_000_000,
199
+ "TiB": 1_099_511_627_776, "tebibyte": 1_099_511_627_776, "tebibytes": 1_099_511_627_776,
200
+ # S21.4 — single-letter abbreviations → powers of two (java -Xmx convention).
201
+ "K": 1_024, "k": 1_024,
202
+ "M": 1_024 ** 2, "m": 1_024 ** 2,
203
+ "G": 1_024 ** 3, "g": 1_024 ** 3,
204
+ "T": 1_024 ** 4, "t": 1_024 ** 4,
205
+ "P": 1_024 ** 5, "p": 1_024 ** 5,
206
+ "E": 1_024 ** 6, "e": 1_024 ** 6,
207
+ # lowercase short-form aliases
208
+ "b": 1,
209
+ "kb": 1_000, "kib": 1_024,
210
+ "mb": 1_000_000, "mib": 1_048_576,
211
+ "gb": 1_000_000_000, "gib": 1_073_741_824,
212
+ "tb": 1_000_000_000_000, "tib": 1_099_511_627_776,
213
+ }
214
+
215
+ _OUTPUT_BYTE_UNITS: dict[str, float] = {
216
+ "B": 1, "KB": 1_000, "KiB": 1_024, "MB": 1_000_000, "MiB": 1_048_576,
217
+ "GB": 1_000_000_000, "GiB": 1_073_741_824, "TB": 1_000_000_000_000, "TiB": 1_099_511_627_776,
218
+ }
219
+
220
+ # JS Number.MAX_SAFE_INTEGER = 2^53 - 1. Kept for parity with the ts.hocon
221
+ # overflow guard even though Python ints are unbounded.
222
+ _MAX_SAFE_INTEGER = 2 ** 53 - 1
223
+
224
+
225
+ def parse_bytes(value: str, output_unit: ByteUnit | None = None) -> float:
226
+ unit_out = output_unit if output_unit is not None else "B"
227
+ trimmed = _trim_hocon_ws(value)
228
+ i = _leading_numeric_prefix_len(trimmed)
229
+ if i == 0:
230
+ return math.nan
231
+ try:
232
+ num = float(trimmed[:i])
233
+ except ValueError:
234
+ return math.nan
235
+ if math.isnan(num):
236
+ return math.nan
237
+ unit = _trim_hocon_ws(trimmed[i:])
238
+ divisor = _OUTPUT_BYTE_UNITS.get(unit_out)
239
+ if divisor is None:
240
+ return math.nan
241
+ # S18.1 + S18.4: bare number (no unit) → truncate toward zero (Lightbend
242
+ # BigDecimal.toBigInteger) and treat as default unit (bytes).
243
+ if unit == "":
244
+ num_bytes = math.trunc(num)
245
+ if abs(num_bytes) > _MAX_SAFE_INTEGER:
246
+ raise OverflowError("byte size overflows representable range (max 2^53-1 bytes)")
247
+ return num_bytes / divisor
248
+ mult = _BYTE_UNITS.get(unit)
249
+ if mult is None:
250
+ mult = _BYTE_UNITS.get(unit.lower())
251
+ if mult is None:
252
+ return math.nan
253
+ num_bytes_f = num * mult
254
+ if abs(num_bytes_f) > _MAX_SAFE_INTEGER:
255
+ raise OverflowError("byte size overflows representable range (max 2^53-1 bytes)")
256
+ result = num_bytes_f / divisor
257
+ return round(result) if unit_out == "B" else result