mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
fix(tests): qualify traced function names on Python 3.10
This commit is contained in:
parent
32f71950ec
commit
f9f32b49a6
2 changed files with 80 additions and 8 deletions
|
|
@ -2,11 +2,12 @@ from __future__ import annotations
|
|||
|
||||
import sys
|
||||
import threading
|
||||
from collections.abc import Generator, Sequence
|
||||
from collections.abc import Generator, Iterator, Mapping, Sequence
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from types import CodeType, FrameType, FunctionType
|
||||
from types import CodeType, FrameType, FunctionType, MappingProxyType
|
||||
from typing import Final
|
||||
|
||||
|
||||
|
|
@ -27,11 +28,11 @@ class PythonProfiler:
|
|||
def __call__(self, frame: FrameType, event: str, _arg: object) -> None:
|
||||
if event != "call" or frame in self._seen_frames:
|
||||
return
|
||||
function_name: Final = self.function_name(frame.f_code)
|
||||
function_name: Final = self.function_name(frame)
|
||||
if function_name is None:
|
||||
return
|
||||
ancestors: Final = tuple(
|
||||
name for ancestor in _frame_ancestors(frame) if (name := self.function_name(ancestor.f_code)) is not None
|
||||
name for ancestor in _frame_ancestors(frame) if (name := self.function_name(ancestor)) is not None
|
||||
)
|
||||
self._seen_frames.add(frame)
|
||||
self.events.append(
|
||||
|
|
@ -42,13 +43,57 @@ class PythonProfiler:
|
|||
)
|
||||
)
|
||||
|
||||
def function_name(self, code: CodeType) -> str | None:
|
||||
def function_name(self, frame: FrameType) -> str | None:
|
||||
code: Final = frame.f_code
|
||||
if self._source_root is None:
|
||||
return self._names_by_code.get(code)
|
||||
if not code.co_filename.startswith(self._source_root):
|
||||
return None
|
||||
relative: Final = code.co_filename.removeprefix(self._source_root)
|
||||
return f"{relative}:{code.co_firstlineno} {getattr(code, 'co_qualname', code.co_name)}"
|
||||
return f"{relative}:{code.co_firstlineno} {_qualified_name(frame)}"
|
||||
|
||||
|
||||
def _qualified_name(frame: FrameType) -> str:
|
||||
code: Final = frame.f_code
|
||||
native: Final = getattr(code, "co_qualname", None)
|
||||
if isinstance(native, str):
|
||||
return native
|
||||
module_name: Final = frame.f_globals.get("__name__")
|
||||
if not isinstance(module_name, str):
|
||||
return code.co_name
|
||||
return _module_qualnames(module_name).get(code, code.co_name)
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _module_qualnames(module_name: str) -> Mapping[CodeType, str]:
|
||||
module: Final = sys.modules.get(module_name)
|
||||
if module is None:
|
||||
return MappingProxyType({})
|
||||
return MappingProxyType(dict(_declared_functions(vars(module), frozenset())))
|
||||
|
||||
|
||||
def _declared_functions(namespace: Mapping[str, object], visited: frozenset[int]) -> Iterator[tuple[CodeType, str]]:
|
||||
for attribute in tuple(namespace.values()):
|
||||
for value in _accessors(attribute):
|
||||
if isinstance(value, FunctionType):
|
||||
yield from ((wrapped.__code__, wrapped.__qualname__) for wrapped in _unwrapped(value))
|
||||
elif isinstance(value, type) and id(value) not in visited:
|
||||
yield from _declared_functions(dict(vars(value)), visited | {id(value)})
|
||||
|
||||
|
||||
def _unwrapped(function: FunctionType) -> Iterator[FunctionType]:
|
||||
yield function
|
||||
inner: Final = getattr(function, "__wrapped__", None)
|
||||
if isinstance(inner, FunctionType):
|
||||
yield from _unwrapped(inner)
|
||||
|
||||
|
||||
def _accessors(value: object) -> tuple[object, ...]:
|
||||
if isinstance(value, (staticmethod, classmethod)):
|
||||
return (value.__func__,)
|
||||
if isinstance(value, property):
|
||||
return tuple(accessor for accessor in (value.fget, value.fset, value.fdel) if accessor is not None)
|
||||
return (value,)
|
||||
|
||||
|
||||
def _frame_ancestors(frame: FrameType) -> Generator[FrameType]:
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ from __future__ import annotations
|
|||
|
||||
import asyncio
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
from types import FunctionType
|
||||
from typing import Final, cast
|
||||
from typing import Final, ParamSpec, TypeVar, cast
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -14,7 +16,18 @@ from tests.sdk_function_trace import (
|
|||
TraceStep,
|
||||
assert_function_trace_parity,
|
||||
)
|
||||
from tests.sdk_function_trace.profiler import profile_python
|
||||
from tests.sdk_function_trace.profiler import _module_qualnames, profile_python
|
||||
|
||||
_P = ParamSpec("_P")
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
def _passthrough(function: Callable[_P, _T]) -> Callable[_P, _T]:
|
||||
@wraps(function)
|
||||
def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _T:
|
||||
return function(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class First:
|
||||
|
|
@ -29,6 +42,20 @@ class Second:
|
|||
return None
|
||||
|
||||
|
||||
class Decorated:
|
||||
@_passthrough
|
||||
def call(self) -> None:
|
||||
return None
|
||||
|
||||
|
||||
def test_source_profiler_qualifies_decorated_methods_by_class() -> None:
|
||||
with profile_python(source_root=Path(__file__).parent) as profiler:
|
||||
Decorated().call()
|
||||
|
||||
assert any(event.function.endswith(" Decorated.call") for event in profiler.events)
|
||||
assert _module_qualnames(__name__)[cast(FunctionType, Decorated.call.__wrapped__).__code__] == "Decorated.call"
|
||||
|
||||
|
||||
def test_profiler_matches_code_objects_and_keeps_repeated_calls() -> None:
|
||||
with profile_python((First.run,)) as profiler:
|
||||
Second.run()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue