Source code for fitzzftw.patch.legacy_311

# File: src/fitzzftw/patch/legacy_311.py
# Author: Fitzz TeXnik Welt
# Email: FitzzTeXnikWelt@t-online.de
# License: LGPLv2 or above
"""
legacy_311
===============================


Legacy Introspection Support for Python 3.11.

This module provides specialized tools to extract function signatures from
Protocols and Callables where the standard `inspect` module fails in
Python versions < 3.12 (specifically on Debian 12 LTS environments).

Note:
    For Python 3.12/13, use :mod:`.current_312_313`.
    For Python 3.14+, use :mod:`.future_314_ge`.

"""

from copy import deepcopy
from inspect import Signature, isfunction, ismethod, signature
from pathlib import Path
from typing import Any, Callable, get_type_hints

from fitzzftw.patch.utils import get_string_of_type


[docs] def sig2str(meth:Callable) -> str: """ Generates a human-readable signature string from a callable's annotations. Unlike `inspect.signature`, this function directly accesses `__annotations__` to avoid `TypeError` and `ValueError` often encountered when inspecting `runtime_checkable` Protocols in Python 3.11. :param meth: The method or function to inspect. :returns: A formatted signature string, e.g., "(self, a: int, b: list[str] | None)-> str". Returns "(<not available>)" if introspection fails. """ try: param: dict[str, type] = deepcopy(meth.__annotations__) start_str="(self" return_ = param.pop("return") ret_str = ") -> "+str(return_.__name__ if return_ is not None else "None") args= ", ".join([f"{k}: {v.__name__ if hasattr(v,'__name__') else v}" for k,v in param.items()]) if args: args = "".join([", ",args]) return "".join([start_str,args, ret_str]) except Exception: return "(<not availible>)"
[docs] class FtwProtocolWrap: """ A metadata wrapper for Python Protocol classes. This class introspects a given Protocol to extract its structural definition, including type hints, mandatory attributes, and method signatures. It serves as a bridge for documentation or validation tools that need to inspect protocol requirements without interacting with the original class directly. Attributes: _name (str): The name of the wrapped protocol. _annotations (dict): Mapping of attribute/method names to their type hint strings. _attributes (set): All members defined as part of the protocol. _non_callable (set): Protocol members that are data attributes. _callable (set): Protocol members that are methods. _signatures (dict): Mapping of method names to their stringified inspect.Signature. """
[docs] def __init__(self, protocol=None) -> None: """ Initializes the wrapper. If a protocol is provided, it is processed immediately. :param protocol: The Protocol class to inspect. """ if protocol: self.set_new_protocol(protocol)
[docs] def set_new_protocol(self, protocol) -> None: """ Introspects the given protocol and populates all metadata fields. This method extracts: 1. Type hints for all members. 2. Required protocol attributes via '__protocol_attrs__'. 3. Separation of callable (methods) and non-callable (data) members. 4. Signatures for all callable members. :param protocol: The Protocol class to wrap. """ self._name: str = protocol.__name__ self._annotations: dict[str, Any] = { k: get_string_of_type(v) for (k, v) in get_type_hints(protocol).items() } self._attributes: set[str] = set(self._annotations.keys()) # print(f"{sorted(self._attributes)=}") self._non_callable: set[str] = set(self._annotations.keys()) self._callable: set[str] = set( [item for item in protocol.__dict__.keys() if not item.startswith("_")] ) self._attributes |= self._callable self._signatures: dict[str, str] = {} for call in sorted(self._callable): self._signatures[call] = str(sig2str(protocol.__dict__[call]))
@property def name(self) -> str: """The name of the protocol class (ro).""" return self._name or "" @property def non_callable(self) -> set[str]: """Set of names of all non-method members (ro).""" return self._non_callable or set() @property def callable(self) -> set[str]: """Set of names of all method members (ro).""" return self._callable or set() @property def annotations(self) -> dict[str, Any]: """Dictionary of member names and their stringified type hints (ro).""" return self._annotations or {} @property def attributes(self) -> set[str]: """Set of all member names required by the protocol (ro).""" return self._attributes or set() @property def signatures(self) -> dict[str, str]: """Dictionary mapping method names to their full call signatures (ro).""" return self._signatures or {}
[docs] class FtwMethFuncWrap: """ A metadata wrapper for callables (functions and methods). This class inspects a given callable to determine its nature (function vs. method) and extracts its signature and qualified name. It provides a consistent string representation that is particularly useful for generating error messages or documentation. Attributes: _len (int): Internal flag indicating if the wrapper is empty (0) or populated (1). _signature (Signature): The inspect.Signature object of the callable. _name (str): The qualified name (__qualname__) of the callable. _is_methode (bool): True if the callable is identified as a method. _is_function (bool): True if the callable is identified as a pure function. """
[docs] def __init__(self, meth_func: Callable | None = None) -> None: """ Initializes the wrapper and introspects the provided callable. The inspector handles edge cases where methods might be identified as functions by checking for dots in the qualified name. :param meth_func: The function or method to wrap. """ if meth_func is None: self._len: int = 0 self._name: str = "" self._is_methode: bool = False self._is_function: bool = False return self._len: int = 1 self._signature: Signature = signature(meth_func) self._name: str = meth_func.__qualname__ self._is_methode: bool = ismethod(meth_func) self._is_function: bool = isfunction(meth_func) if self._is_function and "." in self._name: self._is_methode = True self._is_function = False
@property def is_methode(self) -> bool: """True if the wrapped object is a method (ro).""" return self._is_methode @property def is_function(self) -> bool: """True if the wrapped object is a function (ro).""" return self._is_function @property def is_empty(self) -> bool: """True if no callable was provided during initialization (ro).""" return self._len <= 0 @property def name(self) -> str: """The qualified name of the callable (ro).""" return self._name def __len__(self) -> int: """Returns 1 if a callable is wrapped, 0 otherwise.""" return self._len def __str__(self) -> str: """ Returns a formatted string of the callable including its name and signature. For methods, it ensures that 'self' is visible in the signature string to accurately represent class-bound callables. """ if self.is_empty: return str(None) sig: str = str(self._signature) if self._is_methode: if not sig.startswith("(self"): # if not len(self._signature.parameters): # print("treffer") # sig = "".join(["(self", sig.lstrip("(")]) # else: sig = ", ".join(["(self", sig.lstrip("(")]) return f"{self._name}{sig}"
if __name__ == "__main__": # pragma: no cover from doctest import FAIL_FAST, testfile be_verbose = False be_verbose = True option_flags = 0 option_flags = FAIL_FAST test_sum = 0 test_failed = 0 # Pfad zu den dokumentierenden Tests testfiles_dir = Path(__file__).parents[3] / "doc/source/devel" test_file = testfiles_dir / "get_started_legacy_311.rst" if test_file.exists(): print(f"--- Running Doctest for {test_file.name} ---") doctestresult = testfile( str(test_file), module_relative=False, verbose=be_verbose, optionflags=option_flags, ) test_failed += doctestresult.failed test_sum += doctestresult.attempted if test_failed == 0: print(f"\nDocTests passed without errors, {test_sum} tests.") else: print(f"\nDocTests failed: {test_failed} tests.") else: print(f"⚠️ Warning: Test file {test_file.name} not found.")