Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions ddtrace/debugging/_function/discovery.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from collections import defaultdict
from collections import deque
from pathlib import Path

from wrapt import FunctionWrapper

Expand Down Expand Up @@ -32,6 +31,7 @@
from ddtrace.internal.utils.inspection import collect_code_objects
from ddtrace.internal.utils.inspection import functions_for_code
from ddtrace.internal.utils.inspection import linenos
from ddtrace.internal.utils.inspection import resolved_code_origin


log = get_logger(__name__)
Expand Down Expand Up @@ -216,7 +216,7 @@ def _collect_functions(module: ModuleType) -> Dict[str, _FunctionCodePair]:

for name in (k, local_name) if isinstance(k, str) and k != local_name else (local_name,):
fullname = ".".join((c.__fullname__, name)) if c.__fullname__ else name
if fullname not in functions or Path(code.co_filename).resolve() == path:
if fullname not in functions or resolved_code_origin(code) == path:
# Give precedence to code objects from the module and
# try to retrieve any potentially decorated function so
# that we don't end up returning the decorator function
Expand Down Expand Up @@ -292,7 +292,7 @@ def __init__(self, module: ModuleType) -> None:

if (
function not in seen_functions
and Path(cast(FunctionType, function).__code__.co_filename).resolve() == module_path
and resolved_code_origin(cast(FunctionType, function).__code__) == module_path
):
# We only map line numbers for functions that actually belong to
# the module.
Expand Down Expand Up @@ -349,7 +349,7 @@ def _resolve_pair(self, pair: _FunctionCodePair, fullname: str) -> FullyNamedFun

code = pair.code
assert code is not None # nosec
f = undecorated(cast(FunctionType, target), cast(str, part), Path(code.co_filename).resolve())
f = undecorated(cast(FunctionType, target), cast(str, part), resolved_code_origin(code))
if not (isinstance(f, FunctionType) and f.__code__ is code):
raise e

Expand Down
2 changes: 1 addition & 1 deletion ddtrace/debugging/_origin/span.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ def from_frame(cls, frame: FrameType) -> "ExitSpanProbe":
ExitSpanProbe,
cls.build(
name=code.co_qualname if sys.version_info >= (3, 11) else code.co_name, # type: ignore[attr-defined]
filename=str(Path(code.co_filename).resolve()),
filename=str(Path(code.co_filename)),
line=frame.f_lineno,
),
)
Expand Down
3 changes: 2 additions & 1 deletion ddtrace/internal/coverage/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from ddtrace.internal.packages import purelib_path
from ddtrace.internal.packages import stdlib_path
from ddtrace.internal.test_visibility.coverage_lines import CoverageLines
from ddtrace.internal.utils.inspection import resolved_code_origin


log = get_logger(__name__)
Expand Down Expand Up @@ -317,7 +318,7 @@ def transform(self, code: CodeType, _module: ModuleType) -> CodeType:
if _module is None:
return code

code_path = Path(code.co_filename).resolve()
code_path = resolved_code_origin(code)

if not any(code_path.is_relative_to(include_path) for include_path in self._include_paths):
# Not a code object we want to instrument
Expand Down
33 changes: 22 additions & 11 deletions ddtrace/internal/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,21 +111,32 @@ def unregister_post_run_module_hook(hook: ModuleHookType) -> None:
def origin(module: ModuleType) -> t.Optional[Path]:
"""Get the origin source file of the module."""
try:
# DEV: Use object.__getattribute__ to avoid potential side-effects.
orig = Path(object.__getattribute__(module, "__file__")).resolve()
except (AttributeError, TypeError):
# Module is probably only partially initialised, so we look at its
# spec instead
return module.__dd_origin__
except AttributeError:
try:
# DEV: Use object.__getattribute__ to avoid potential side-effects.
orig = Path(object.__getattribute__(module, "__spec__").origin).resolve()
except (AttributeError, ValueError, TypeError):
orig = None
orig = Path(object.__getattribute__(module, "__file__")).resolve()
except (AttributeError, TypeError):
# Module is probably only partially initialised, so we look at its
# spec instead
try:
# DEV: Use object.__getattribute__ to avoid potential side-effects.
orig = Path(object.__getattribute__(module, "__spec__").origin).resolve()
except (AttributeError, ValueError, TypeError):
orig = None

