#!/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