# File: src/fitzzftw/patch/future_314_ge.py
# Author: Fitzz TeXnik Welt
# Email: FitzzTeXnikWelt@t-online.de
# License: LGPLv2 or above
"""
Modern Protocol Introspection (Python 3.14+)
=============================================
This module implements metadata extraction for Protocols leveraging PEP 649
(Deferred Evaluation) and the 'annotationlib' library.
It provides high-performance introspection by utilizing structural pattern
matching for type normalization and 'Format.VALUE' for robust type resolution.
Note:
For Python 3.12/13, use :mod:`.current_312_313`.
For Python 3.11, use :mod:`.legacy_311`.
"""
import types
import typing
from collections.abc import Callable
from inspect import Signature, isfunction, ismethod, signature
from pathlib import Path
from typing import Any
import annotationlib
from annotationlib import Format
[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)
# def _build_sig_from_annos(self, obj) -> str:
# """
# Baut die Signatur NUR mit Bordmitteln der annotationlib.
# """
# # Format.VALUE liefert die echten Typ-Objekte (PEP 649)
# annos = annotationlib.get_annotations(obj, format=Format.VALUE)
# params = ["self"]
# for name, val in annos.items():
# if name == "return":
# continue
# # Wir nutzen direkt type_repr aus der Lib
# params.append(f"{name}: {type_repr(val)}")
# ret_val = annos.get("return", type(None))
# return f"({', '.join(params)}) -> {type_repr(ret_val)}"
def _get_type_str(self, v: Any) -> str:
"""
Normalizes type objects into human-readable strings using PEP 634 matching.
This method resolves Unions (including the | operator), Generics, and
standard types into their canonical string names. It is designed to
work with the deferred evaluation objects returned by 'annotationlib'.
:param v: The type or annotation object to normalize.
:return: A string representation (e.g., "int | str", "list", or "None").
"""
# 1. Strukturelle Analyse über den Origin (Generics/Unions)
match typing.get_origin(v):
case typing.Union | types.UnionType:
# Rekursive Auflösung der Union-Member für die Pipe-Syntax
return " | ".join(self._get_type_str(a) for a in typing.get_args(v))
case type() as origin:
# Extrahiert den Namen von Standard-Generics (dict, list, etc.)
return origin.__name__
# 2. Wert-Analyse für Einzeltypen und Konstanten
match v:
case types.NoneType:
return "None"
case typing.Any:
return "Any"
case type() as t:
return t.__name__
case _:
# Fallback für alles, was keinem bekannten Muster entspricht
return str(v)
[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.
"""
# print(protocol.__non_callable_proto_members__)
self._name: str = protocol.__name__
# Direktes Mapping der Type Hints zu Strings via type_repr
# get_annotations(..., format=Format.VALUE) übernimmt das 'get_type_hints'
raw_hints = annotationlib.get_annotations(protocol, format=Format.VALUE)
if not raw_hints:
raw_hints = typing.get_type_hints(protocol, include_extras=True)
# print(raw_hints)
# print(dir(protocol))
# print((protocol.__dict__))
# print(dir(protocol.__annotate_func__))
# print("T",typing.get_type_hints(protocol, include_extras=True))
self._annotations: dict[str, str] = {
k: self._get_type_str(v) for k, v in raw_hints.items()
}
proto_dict = protocol.__dict__
# Protocol-Metadaten
self._attributes: set[str] = protocol.__dict__.get("__protocol_attrs__", set())
# self._non_callable: set[str] = getattr(protocol, "__non_callable_proto_members__", set())
self._non_callable: set[str] = protocol.__non_callable_proto_members__
self._callable: set[str] = self._attributes - self._non_callable
# print("an", self._annotations)
# print("nC", self._non_callable)
# print("attr", self._attributes)
# print("C", self._callable)
# Signaturen
self._signatures: dict[str, str] = {}
for call in sorted(self._callable):
member = proto_dict[call]
m_annos = annotationlib.get_annotations(member, format=Format.VALUE)
# Parameter-Liste bauen
params = ["self"]
for name, val in m_annos.items():
if name == "return":
continue
# Hier nutzen wir die Methode für jeden Parameter
params.append(f"{name}: {self._get_type_str(val)}")
# Rückgabewert normalisieren
ret_val = m_annos.get("return", types.NoneType)
ret_str = self._get_type_str(ret_val)
self._signatures[call] = f"({', '.join(params)}) -> {ret_str}"
@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)."""
# print(self._name, self._annotations)
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 {}
# 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] = protocol.__dict__["__protocol_attrs__"]
# self._non_callable: set[str] = protocol.__non_callable_proto_members__
# self._callable: set[str] = self._attributes - self._non_callable
# self._signatures: dict[str, str] = {}
# for call in sorted(self._callable):
# self._signatures[call] = str(signature(protocol.__dict__[call]))
[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_future_314_ge.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.")