if orig is not None and orig.is_file():
return orig.with_suffix(".py") if orig.suffix == ".pyc" else orig
if orig is not None and orig.suffix == "pyc":
orig = orig.with_suffix(".py")

return None
if orig is not None:
# If we failed to find a valid origin we don't cache the value and
# try again the next time.
try:
module.__dd_origin__ = orig # type: ignore[attr-defined]
except AttributeError:
pass

return orig


def _resolve(path: Path) -> t.Optional[Path]:
Expand Down
9 changes: 7 additions & 2 deletions ddtrace/internal/packages.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ def _effective_root(rel_path: Path, parent: Path) -> str:
return base if root.is_dir() and (root / "__init__.py").exists() else "/".join(rel_path.parts[:2])


RESOLVED_SYS_PATH = [Path(_).resolve() for _ in sys.path]


def _root_module(path: Path) -> str:
# Try the most likely prefixes first
for parent_path in (purelib_path, platlib_path):
Expand All @@ -112,7 +115,7 @@ def _root_module(path: Path) -> str:
# Try to resolve the root module using sys.path. We keep the shortest
# relative path as the one more likely to give us the root module.
min_relative_path = max_parent_path = None
for parent_path in (Path(_).resolve() for _ in sys.path):
for parent_path in RESOLVED_SYS_PATH:
try:
relative = path.relative_to(parent_path)
if min_relative_path is None or len(relative.parents) < len(min_relative_path.parents):
Expand Down Expand Up @@ -240,7 +243,9 @@ def module_to_package(module: ModuleType) -> t.Optional[Distribution]:

@cached(maxsize=256)
def is_stdlib(path: Path) -> bool:
rpath = path.resolve()
rpath = path
if not rpath.is_absolute() or rpath.is_symlink():
rpath = rpath.resolve()

return (rpath.is_relative_to(stdlib_path) or rpath.is_relative_to(platstdlib_path)) and not (
rpath.is_relative_to(purelib_path) or rpath.is_relative_to(platlib_path)
Expand Down
5 changes: 3 additions & 2 deletions ddtrace/internal/symbol_db/symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from ddtrace.internal.utils.http import connector
from ddtrace.internal.utils.http import multipart
from ddtrace.internal.utils.inspection import linenos
from ddtrace.internal.utils.inspection import resolved_code_origin
from ddtrace.internal.utils.inspection import undecorated
from ddtrace.settings._agent import config as agent_config
from ddtrace.settings.symbol_db import config as symdb_config
Expand Down Expand Up @@ -323,7 +324,7 @@ def _(cls, code: CodeType, data: ScopeData, recursive: bool = True):
return None
data.seen.add(code_id)

if Path(code.co_filename).resolve() != data.origin:
if (code_origin := resolved_code_origin(code)) != data.origin:
# Comes from another module.
return None

Expand All @@ -337,7 +338,7 @@ def _(cls, code: CodeType, data: ScopeData, recursive: bool = True):
return Scope(
scope_type=ScopeType.CLOSURE, # DEV: Not in the sense of a Python closure.
name=code.co_name,
source_file=str(Path(code.co_filename).resolve()),
source_file=str(code_origin),
start_line=start_line,
end_line=end_line,
symbols=Symbol.from_code(code),
Expand Down
12 changes: 11 additions & 1 deletion ddtrace/internal/utils/inspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from typing import cast

from ddtrace.internal.safety import _isinstance
from ddtrace.internal.utils.cache import cached


@singledispatch
Expand All @@ -31,14 +32,23 @@ def _(f: FunctionType) -> Set[int]:
return linenos(f.__code__)


@cached(maxsize=4 << 10)
def _filename_to_resolved_path(filename: str) -> Path:
return Path(filename).resolve()


def resolved_code_origin(code: CodeType) -> Path:
return _filename_to_resolved_path(code.co_filename)


def undecorated(f: FunctionType, name: str, path: Path) -> FunctionType:
# Find the original function object from a decorated function. We use the
# expected function name to guide the search and pick the correct function.
# The recursion is needed in case of multiple decorators. We make it BFS
# to find the function as soon as possible.

def match(g):
return g.__code__.co_name == name and Path(g.__code__.co_filename).resolve() == path
return g.__code__.co_name == name and resolved_code_origin(g.__code__) == path

if _isinstance(f, FunctionType) and match(f):
return f
Expand Down
Loading