Source code for scitex_compat._compat

#!/usr/bin/env python3
# File: src/scitex_compat/_compat.py

"""Core deprecation decorator and compat shims.

Implementation of the public surface re-exported from
``scitex_compat.__init__`` — kept in its own module so the test suite can
mirror it as ``tests/scitex_compat/test__compat.py``.
"""

from __future__ import annotations

import functools
import importlib
import warnings


[docs] def deprecated(reason: str | None = None, forward_to: str | None = None): """Mark a function as deprecated, optionally forwarding calls to its replacement. Canonical SSOT for the ecosystem (see SOC.md). Pure stdlib — safe for any Layer 0 leaf to import without dragging numpy / scitex-decorators. Parameters ---------- reason : str, optional Human-readable explanation of why the function was deprecated. forward_to : str, optional Dotted module path to the replacement, e.g. ``"..session.start"`` or ``"scitex.session.start"``. When provided, calls are forwarded to the new function (via ``importlib``) and the wrapper's docstring is auto-generated by combining a deprecation notice with the target's docstring. Relative paths starting with ``..`` are resolved against the decorated function's ``__module__``. """ def decorator(func): if forward_to: # Forwarding wrapper: emit warning then call the new target. # Capture into a non-Optional local so closures see a str. _forward_to: str = forward_to @functools.wraps(func) def _forwarding_wrapper(*args, **kwargs): warnings.warn( f"{func.__name__} is deprecated: {reason}", DeprecationWarning, stacklevel=2, ) module_path, function_name = _forward_to.rsplit(".", 1) if module_path.startswith(".."): func_module = func.__module__ if func_module: package_parts = func_module.split(".") level_count = 0 for char in module_path: if char == ".": level_count += 1 else: break if level_count > 0: base_package_parts = package_parts[:-level_count] if base_package_parts: base_package = ".".join(base_package_parts) relative_part = module_path.lstrip(".") module_path = ( base_package + "." + relative_part if relative_part else base_package ) else: module_path = module_path.lstrip(".") try: target_module = importlib.import_module(module_path) target_function = getattr(target_module, function_name) return target_function(*args, **kwargs) except (ImportError, AttributeError) as e: warnings.warn( f"Failed to forward {func.__name__} to {forward_to}: {e}. " f"Using original deprecated implementation.", RuntimeWarning, stacklevel=2, ) return func(*args, **kwargs) original_name = func.__name__ new_location = forward_to.replace("..", "scitex.").lstrip(".") target_docstring = "" try: target_module_path, target_function_name = forward_to.rsplit(".", 1) if target_module_path.startswith(".."): func_module = func.__module__ if func_module: package_parts = func_module.split(".") level_count = 0 for char in target_module_path: if char == ".": level_count += 1 else: break if level_count > 0: base_package_parts = package_parts[:-level_count] if base_package_parts: base_package = ".".join(base_package_parts) relative_part = target_module_path.lstrip(".") target_module_path = ( base_package + "." + relative_part if relative_part else base_package ) else: target_module_path = target_module_path.lstrip(".") target_module = importlib.import_module(target_module_path) target_function = getattr(target_module, target_function_name) if target_function.__doc__: target_docstring = target_function.__doc__.strip() except (ImportError, AttributeError): pass if target_docstring: forwarding_docstring = ( f"**DEPRECATED: Use {new_location} instead**\n\n" f"{target_docstring}\n\n" f"Deprecation Notice\n------------------\n" f"This function is deprecated and will be removed in a future " f"version. Use `{new_location}` instead. This wrapper forwards " f"all calls to the new function while displaying a deprecation " f"warning." ) else: forwarding_docstring = ( f"**DEPRECATED: Use {new_location} instead**\n\n" f"Backward-compat shim for {original_name}(). Forwards all " f"calls to {new_location} while displaying a deprecation " f"warning." ) _forwarding_wrapper.__doc__ = forwarding_docstring return _forwarding_wrapper else: # No forwarding: just emit the warning then call original. @functools.wraps(func) def _warning_wrapper(*args, **kwargs): warnings.warn( f"{func.__name__} is deprecated: {reason}", DeprecationWarning, stacklevel=2, ) return func(*args, **kwargs) return _warning_wrapper return decorator
[docs] def notify(*args, **kwargs): """Deprecated: Use scitex.notify.alert() instead. In standalone mode, this only emits a deprecation warning. The actual notification requires scitex.notify to be installed. """ warnings.warn( "scitex.compat.notify is deprecated. Use scitex.notify.alert instead.", DeprecationWarning, stacklevel=2, ) try: from scitex.notify import alert return alert(*args, **kwargs) except ImportError: warnings.warn( "scitex.notify is not installed. Notification not sent.", RuntimeWarning, stacklevel=2, ) return None
[docs] async def notify_async(*args, **kwargs): """Deprecated: Use scitex.notify.alert_async() instead. In standalone mode, this only emits a deprecation warning. The actual notification requires scitex.notify to be installed. """ warnings.warn( "scitex.compat.notify_async is deprecated. Use scitex.notify.alert_async instead.", DeprecationWarning, stacklevel=2, ) try: from scitex.notify import alert_async return await alert_async(*args, **kwargs) except ImportError: warnings.warn( "scitex.notify is not installed. Notification not sent.", RuntimeWarning, stacklevel=2, ) return None
__all__ = ["deprecated", "notify", "notify_async"] # EOF