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/parse.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Public parse entry points.
|
|
2
|
+
|
|
3
|
+
Mirrors ts.hocon ``src/parse.ts`` (``parse`` / ``parseFile`` / ``parseString``
|
|
4
|
+
and the deferred ``resolveSubstitutions=False`` path). Python has no sync/async
|
|
5
|
+
split at this layer, so the ``*Async`` variants have no counterpart. Options are
|
|
6
|
+
keyword arguments rather than a TS options object.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
|
|
14
|
+
from ._internal.lexer.lexer import tokenize
|
|
15
|
+
from ._internal.parser.empty_check import assert_non_empty_document
|
|
16
|
+
from ._internal.parser.parser import parse_tokens
|
|
17
|
+
from ._internal.resolver.resolver import build_tree, contains_placeholders, resolve
|
|
18
|
+
from ._internal.resolver.types import PackageResolver, ResolveOptions
|
|
19
|
+
from .config import Config
|
|
20
|
+
from .value import HoconObject
|
|
21
|
+
|
|
22
|
+
__all__ = ["parse", "parse_file", "parse_string"]
|
|
23
|
+
|
|
24
|
+
_ReadFile = Callable[[str], str]
|
|
25
|
+
_ResolveFrom = str | list[str] | None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _default_read_file_sync(path: str) -> str:
|
|
29
|
+
with open(path, encoding="utf-8") as f:
|
|
30
|
+
return f.read()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_env(env: dict[str, str] | None) -> dict[str, str]:
|
|
34
|
+
return env if env is not None else dict(os.environ)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _build_resolve_context(
|
|
38
|
+
text: str,
|
|
39
|
+
base_dir: str | None,
|
|
40
|
+
env: dict[str, str] | None,
|
|
41
|
+
read_file: _ReadFile | None,
|
|
42
|
+
origin_description: str | None,
|
|
43
|
+
resolve_from: _ResolveFrom,
|
|
44
|
+
package_resolver: PackageResolver | None,
|
|
45
|
+
) -> tuple[object, ResolveOptions]:
|
|
46
|
+
tokens = tokenize(text)
|
|
47
|
+
# S3.1 — HOCON.md L130: empty / whitespace-only / comment-only files are invalid.
|
|
48
|
+
assert_non_empty_document(tokens, "input")
|
|
49
|
+
ast = parse_tokens(tokens)
|
|
50
|
+
opts = ResolveOptions(
|
|
51
|
+
env=_get_env(env),
|
|
52
|
+
base_dir=base_dir,
|
|
53
|
+
read_file_sync=read_file or _default_read_file_sync,
|
|
54
|
+
resolve_from=resolve_from,
|
|
55
|
+
package_resolver=package_resolver,
|
|
56
|
+
origin_description=origin_description,
|
|
57
|
+
)
|
|
58
|
+
return ast, opts
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def parse(
|
|
62
|
+
text: str,
|
|
63
|
+
*,
|
|
64
|
+
base_dir: str | None = None,
|
|
65
|
+
env: dict[str, str] | None = None,
|
|
66
|
+
read_file: _ReadFile | None = None,
|
|
67
|
+
resolve_substitutions: bool = True,
|
|
68
|
+
origin_description: str | None = None,
|
|
69
|
+
resolve_from: _ResolveFrom = None,
|
|
70
|
+
package_resolver: PackageResolver | None = None,
|
|
71
|
+
) -> Config:
|
|
72
|
+
"""Parse HOCON source text. Fully resolves by default; pass
|
|
73
|
+
``resolve_substitutions=False`` to return an unresolved Config with
|
|
74
|
+
substitution placeholders intact (complete later with ``Config.resolve``)."""
|
|
75
|
+
ast, opts = _build_resolve_context(
|
|
76
|
+
text, base_dir, env, read_file, origin_description, resolve_from, package_resolver
|
|
77
|
+
)
|
|
78
|
+
if resolve_substitutions:
|
|
79
|
+
value = resolve(ast, opts) # type: ignore[arg-type]
|
|
80
|
+
if not isinstance(value, HoconObject):
|
|
81
|
+
raise RuntimeError("resolved value is not an object")
|
|
82
|
+
return Config._from_resolved_value(value, origin_description)
|
|
83
|
+
tree = build_tree(ast, opts) # type: ignore[arg-type]
|
|
84
|
+
has_placeholders = contains_placeholders(tree)
|
|
85
|
+
return Config._from_unresolved_res_obj(
|
|
86
|
+
tree,
|
|
87
|
+
parse_base_dir=base_dir,
|
|
88
|
+
origin_description=origin_description,
|
|
89
|
+
resolved=not has_placeholders,
|
|
90
|
+
resolve_opts=opts,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# Lightbend-aligned alias for parse(). Produces a fully resolved Config.
|
|
95
|
+
parse_string = parse
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def parse_file(
|
|
99
|
+
path: str,
|
|
100
|
+
*,
|
|
101
|
+
base_dir: str | None = None,
|
|
102
|
+
env: dict[str, str] | None = None,
|
|
103
|
+
read_file: _ReadFile | None = None,
|
|
104
|
+
resolve_substitutions: bool = True,
|
|
105
|
+
origin_description: str | None = None,
|
|
106
|
+
resolve_from: _ResolveFrom = None,
|
|
107
|
+
package_resolver: PackageResolver | None = None,
|
|
108
|
+
) -> Config:
|
|
109
|
+
"""Parse a HOCON file, resolving relative includes against its directory."""
|
|
110
|
+
resolved_path = os.path.abspath(path)
|
|
111
|
+
reader = read_file or _default_read_file_sync
|
|
112
|
+
text = reader(resolved_path)
|
|
113
|
+
return parse(
|
|
114
|
+
text,
|
|
115
|
+
base_dir=base_dir if base_dir is not None else os.path.dirname(resolved_path),
|
|
116
|
+
env=env,
|
|
117
|
+
read_file=reader,
|
|
118
|
+
resolve_substitutions=resolve_substitutions,
|
|
119
|
+
origin_description=origin_description,
|
|
120
|
+
resolve_from=resolve_from,
|
|
121
|
+
package_resolver=package_resolver,
|
|
122
|
+
)
|
hocon/py.typed
ADDED
|
File without changes
|
hocon/value.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Value model for parsed/resolved HOCON trees.
|
|
2
|
+
|
|
3
|
+
Mirrors ts.hocon ``src/value.ts``. The three variants (object / array / scalar)
|
|
4
|
+
are plain classes discriminated by ``isinstance`` (Python's structural narrowing
|
|
5
|
+
replaces the TS ``kind`` discriminated union). Object fields use a ``dict``,
|
|
6
|
+
which preserves insertion order like the JS ``Map`` ts.hocon relies on.
|
|
7
|
+
|
|
8
|
+
Standalone ``as_*`` / ``is_*`` accessors mirror rs.hocon's ``HoconValue::as_*``
|
|
9
|
+
(a discriminated union has no methods of its own).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Literal, TypeGuard
|
|
15
|
+
|
|
16
|
+
from .coerce import coerce_boolean, coerce_number
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"HoconArray",
|
|
20
|
+
"HoconObject",
|
|
21
|
+
"HoconScalar",
|
|
22
|
+
"HoconValue",
|
|
23
|
+
"ScalarValueType",
|
|
24
|
+
"as_array",
|
|
25
|
+
"as_boolean",
|
|
26
|
+
"as_number",
|
|
27
|
+
"as_object",
|
|
28
|
+
"as_string",
|
|
29
|
+
"is_array",
|
|
30
|
+
"is_null",
|
|
31
|
+
"is_object",
|
|
32
|
+
"is_scalar",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
ScalarValueType = Literal["string", "number", "boolean", "null"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class HoconObject:
|
|
39
|
+
kind: Literal["object"] = "object"
|
|
40
|
+
|
|
41
|
+
def __init__(self, fields: dict[str, HoconValue]) -> None:
|
|
42
|
+
self.fields = fields
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class HoconArray:
|
|
46
|
+
kind: Literal["array"] = "array"
|
|
47
|
+
|
|
48
|
+
def __init__(self, items: list[HoconValue]) -> None:
|
|
49
|
+
self.items = items
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class HoconScalar:
|
|
53
|
+
kind: Literal["scalar"] = "scalar"
|
|
54
|
+
|
|
55
|
+
def __init__(self, raw: str, value_type: ScalarValueType) -> None:
|
|
56
|
+
self.raw = raw
|
|
57
|
+
self.value_type = value_type
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
HoconValue = HoconObject | HoconArray | HoconScalar
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ─── value accessors ──────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def as_string(v: HoconValue) -> str | None:
|
|
67
|
+
"""Scalar string only (strict): non-string scalars and containers → None."""
|
|
68
|
+
if isinstance(v, HoconScalar) and v.value_type == "string":
|
|
69
|
+
return v.raw
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def as_number(v: HoconValue) -> float | None:
|
|
74
|
+
"""Scalar coerced to a number via :func:`coerce_number` (lenient)."""
|
|
75
|
+
if isinstance(v, HoconScalar):
|
|
76
|
+
return coerce_number(v.raw)
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def as_boolean(v: HoconValue) -> bool | None:
|
|
81
|
+
"""Scalar coerced to a boolean (true/yes/on, false/no/off)."""
|
|
82
|
+
if isinstance(v, HoconScalar):
|
|
83
|
+
return coerce_boolean(v.raw)
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def as_object(v: HoconValue) -> dict[str, HoconValue] | None:
|
|
88
|
+
"""The object's fields, or None if not an object."""
|
|
89
|
+
return v.fields if isinstance(v, HoconObject) else None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def as_array(v: HoconValue) -> list[HoconValue] | None:
|
|
93
|
+
"""The array's items, or None if not an array."""
|
|
94
|
+
return v.items if isinstance(v, HoconArray) else None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def is_object(v: HoconValue) -> TypeGuard[HoconObject]:
|
|
98
|
+
return isinstance(v, HoconObject)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def is_array(v: HoconValue) -> TypeGuard[HoconArray]:
|
|
102
|
+
return isinstance(v, HoconArray)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def is_scalar(v: HoconValue) -> TypeGuard[HoconScalar]:
|
|
106
|
+
return isinstance(v, HoconScalar)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def is_null(v: HoconValue) -> bool:
|
|
110
|
+
"""True for a null scalar."""
|
|
111
|
+
return isinstance(v, HoconScalar) and v.value_type == "null"
|
hocon/value_factory.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Programmatic construction of resolved Configs (``from_map`` / ``empty``).
|
|
2
|
+
|
|
3
|
+
Mirrors ts.hocon ``src/value-factory.ts``. Keys are treated as plain keys (NOT
|
|
4
|
+
path expressions — ``"a.b"`` is a literal top-level key, not nested). Values are
|
|
5
|
+
coerced per E12 § "Value factories".
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .config import Config
|
|
14
|
+
from .errors import ConfigError
|
|
15
|
+
from .value import HoconArray, HoconObject, HoconScalar, HoconValue
|
|
16
|
+
|
|
17
|
+
__all__ = ["empty", "from_map"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def from_map(values: dict[str, Any], origin_description: str | None = None) -> Config:
|
|
21
|
+
"""Construct a resolved Config from a plain ``dict``."""
|
|
22
|
+
fields = _coerce_object(values, "")
|
|
23
|
+
return Config._from_resolved_value(HoconObject(fields), origin_description)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def empty(origin_description: str | None = None) -> Config:
|
|
27
|
+
"""Return a resolved Config with no keys (equivalent to ``from_map({})``)."""
|
|
28
|
+
return Config._from_resolved_value(HoconObject({}), origin_description)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _coerce_object(obj: dict[str, Any], at_path: str) -> dict[str, HoconValue]:
|
|
32
|
+
fields: dict[str, HoconValue] = {}
|
|
33
|
+
for k, v in obj.items():
|
|
34
|
+
child_path = f"{at_path}.{k}" if at_path else k
|
|
35
|
+
fields[str(k)] = _coerce_value(v, child_path)
|
|
36
|
+
return fields
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _coerce_value(v: Any, at_path: str) -> HoconValue:
|
|
40
|
+
if v is None:
|
|
41
|
+
return HoconScalar("null", "null")
|
|
42
|
+
# bool must precede int — bool is a subclass of int in Python.
|
|
43
|
+
if isinstance(v, bool):
|
|
44
|
+
return HoconScalar("true" if v else "false", "boolean")
|
|
45
|
+
if isinstance(v, int):
|
|
46
|
+
return HoconScalar(str(v), "number")
|
|
47
|
+
if isinstance(v, float):
|
|
48
|
+
if math.isnan(v) or math.isinf(v):
|
|
49
|
+
raise ConfigError(
|
|
50
|
+
f"from_map: {v} is not representable in HOCON "
|
|
51
|
+
f'(NaN/Infinity not allowed) at path "{at_path}"',
|
|
52
|
+
at_path,
|
|
53
|
+
)
|
|
54
|
+
return HoconScalar(_render_float(v), "number")
|
|
55
|
+
if isinstance(v, str):
|
|
56
|
+
return HoconScalar(v, "string")
|
|
57
|
+
if isinstance(v, (list, tuple)):
|
|
58
|
+
return HoconArray([_coerce_value(item, f"{at_path}[{i}]") for i, item in enumerate(v)])
|
|
59
|
+
if isinstance(v, dict):
|
|
60
|
+
return HoconObject(_coerce_object(v, at_path))
|
|
61
|
+
raise ConfigError(
|
|
62
|
+
f'from_map: unsupported value type "{type(v).__name__}" at path "{at_path}"',
|
|
63
|
+
at_path,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _render_float(v: float) -> str:
|
|
68
|
+
"""Render a float like JS ``String(v)``: integral floats drop the ``.0``."""
|
|
69
|
+
if v.is_integer():
|
|
70
|
+
return str(int(v))
|
|
71
|
+
return repr(v)
|