Coverage for src/taipanstack/core/compat.py: 100%
128 statements
« prev ^ index » next coverage.py v7.15.1, created at 2026-10-13 20:52 +0000
« prev ^ index » next coverage.py v7.15.1, created at 2026-10-13 20:52 +0000
1"""
2Python Version Compatibility and Feature Detection.
4This module provides runtime detection of Python version and available
5performance features, enabling version-specific optimizations while
6maintaining compatibility with Python 3.11+.
8Following TaipanStack pillars: Security, Stability, Simplicity,
9Scalability, Compatibility.
10"""
12import logging
13import os
14import platform
15import sys
16import sysconfig
17from dataclasses import dataclass
18from enum import StrEnum
19from typing import Final
21__all__ = [
22 "PY311",
23 "PY312",
24 "PY313",
25 "PY314",
26 "PY_VERSION",
27 "PythonFeatures",
28 "VersionTier",
29 "get_features",
30 "get_python_info",
31 "is_experimental_enabled",
32]
34logger = logging.getLogger(__name__)
36# =============================================================================
37# Version Constants
38# =============================================================================
40PY_VERSION: Final = sys.version_info
41"""Current Python version tuple."""
43PY311: Final[bool] = PY_VERSION >= (3, 11)
44"""True if running Python 3.11 or higher."""
46PY312: Final[bool] = PY_VERSION >= (3, 12)
47"""True if running Python 3.12 or higher."""
49PY313: Final[bool] = PY_VERSION >= (3, 13)
50"""True if running Python 3.13 or higher."""
52PY314: Final[bool] = PY_VERSION >= (3, 14)
53"""True if running Python 3.14 or higher."""
56class VersionTier(StrEnum):
57 """Python version tier for optimization profiles."""
59 STABLE = "stable" # 3.11 - fully stable, conservative optimizations
60 ENHANCED = "enhanced" # 3.12 - improved features, moderate optimizations
61 MODERN = "modern" # 3.13 - JIT/free-threading available (experimental)
62 CUTTING_EDGE = "cutting_edge" # 3.14+ - latest optimizations
65# =============================================================================
66# Environment Variables for Experimental Features
67# =============================================================================
69ENV_ENABLE_EXPERIMENTAL = "STACK_ENABLE_EXPERIMENTAL"
70"""Environment variable to enable experimental features (JIT, free-threading)."""
72ENV_OPTIMIZATION_LEVEL = "STACK_OPTIMIZATION_LEVEL"
73"""Optimization level: 0=none, 1=safe, 2=aggressive (requires experimental)."""
76# =============================================================================
77# Feature Detection Functions
78# =============================================================================
81def _check_jit_available() -> bool:
82 """Check if JIT compiler is available and enabled.
84 JIT is available in Python 3.13+ when built with --enable-experimental-jit.
85 """
86 if not PY313:
87 return False
89 try:
90 # Check if the JIT module exists (Python 3.13+)
91 # This is a build-time option, not all builds have it
92 return hasattr(sys, "_jit") or "jit" in sys.flags.__class__.__annotations__
93 except (AttributeError, TypeError):
94 return False
97def _check_nogil_flag() -> bool | None:
98 """Check if nogil flag is present and enabled.
100 Returns:
101 True if the flag is present and True.
102 False if the flag is present and False.
103 None if the flag is not present.
105 """
106 try:
107 if hasattr(sys.flags, "nogil"):
108 return bool(sys.flags.nogil) # type: ignore[misc]
109 except (AttributeError, TypeError):
110 _ = None
111 return None
114def _check_disable_gil_config() -> bool:
115 """Check if build was configured with --disable-gil."""
116 try:
117 config_args = sysconfig.get_config_var("CONFIG_ARGS") or "" # type: ignore[misc]
118 except (AttributeError, TypeError):
119 return False
120 else:
121 return "--disable-gil" in config_args # type: ignore[misc]
124def _check_free_threading_available() -> bool:
125 """Check if free-threading (no-GIL) build is being used.
127 Free-threading is available in Python 3.13+ experimental builds.
128 """
129 if not PY313:
130 return False
132 nogil = _check_nogil_flag()
133 if nogil is not None:
134 return nogil
136 return _check_disable_gil_config()
139def _check_mimalloc_available() -> bool:
140 """Check if mimalloc allocator is being used.
142 mimalloc is the default allocator in Python 3.13+ on some platforms.
143 """
144 if not PY313:
145 return False
147 try:
148 # Check if built with mimalloc
149 config_args = sysconfig.get_config_var("CONFIG_ARGS") or "" # type: ignore[misc]
150 return "mimalloc" in config_args.lower() # type: ignore[misc]
151 except (AttributeError, TypeError):
152 return False
155def _check_tail_call_interpreter() -> bool:
156 """Check if tail-call interpreter optimization is available.
158 Tail-call interpreter is available in Python 3.14+.
159 """
160 return PY314
163_cached_experimental_enabled: bool | None = None
166def is_experimental_enabled(*, force_refresh: bool = False) -> bool:
167 """Check if experimental features are explicitly enabled.
169 Features are cached after first detection for performance.
171 Args:
172 force_refresh: If True, re-detect instead of using cache.
174 Returns:
175 True if STACK_ENABLE_EXPERIMENTAL=1 is set.
177 """
178 global _cached_experimental_enabled # noqa: PLW0603
180 if _cached_experimental_enabled is not None and not force_refresh:
181 return _cached_experimental_enabled
183 value = os.environ.get(ENV_ENABLE_EXPERIMENTAL, "").lower()
184 _cached_experimental_enabled = value in {"1", "true", "yes", "on"}
185 return _cached_experimental_enabled
188_cached_optimization_level: int | None = None
191def get_optimization_level(*, force_refresh: bool = False) -> int:
192 """Get the configured optimization level.
194 Features are cached after first detection for performance.
196 Args:
197 force_refresh: If True, re-detect instead of using cache.
199 Returns:
200 0 = No optimizations
201 1 = Safe optimizations only (default)
202 2 = Aggressive optimizations (requires experimental)
204 """
205 global _cached_optimization_level # noqa: PLW0603
207 if _cached_optimization_level is not None and not force_refresh:
208 return _cached_optimization_level
210 try:
211 level = int(os.environ.get(ENV_OPTIMIZATION_LEVEL, "1"))
212 _cached_optimization_level = max(0, min(2, level)) # Clamp to 0-2
213 except ValueError:
214 _cached_optimization_level = 1
216 return _cached_optimization_level
219# =============================================================================
220# Feature Data Classes
221# =============================================================================
224@dataclass(frozen=True, slots=True)
225class PythonFeatures:
226 """Available Python features based on version and build configuration.
228 This dataclass is immutable and optimized for performance with slots.
229 """
231 version: tuple[int, int, int]
232 version_string: str
233 tier: VersionTier
235 # Build features
236 has_jit: bool = False
237 has_free_threading: bool = False
238 has_mimalloc: bool = False
239 has_tail_call_interpreter: bool = False
241 # Language features by version
242 has_exception_groups: bool = False # 3.11+
243 has_self_type: bool = False # 3.11+
244 has_type_params: bool = False # 3.12+
245 has_override_decorator: bool = False # 3.12+
246 has_deprecated_decorator: bool = False # 3.13+
247 has_deferred_annotations: bool = False # 3.14+
249 # Experimental features enabled
250 experimental_enabled: bool = False
252 def to_dict(self) -> dict[str, object]:
253 """Convert to dictionary for serialization."""
254 return {
255 "version": ".".join(map(str, self.version)),
256 "tier": self.tier.value,
257 "features": {
258 "jit": self.has_jit,
259 "free_threading": self.has_free_threading,
260 "mimalloc": self.has_mimalloc,
261 "tail_call_interpreter": self.has_tail_call_interpreter,
262 },
263 "language": {
264 "exception_groups": self.has_exception_groups,
265 "self_type": self.has_self_type,
266 "type_params": self.has_type_params,
267 "override_decorator": self.has_override_decorator,
268 "deprecated_decorator": self.has_deprecated_decorator,
269 "deferred_annotations": self.has_deferred_annotations,
270 },
271 "experimental_enabled": self.experimental_enabled,
272 }
275def _get_version_tier() -> VersionTier:
276 """Determine the version tier based on Python version."""
277 if PY314:
278 return VersionTier.CUTTING_EDGE
279 if PY313:
280 return VersionTier.MODERN
281 if PY312:
282 return VersionTier.ENHANCED
283 return VersionTier.STABLE
286def _get_build_features(experimental: bool) -> dict[str, bool]:
287 """Determine build features based on experimental flag."""
288 return {
289 "has_jit": _check_jit_available() if experimental else False,
290 "has_free_threading": _check_free_threading_available()
291 if experimental
292 else False,
293 "has_mimalloc": _check_mimalloc_available(),
294 "has_tail_call_interpreter": _check_tail_call_interpreter(),
295 }
298def _get_language_features() -> dict[str, bool]:
299 """Determine language features based on Python version."""
300 return {
301 "has_exception_groups": PY311,
302 "has_self_type": PY311,
303 "has_type_params": PY312,
304 "has_override_decorator": PY312,
305 "has_deprecated_decorator": PY313,
306 "has_deferred_annotations": PY314,
307 }
310# =============================================================================
311# Main Detection Functions
312# =============================================================================
314# Cache the features after first detection
315_cached_features: PythonFeatures | None = None
318def get_features(*, force_refresh: bool = False) -> PythonFeatures:
319 """Detect and return available Python features.
321 Features are cached after first detection for performance.
323 Args:
324 force_refresh: If True, re-detect features instead of using cache.
326 Returns:
327 PythonFeatures dataclass with all detected features.
329 """
330 global _cached_features # noqa: PLW0603 - intentional cache pattern
332 if _cached_features is not None and not force_refresh:
333 return _cached_features
335 tier = _get_version_tier()
336 experimental = is_experimental_enabled(force_refresh=force_refresh)
337 build_feats = _get_build_features(experimental)
338 lang_feats = _get_language_features()
340 features = PythonFeatures(
341 version=(PY_VERSION.major, PY_VERSION.minor, PY_VERSION.micro),
342 version_string=f"{PY_VERSION.major}.{PY_VERSION.minor}.{PY_VERSION.micro}",
343 tier=tier,
344 **build_feats,
345 **lang_feats,
346 experimental_enabled=experimental,
347 )
349 _cached_features = features
351 # Log detected features at DEBUG level
352 logger.debug(
353 "Python %s detected (tier=%s, experimental=%s): %r",
354 features.version_string,
355 tier.value,
356 experimental,
357 features,
358 )
360 return features
363def get_python_info() -> dict[str, object]:
364 """Get comprehensive Python runtime information.
366 Returns:
367 Dictionary with version, platform, and feature information.
369 """
370 features = get_features()
372 return {
373 "version": features.version_string,
374 "version_tuple": features.version,
375 "tier": features.tier.value,
376 "implementation": platform.python_implementation(),
377 "platform": platform.platform(),
378 "compiler": platform.python_compiler(),
379 "features": features.to_dict(),
380 "optimization_level": get_optimization_level(),
381 }