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/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)