confluid 0.1.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.
confluid/__init__.py ADDED
@@ -0,0 +1,152 @@
1
+ """
2
+ Confluid: Modern, hierarchical configuration and dependency injection.
3
+
4
+ The pydantic-powered schema-export API (``to_pydantic``, ``confluid_class_of``)
5
+ is exposed lazily via :pep:`562` ``__getattr__`` so importing confluid never
6
+ requires pydantic — it is the optional ``confluid[pydantic]`` extra. Accessing
7
+ those names without pydantic installed raises an ``ImportError`` naming the
8
+ extra.
9
+
10
+ ``__all__`` is the CURATED public surface (pruned 2026-07): internal
11
+ machinery (validation plumbing, scope resolution, annotation predicates,
12
+ marker internals) stays importable from its home module but is deliberately
13
+ not re-exported here.
14
+ """
15
+
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ from confluid.configurator import configure, configure_from_file
19
+ from confluid.decorators import configurable, ignore_config, output, register
20
+ from confluid.dumper import dump
21
+ from confluid.engine import active_context, cast, collect_report, flow, get_configurable_attrs, materialize, resolve
22
+ from confluid.exceptions import (
23
+ CircularIncludeError,
24
+ ConfigFileNotFoundError,
25
+ ConfigurableDefinitionError,
26
+ ConfigurationError,
27
+ ConfluidError,
28
+ ConstructionError,
29
+ IntrospectionError,
30
+ ReferenceResolutionError,
31
+ ScopeError,
32
+ UnknownClassError,
33
+ ValidationModeError,
34
+ WorkspaceEnvError,
35
+ )
36
+ from confluid.fluid import Class, Clone, Fluid, Instance
37
+ from confluid.fluid import Lazy as LazyClass
38
+ from confluid.fluid import Reference, format_yaml_loc
39
+ from confluid.lazy import Lazy, lazy_param_names
40
+ from confluid.llm_schema import sanitize_schema
41
+ from confluid.loader import get_app_name, load, load_config, load_config_with_paths, resolve_config_path, set_app_name
42
+ from confluid.mandatory import Mandatory, mandatory_param_names
43
+ from confluid.merger import deep_merge, expand_dotted_keys
44
+ from confluid.no_broadcast import NoBroadcast, no_broadcast_param_names
45
+ from confluid.registry import get_registry
46
+ from confluid.report import ConfigurationReport
47
+ from confluid.resolver import parse_value
48
+ from confluid.schema import (
49
+ InputSpec,
50
+ OutputSpec,
51
+ get_hierarchy,
52
+ get_hierarchy_from_instance,
53
+ input_specs,
54
+ output_specs,
55
+ parse_param_docs,
56
+ shortest_unique_paths,
57
+ )
58
+ from confluid.scopes import discover_dimensions
59
+ from confluid.validation import ValidationMode, ValidationPolicy, get_policy, reset_policy, set_policy, validate_model
60
+
61
+ __all__ = [
62
+ "ConfluidError",
63
+ "ConfigurationError",
64
+ "CircularIncludeError",
65
+ "ReferenceResolutionError",
66
+ "UnknownClassError",
67
+ "ConfigurableDefinitionError",
68
+ "ValidationModeError",
69
+ "ScopeError",
70
+ "ConfigFileNotFoundError",
71
+ "ConstructionError",
72
+ "WorkspaceEnvError",
73
+ "IntrospectionError",
74
+ "configurable",
75
+ "register",
76
+ "ignore_config",
77
+ "output",
78
+ "get_registry",
79
+ "load",
80
+ "load_config",
81
+ "load_config_with_paths",
82
+ "resolve_config_path",
83
+ "set_app_name",
84
+ "get_app_name",
85
+ "materialize",
86
+ "resolve",
87
+ "active_context",
88
+ "deep_merge",
89
+ "expand_dotted_keys",
90
+ "parse_value",
91
+ "dump",
92
+ "configure",
93
+ "configure_from_file",
94
+ "ConfigurationReport",
95
+ "collect_report",
96
+ "Fluid",
97
+ "Class",
98
+ "Clone",
99
+ "Instance",
100
+ "Reference",
101
+ "flow",
102
+ "cast",
103
+ "format_yaml_loc",
104
+ "Lazy",
105
+ "LazyClass",
106
+ "lazy_param_names",
107
+ "Mandatory",
108
+ "mandatory_param_names",
109
+ "NoBroadcast",
110
+ "no_broadcast_param_names",
111
+ "get_hierarchy",
112
+ "get_hierarchy_from_instance",
113
+ "input_specs",
114
+ "output_specs",
115
+ "InputSpec",
116
+ "OutputSpec",
117
+ "parse_param_docs",
118
+ "shortest_unique_paths",
119
+ "get_configurable_attrs",
120
+ "to_pydantic",
121
+ "confluid_class_of",
122
+ "discover_dimensions",
123
+ "ValidationMode",
124
+ "ValidationPolicy",
125
+ "get_policy",
126
+ "set_policy",
127
+ "reset_policy",
128
+ "validate_model",
129
+ "sanitize_schema",
130
+ ]
131
+
132
+ if TYPE_CHECKING:
133
+ from confluid.pydantic_export import confluid_class_of, to_pydantic
134
+
135
+ # Names served lazily from ``confluid.pydantic_export`` (requires the
136
+ # ``confluid[pydantic]`` extra) — see the module docstring.
137
+ _PYDANTIC_EXPORTS = ("to_pydantic", "confluid_class_of")
138
+
139
+
140
+ def __getattr__(name: str) -> Any:
141
+ if name in _PYDANTIC_EXPORTS:
142
+ try:
143
+ from confluid import pydantic_export
144
+ except ModuleNotFoundError as exc:
145
+ if exc.name in ("pydantic", "annotated_types"):
146
+ raise ImportError(
147
+ f"confluid.{name} requires pydantic, which is an optional dependency — "
148
+ "install the extra: pip install 'confluid[pydantic]'"
149
+ ) from exc
150
+ raise
151
+ return getattr(pydantic_export, name)
152
+ raise AttributeError(f"module 'confluid' has no attribute {name!r}")
confluid/bake.py ADDED
@@ -0,0 +1,174 @@
1
+ """Build-time AST bake of ``__init__`` body-slot names — the packaged-mode scan.
2
+
3
+ Confluid's broadcasting discovers post-init body attributes (``self.loss_fn = …``
4
+ inside ``__init__``) by AST-scanning constructor SOURCE at runtime
5
+ (:func:`confluid.introspect.scan_init_body`). Compiled / frozen / zip
6
+ deployments ship no source, so the runtime scan is silently empty and body
7
+ slots vanish from the broadcast surface. This module runs the SAME scan at
8
+ BUILD time — while source still exists — and emits a generated
9
+ ``<package>/_confluid_baked.py`` table that the engine unions in at runtime
10
+ (scanned ∪ declared ``broadcast_attrs`` ∪ baked): automatic dev/packaged
11
+ parity, no hand-maintained ``broadcast_attrs`` lists.
12
+
13
+ Run it in the packaging pipeline, BEFORE freezing/zipping::
14
+
15
+ python -m confluid.bake mypackage [otherpackage ...]
16
+ python -m confluid.bake mypackage --check # CI drift guard (no write)
17
+
18
+ Every class the package defines with its own ``__init__`` is baked — not only
19
+ ``@configurable`` ones — so in-package base classes contribute their body
20
+ slots through the MRO exactly as the runtime scan would. An empty entry is
21
+ meaningful ("scanned at build time, no body slots"): it silences the engine's
22
+ cannot-scan warning for that class.
23
+
24
+ Frozen-bundler caveat: the engine imports ``<pkg>._confluid_baked`` lazily by
25
+ dotted name (:func:`confluid.introspect.baked_init_attrs`), which static
26
+ import tracers (PyInstaller) don't see — declare it as a hidden import
27
+ (``--hidden-import mypkg._confluid_baked``) or import it explicitly from the
28
+ package's ``__init__``. Wheel/zip/pyc-only deployments need nothing extra.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import argparse
34
+ import importlib
35
+ import pkgutil
36
+ import sys
37
+ from pathlib import Path
38
+ from typing import Any, Dict, List, Optional, Tuple
39
+
40
+ from loggair import get_logger
41
+
42
+ from confluid.exceptions import ConfigurationError
43
+ from confluid.introspect import BAKED_MODULE_BASENAME, init_setattr_names
44
+
45
+ logger = get_logger("confluid.bake")
46
+
47
+ _HEADER_TEMPLATE = """\
48
+ # =========================================================================
49
+ # AUTO-GENERATED FILE — DO NOT EDIT BY HAND
50
+ # Generated by: python -m confluid.bake {package}
51
+ # Source: confluid/bake.py (bake_broadcast_attrs)
52
+ # To regenerate: python -m confluid.bake {package}
53
+ # =========================================================================
54
+ \"\"\"Baked ``__init__`` body-slot names for ``{package}`` (build-time AST scan).
55
+
56
+ Consumed by ``confluid.introspect.baked_init_attrs`` so broadcasting keeps
57
+ seeing post-init attributes in compiled/frozen/zip deployments where
58
+ ``inspect.getsource`` fails. See ``confluid/bake.py``.
59
+ \"\"\"
60
+
61
+ from typing import Dict, Tuple
62
+
63
+ BROADCAST_ATTRS: Dict[str, Tuple[str, ...]] = {{
64
+ {entries}}}
65
+ """
66
+
67
+
68
+ def scan_package(package_name: str) -> Dict[str, Tuple[str, ...]]:
69
+ """AST-scan every class the package defines; return ``{module.QualName: (names,)}``.
70
+
71
+ Imports the package and each of its submodules (a module that fails to
72
+ import is skipped with a warning — its classes simply stay unbaked), then
73
+ scans the ``__init__`` of every top-level class DEFINED in those modules
74
+ (``cls.__module__`` matches — re-exports are skipped, as is the bake
75
+ module itself). Deterministic: modules and attr names are sorted.
76
+ """
77
+ package = importlib.import_module(package_name)
78
+ module_names: List[str] = [package_name]
79
+ search_paths = getattr(package, "__path__", None)
80
+ if search_paths is not None:
81
+ module_names.extend(info.name for info in pkgutil.walk_packages(search_paths, prefix=f"{package_name}."))
82
+
83
+ table: Dict[str, Tuple[str, ...]] = {}
84
+ for module_name in sorted(module_names):
85
+ if module_name.rsplit(".", 1)[-1] == BAKED_MODULE_BASENAME:
86
+ continue # never scan a previous bake output
87
+ try:
88
+ module = importlib.import_module(module_name)
89
+ except Exception as exc: # noqa: BLE001 — a broken optional submodule must not kill the bake
90
+ logger.warning(f"confluid.bake: skipping module {module_name!r} (import failed: {exc})")
91
+ continue
92
+ for value in vars(module).values():
93
+ if not isinstance(value, type) or value.__module__ != module_name:
94
+ continue
95
+ init = value.__dict__.get("__init__")
96
+ if init is None:
97
+ continue
98
+ table[f"{value.__module__}.{value.__qualname__}"] = tuple(sorted(init_setattr_names(init)))
99
+ return table
100
+
101
+
102
+ def render_baked_module(package_name: str, table: Dict[str, Tuple[str, ...]]) -> str:
103
+ """Render the generated ``_confluid_baked.py`` source (deterministic output)."""
104
+ lines: List[str] = []
105
+ for key in sorted(table):
106
+ names = table[key]
107
+ # ("a",) — the trailing comma keeps 1-tuples valid; () renders bare.
108
+ rendered = "(" + "".join(f"{name!r}, " for name in names).rstrip() + ")" if names else "()"
109
+ lines.append(f" {key!r}: {rendered},\n")
110
+ return _HEADER_TEMPLATE.format(package=package_name, entries="".join(lines))
111
+
112
+
113
+ def baked_module_path(package_name: str) -> Path:
114
+ """Filesystem location of the package's bake table (``<pkg>/_confluid_baked.py``)."""
115
+ package = importlib.import_module(package_name)
116
+ package_file = getattr(package, "__file__", None)
117
+ if package_file is None:
118
+ raise ConfigurationError(
119
+ f"confluid.bake: package {package_name!r} has no __file__ (namespace package?) — "
120
+ "the bake table needs a regular package directory to live in."
121
+ )
122
+ return Path(package_file).parent / f"{BAKED_MODULE_BASENAME}.py"
123
+
124
+
125
+ def bake_broadcast_attrs(package_names: List[str], check: bool = False) -> Dict[str, Any]:
126
+ """Scan each package and write (or, with ``check=True``, verify) its bake table.
127
+
128
+ Returns a JSON-serializable summary per package:
129
+ ``{package: {"path": str, "classes": int, "changed": bool}}`` — ``changed``
130
+ means the on-disk table differed from the fresh scan (in check mode
131
+ nothing is written; the caller decides whether drift fails the build).
132
+ """
133
+ summary: Dict[str, Any] = {}
134
+ for package_name in package_names:
135
+ table = scan_package(package_name)
136
+ rendered = render_baked_module(package_name, table)
137
+ path = baked_module_path(package_name)
138
+ existing = path.read_text() if path.exists() else None
139
+ changed = existing != rendered
140
+ if changed and not check:
141
+ path.write_text(rendered)
142
+ logger.info(f"confluid.bake: wrote {path} ({len(table)} classes)")
143
+ summary[package_name] = {"path": str(path), "classes": len(table), "changed": changed}
144
+ return summary
145
+
146
+
147
+ def main(argv: Optional[List[str]] = None) -> int:
148
+ """CLI entry point: ``python -m confluid.bake <package> [...] [--check]``."""
149
+ parser = argparse.ArgumentParser(
150
+ prog="python -m confluid.bake",
151
+ description="Bake __init__ body-slot names into <package>/_confluid_baked.py "
152
+ "so broadcasting keeps working in compiled/frozen deployments.",
153
+ )
154
+ parser.add_argument("packages", nargs="+", help="importable package names to scan")
155
+ parser.add_argument(
156
+ "--check",
157
+ action="store_true",
158
+ help="verify the existing bake tables are up to date (write nothing; exit 1 on drift)",
159
+ )
160
+ args = parser.parse_args(argv)
161
+
162
+ summary = bake_broadcast_attrs(args.packages, check=args.check)
163
+ drifted = [pkg for pkg, info in summary.items() if info["changed"]]
164
+ for pkg, info in summary.items():
165
+ status = "STALE" if info["changed"] and args.check else ("updated" if info["changed"] else "up to date")
166
+ print(f"{pkg}: {info['classes']} classes -> {info['path']} [{status}]")
167
+ if args.check and drifted:
168
+ print(f"bake tables out of date for: {', '.join(drifted)} — rerun python -m confluid.bake", file=sys.stderr)
169
+ return 1
170
+ return 0
171
+
172
+
173
+ if __name__ == "__main__":
174
+ sys.exit(main())