exonware-xwlazy 0.1.0.10__py3-none-any.whl → 0.1.0.11__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,279 @@
1
+ """Host-facing configuration helpers for enabling lazy mode via `exonware.conf`.
2
+
3
+ This module centralizes the legacy configuration surface so host packages no
4
+ longer need to ship their own lazy bootstrap logic. Consumers import
5
+ ``exonware.conf`` as before, while the real implementation now lives in
6
+ ``xwlazy.lazy.host_conf`` to keep lazy concerns within the xwlazy project.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib
12
+ import importlib.metadata
13
+ import subprocess
14
+ import sys
15
+ import types
16
+ import warnings
17
+ from typing import Any, Dict, Optional
18
+
19
+ from .host_packages import refresh_host_package
20
+ from .lazy_core import (
21
+ config_package_lazy_install_enabled,
22
+ install_import_hook,
23
+ uninstall_import_hook,
24
+ is_import_hook_installed,
25
+ is_lazy_install_enabled,
26
+ )
27
+
28
+
29
+ class _PackageConfig:
30
+ """Per-package configuration wrapper."""
31
+
32
+ def __init__(self, package_name: str, parent_conf: "_LazyConfModule"):
33
+ self._package_name = package_name
34
+ self._parent_conf = parent_conf
35
+
36
+ @property
37
+ def lazy_install(self) -> bool:
38
+ """Return lazy install status for this package."""
39
+ return is_lazy_install_enabled(self._package_name)
40
+
41
+ @lazy_install.setter
42
+ def lazy_install(self, value: bool) -> None:
43
+ """Enable/disable lazy mode for this package."""
44
+ if value:
45
+ config_package_lazy_install_enabled(self._package_name, True, install_hook=False)
46
+ install_import_hook(self._package_name)
47
+ refresh_host_package(self._package_name)
48
+ else:
49
+ config_package_lazy_install_enabled(self._package_name, False, install_hook=False)
50
+ uninstall_import_hook(self._package_name)
51
+
52
+ def lazy_install_status(self) -> Dict[str, Any]:
53
+ """Return runtime status for this package."""
54
+ return {
55
+ "package": self._package_name,
56
+ "enabled": is_lazy_install_enabled(self._package_name),
57
+ "hook_installed": is_import_hook_installed(self._package_name),
58
+ "active": is_lazy_install_enabled(self._package_name)
59
+ and is_import_hook_installed(self._package_name),
60
+ }
61
+
62
+ def is_lazy_active(self) -> bool:
63
+ """Return True if lazy install + hook are active."""
64
+ return self.lazy_install_status()["active"]
65
+
66
+
67
+ class _FilteredStderr:
68
+ """Stderr wrapper that filters out specific warning messages."""
69
+
70
+ def __init__(self, original_stderr: Any, filter_patterns: list[str]):
71
+ self._original = original_stderr
72
+ self._filter_patterns = filter_patterns
73
+
74
+ def write(self, text: str) -> int:
75
+ """Write to stderr, filtering out unwanted warnings."""
76
+ # Case-insensitive matching to catch all variations
77
+ if any(pattern.lower() in text.lower() for pattern in self._filter_patterns):
78
+ return len(text) # Pretend we wrote it, but don't actually write
79
+ return self._original.write(text)
80
+
81
+ def flush(self) -> None:
82
+ """Flush the original stderr."""
83
+ self._original.flush()
84
+
85
+ def reconfigure(self, *args, **kwargs):
86
+ """Handle reconfigure calls - update original reference and reapply filter."""
87
+ result = self._original.reconfigure(*args, **kwargs)
88
+ # Ensure filter stays active
89
+ if sys.stderr is not self:
90
+ sys.stderr = self # type: ignore[assignment]
91
+ return result
92
+
93
+ def __getattr__(self, name: str):
94
+ """Delegate all other attributes to original stderr."""
95
+ return getattr(self._original, name)
96
+
97
+
98
+ class _LazyConfModule(types.ModuleType):
99
+ """Configuration module for all exonware packages."""
100
+
101
+ def __init__(self, name: str, doc: Optional[str]) -> None:
102
+ super().__init__(name, doc)
103
+ self._package_configs: Dict[str, _PackageConfig] = {}
104
+ self._suppress_warnings: bool = True # Default: suppress warnings
105
+ self._original_stderr: Optional[Any] = None
106
+ self._filtered_stderr: Optional[_FilteredStderr] = None
107
+ # Set up warning suppression by default
108
+ self._setup_warning_filter()
109
+
110
+ # ------------------------------------------------------------------ helpers
111
+ def _is_xwlazy_installed(self) -> bool:
112
+ try:
113
+ importlib.metadata.distribution("exonware-xwlazy")
114
+ return True
115
+ except importlib.metadata.PackageNotFoundError:
116
+ try:
117
+ importlib.metadata.distribution("xwlazy")
118
+ return True
119
+ except importlib.metadata.PackageNotFoundError:
120
+ return False
121
+ except Exception:
122
+ try:
123
+ import xwlazy # noqa: F401
124
+ return True
125
+ except ImportError:
126
+ return False
127
+
128
+ def _ensure_xwlazy_installed(self) -> None:
129
+ if self._is_xwlazy_installed():
130
+ return
131
+ try:
132
+ result = subprocess.run(
133
+ [sys.executable, "-m", "pip", "install", "exonware-xwlazy"],
134
+ capture_output=True,
135
+ text=True,
136
+ check=False,
137
+ )
138
+ if result.returncode == 0:
139
+ print("[OK] Installed exonware-xwlazy for lazy mode")
140
+ else:
141
+ print(f"[WARN] Failed to install exonware-xwlazy: {result.stderr}")
142
+ except Exception as exc: # pragma: no cover - best effort
143
+ print(f"[WARN] Could not install exonware-xwlazy: {exc}")
144
+
145
+ def _uninstall_xwlazy(self) -> None:
146
+ if not self._is_xwlazy_installed():
147
+ return
148
+ try:
149
+ subprocess.run(
150
+ [sys.executable, "-m", "pip", "uninstall", "-y", "exonware-xwlazy", "xwlazy"],
151
+ capture_output=True,
152
+ text=True,
153
+ check=False,
154
+ )
155
+ print("[OK] Uninstalled exonware-xwlazy (lazy mode disabled)")
156
+ except Exception as exc: # pragma: no cover
157
+ print(f"[WARN] Could not uninstall exonware-xwlazy: {exc}")
158
+
159
+ def _get_global_lazy_status(self) -> Dict[str, Any]:
160
+ """Return aggregate status for DX tooling."""
161
+ installed = self._is_xwlazy_installed()
162
+ # Check all known packages, not just those in _package_configs
163
+ # This ensures we catch hooks installed via register_host_package
164
+ known_packages = list(self._package_configs.keys())
165
+ # Also check common package names that might have hooks installed
166
+ for pkg_name in ("xwsystem", "xwnode", "xwdata", "xwschema", "xwaction", "xwentity"):
167
+ if pkg_name not in known_packages and is_import_hook_installed(pkg_name):
168
+ known_packages.append(pkg_name)
169
+
170
+ hook_installed = any(is_import_hook_installed(pkg) for pkg in known_packages)
171
+ # Check if any package has lazy active (including those not yet in _package_configs)
172
+ active_configs = any(cfg.is_lazy_active() for cfg in self._package_configs.values())
173
+ # Also check directly for packages with hooks and enabled lazy install
174
+ active_direct = any(
175
+ is_import_hook_installed(pkg) and is_lazy_install_enabled(pkg)
176
+ for pkg in known_packages
177
+ )
178
+
179
+ return {
180
+ "xwlazy_installed": installed,
181
+ "enabled": installed,
182
+ "hook_installed": hook_installed,
183
+ "active": active_configs or active_direct,
184
+ }
185
+
186
+ def _setup_warning_filter(self) -> None:
187
+ """Set up or remove the stderr warning filter based on current setting."""
188
+ global _ORIGINAL_STDERR, _FILTERED_STDERR
189
+ if self._suppress_warnings:
190
+ # Use global filter (already set up at module import)
191
+ if _FILTERED_STDERR is not None and sys.stderr is not _FILTERED_STDERR:
192
+ sys.stderr = _FILTERED_STDERR # type: ignore[assignment]
193
+ else:
194
+ # Restore original stderr if we were filtering
195
+ if _ORIGINAL_STDERR is not None and sys.stderr is _FILTERED_STDERR:
196
+ sys.stderr = _ORIGINAL_STDERR
197
+
198
+ # ---------------------------------------------------------------- attr API
199
+ def __getattr__(self, name: str):
200
+ package_names = ("xwsystem", "xwnode", "xwdata", "xwschema", "xwaction", "xwentity")
201
+ if name in package_names:
202
+ if name not in self._package_configs:
203
+ self._package_configs[name] = _PackageConfig(name, self)
204
+ return self._package_configs[name]
205
+
206
+ if name == "lazy_install":
207
+ return self._is_xwlazy_installed()
208
+ if name == "lazy_install_status":
209
+ return self._get_global_lazy_status
210
+ if name == "is_lazy_active":
211
+ return any(cfg.is_lazy_active() for cfg in self._package_configs.values())
212
+ if name == "suppress_warnings":
213
+ return self._suppress_warnings
214
+
215
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
216
+
217
+ def __setattr__(self, name: str, value) -> None:
218
+ if name.startswith("_"):
219
+ super().__setattr__(name, value)
220
+ return
221
+ if name == "lazy_install":
222
+ if value:
223
+ self._ensure_xwlazy_installed()
224
+ self.__getattr__("xwsystem").lazy_install = True
225
+ else:
226
+ self.__getattr__("xwsystem").lazy_install = False
227
+ self._uninstall_xwlazy()
228
+ return
229
+ if name == "suppress_warnings":
230
+ self._suppress_warnings = bool(value)
231
+ self._setup_warning_filter()
232
+ return
233
+ super().__setattr__(name, value)
234
+
235
+
236
+ _CONF_INSTANCE: Optional[_LazyConfModule] = None
237
+ _ORIGINAL_STDERR: Optional[Any] = None
238
+ _FILTERED_STDERR: Optional[_FilteredStderr] = None
239
+
240
+
241
+ def _setup_global_warning_filter() -> None:
242
+ """Set up global stderr filter for decimal module warnings (called at module import)."""
243
+ global _ORIGINAL_STDERR, _FILTERED_STDERR
244
+ # Check if a filter is already active (e.g., from exonware/__init__.py or conf.py)
245
+ # Check for both our filter class and the early filter class
246
+ if (hasattr(sys.stderr, '_original') or
247
+ isinstance(sys.stderr, _FilteredStderr) or
248
+ type(sys.stderr).__name__ == '_EarlyStderrFilter'):
249
+ # Filter already active, use existing one
250
+ _FILTERED_STDERR = sys.stderr # type: ignore[assignment]
251
+ return
252
+ if _ORIGINAL_STDERR is None:
253
+ # If stderr has _original, it's already wrapped - use that as original
254
+ if hasattr(sys.stderr, '_original'):
255
+ _ORIGINAL_STDERR = sys.stderr._original
256
+ else:
257
+ _ORIGINAL_STDERR = sys.stderr
258
+ if _FILTERED_STDERR is None:
259
+ _FILTERED_STDERR = _FilteredStderr(
260
+ _ORIGINAL_STDERR,
261
+ ["mpd_setminalloc", "MPD_MINALLOC", "ignoring request to set", "libmpdec", "context.c:57"]
262
+ )
263
+ if sys.stderr is not _FILTERED_STDERR:
264
+ sys.stderr = _FILTERED_STDERR # type: ignore[assignment]
265
+
266
+
267
+ # Set up warning filter immediately when module is imported (default: suppress warnings)
268
+ # Note: conf.py may have already set up a filter, which is fine
269
+ _setup_global_warning_filter()
270
+
271
+
272
+ def get_conf_module(name: str = "exonware.conf", doc: Optional[str] = None) -> types.ModuleType:
273
+ """Return (and memoize) the shared conf module instance."""
274
+ global _CONF_INSTANCE
275
+ if _CONF_INSTANCE is None:
276
+ _CONF_INSTANCE = _LazyConfModule(name, doc)
277
+ return _CONF_INSTANCE
278
+
279
+
@@ -0,0 +1,122 @@
1
+ """Lightweight helpers for registering host packages with xwlazy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from typing import Iterable, Sequence, Tuple
8
+
9
+ from .lazy_core import (
10
+ config_package_lazy_install_enabled,
11
+ install_import_hook,
12
+ is_lazy_install_enabled,
13
+ LazyMetaPathFinder,
14
+ register_lazy_module_methods,
15
+ register_lazy_module_prefix,
16
+ )
17
+
18
+ _TRUTHY = {"1", "true", "yes", "on"}
19
+ _REGISTERED: dict[str, dict[str, Tuple[str, ...]]] = {}
20
+
21
+
22
+ def _normalized(items: Iterable[str]) -> Tuple[str, ...]:
23
+ seen = []
24
+ for item in items:
25
+ if item not in seen:
26
+ seen.append(item)
27
+ return tuple(seen)
28
+
29
+
30
+ def register_host_package(
31
+ package_name: str,
32
+ module_prefixes: Iterable[str] = (),
33
+ method_prefixes: Iterable[str] = (),
34
+ method_names: Sequence[str] = ("encode", "decode"),
35
+ auto_config: bool = True,
36
+ env_var: str | None = None,
37
+ ) -> None:
38
+ """
39
+ Register a host package (e.g., xwsystem) with xwlazy.
40
+
41
+ Args:
42
+ package_name: Host package name.
43
+ module_prefixes: Prefixes that should be lazily wrapped.
44
+ method_prefixes: Prefixes whose classes expose class-level helpers.
45
+ method_names: Methods to expose at class level (default encode/decode).
46
+ auto_config: If True, record lazy config but do not install hook yet.
47
+ env_var: Optional environment variable to force enable (defaults to
48
+ ``{PACKAGE}_LAZY_INSTALL``).
49
+ """
50
+ package_name = package_name.lower()
51
+
52
+ module_prefixes = _normalized(module_prefixes)
53
+ method_prefixes = _normalized(method_prefixes)
54
+ _REGISTERED[package_name] = {
55
+ "module_prefixes": module_prefixes,
56
+ "method_prefixes": method_prefixes,
57
+ }
58
+
59
+ for prefix in module_prefixes:
60
+ register_lazy_module_prefix(prefix)
61
+
62
+ for prefix in method_prefixes:
63
+ register_lazy_module_methods(prefix, tuple(method_names))
64
+
65
+ if auto_config:
66
+ # Detect if lazy should be enabled (checks keyword, marker package, etc.)
67
+ config_package_lazy_install_enabled(package_name, enabled=None, install_hook=False)
68
+
69
+ # If detection found that lazy should be enabled, install the hook automatically
70
+ if is_lazy_install_enabled(package_name):
71
+ try:
72
+ install_import_hook(package_name)
73
+ except Exception:
74
+ # Best-effort: package import must continue even if hook installation fails
75
+ pass
76
+
77
+ _apply_wrappers_for_loaded_modules(package_name, module_prefixes, method_prefixes)
78
+
79
+ env_key = env_var or f"{package_name.upper()}_LAZY_INSTALL"
80
+ flag = os.environ.get(env_key, "")
81
+ if flag.strip().lower() in _TRUTHY:
82
+ config_package_lazy_install_enabled(package_name, enabled=True)
83
+ try:
84
+ install_import_hook(package_name)
85
+ except Exception:
86
+ # Best-effort: package import must continue even if hook installation fails
87
+ pass
88
+
89
+
90
+ def refresh_host_package(package_name: str) -> None:
91
+ """Re-apply wrappers for a registered package."""
92
+ data = _REGISTERED.get(package_name.lower())
93
+ if not data:
94
+ return
95
+ _apply_wrappers_for_loaded_modules(
96
+ package_name,
97
+ data["module_prefixes"],
98
+ data["method_prefixes"],
99
+ )
100
+
101
+
102
+ def _apply_wrappers_for_loaded_modules(
103
+ package_name: str,
104
+ module_prefixes: Iterable[str],
105
+ method_prefixes: Iterable[str],
106
+ ) -> None:
107
+ """Enhance already-imported modules so encode/decode helpers work immediately."""
108
+ prefixes = _normalized((*module_prefixes, *method_prefixes))
109
+ if not prefixes:
110
+ return
111
+
112
+ finder = LazyMetaPathFinder(package_name)
113
+ for module_name, module in list(sys.modules.items()):
114
+ if not isinstance(module_name, str) or module is None:
115
+ continue
116
+ if any(module_name.startswith(prefix) for prefix in prefixes):
117
+ try:
118
+ finder._enhance_classes_with_class_methods(module) # type: ignore[attr-defined]
119
+ except Exception:
120
+ continue
121
+
122
+