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,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
|