Skip to content
Closed
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
86 changes: 86 additions & 0 deletions .claude/skills/type-annotate/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
---
name: type-annotate
description: >
Add complete type annotations to new or modified Python code in dd-trace-py.
Use when writing new Python modules, when the user asks for type hints/annotations,
or before committing production or test Python changes. Covers function signatures,
locals, globals, class attributes, and validation via the lint skill.
allowed-tools:
- Bash
- Read
- Grep
- Glob
- Edit
---

# Type-annotate Python (dd-trace-py)

## When to use

- New Python files or functions in PRs
- User asks to "type-annotate", "add hints", or "fix typing"
- After implementing a feature, before commit

## Standards (match existing ddtrace code)

1. **Import style** — separate `typing` imports per symbol (see `ddtrace/internal/excepthook.py`), not `from typing import *`.
2. **`from __future__ import annotations`** — use only when needed for forward refs; prefer quoted strings or `TYPE_CHECKING` blocks.
3. **Annotate everything new** in production code:
- Function/method parameters and return types
- Module-level globals (`_tool_id: Optional[int] = None`)
- Class attributes when not inferred from `__init__`
- Nested functions and closures used in hot paths when practical
4. **Tests** — annotate fixtures (`-> Iterator[...]`), helper classes, and callbacks; `pytest` fixtures return typed generators.
5. **Prefer precise types** — `CodeType`, `Callable[[...], T]`, `Optional[T]`, `dict[str, Any]`; use `object` over `Any` when any object is accepted.
6. **Version gates** — keep `if sys.version_info >= (3, 15):` blocks typed inside the branch.

## Workflow

```bash
# 1. See what changed in the PR slice
git diff <parent>..<branch> -- '*.py'

# 2. Edit files — add annotations to every new/changed def, global, class attr

# 3. Format + type-check only touched files (never raw mypy/ruff)
scripts/lint fmt -- path/to/file.py
scripts/lint typing -- path/to/file.py

# 4. Commit on the PR branch (one-liner message)
git commit -m "typing: annotate <scope>"
git push origin <branch>
```

For stacked PRs, work **bottom-up**: annotate PR N, push, rebase PR N+1 onto N, repeat.

## Common patterns

```python
# Module global
_active: dict[str, int] = {}

# Callback
def _on_event(code: CodeType, offset: int) -> Optional[object]:
...

# Fixture
@pytest.fixture()
def registered() -> Iterator[Callable[[CodeType, MonitoringEventHandler], None]]:
...

# TYPE_CHECKING for import cycles
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ddtrace.profiling.collector import Collector
```

## Do not

- Run `mypy` or `ruff` directly — use `scripts/lint`
- Add `# type: ignore` unless mypy requires it and a comment explains why
- Change runtime behavior while adding types

## Related

- Format/validate: `.claude/skills/lint/SKILL.md`
- Test changes: `.claude/skills/run-tests/SKILL.md`
5 changes: 5 additions & 0 deletions .github/workflows/generate-package-versions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ jobs:
with:
python-version: "3.14"

- name: Setup Python 3.15
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.15"

- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0

Expand Down
16 changes: 16 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,22 @@ profiling_native:
- PYTHON_VERSION: ["3.12", "3.14"]
SANITIZER: ["valgrind"]

# AIDEV-TODO(py315): fold 3.15 back into the profiling_native matrix once the
# dd/images/dd-trace-py/profiling_native base image installs a Python 3.15
# pyenv version. Today it only carries 3.9–3.14, so PYENV_VERSION=3.15 fails
# with "pyenv: version `3.15' is not installed". allow_failure keeps the
# pipeline green while the upstream image catches up; delete this job and
# re-add "3.15" to the PYTHON_VERSION arrays above once the image is updated.
profiling_native_py315:
extends: .profiling_native_base
allow_failure: true
retry: 2
rules: !reference [profiling_native, rules]
parallel:
matrix:
- PYTHON_VERSION: ["3.15"]
SANITIZER: ["safety", "thread", "", "valgrind"]

test-dd-sts:
stage: tests
needs: []
Expand Down
2 changes: 2 additions & 0 deletions .gitlab/templates/build-base-venvs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@ build_base_venvs:
- core.*
- ddtrace/**/*.so*
- .riot/venv_*
- ddtrace/internal/datadog/profiling/test/test_*
- ddtrace/internal/datadog/profiling/test/py315/test_*
34 changes: 34 additions & 0 deletions .riot/requirements/1c6cb02.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#
# This file is autogenerated by pip-compile with Python 3.15
# by the following command:
#
# pip-compile --allow-unsafe --no-annotate .riot/requirements/1c6cb02.in
#
attrs==26.1.0
coverage[toml]==7.13.5
gunicorn==25.3.0
hypothesis==6.45.0
iniconfig==2.3.0
jsonschema==4.26.0
jsonschema-specifications==2025.9.1
mock==5.2.0
numpy==2.4.4
opentracing==2.4.0
packaging==26.1
pluggy==1.6.0
protobuf==7.34.1
py-cpuinfo==8.0.0
pygments==2.20.0
pytest==9.0.3
pytest-asyncio==0.21.1
pytest-benchmark==5.2.3
pytest-cov==7.1.0
pytest-cpp==2.6.0
pytest-mock==3.15.1
pytest-randomly==4.1.0
referencing==0.37.0
rpds-py==0.30.0
sortedcontainers==2.4.0
uvloop==0.22.1
uwsgi==2.0.31
zstandard==0.25.0
45 changes: 45 additions & 0 deletions .riot/requirements/222bcd0.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#
# This file is autogenerated by pip-compile with Python 3.15
# by the following command:
#
<<<<<<<< HEAD:.riot/requirements/222bcd0.txt
# pip-compile --allow-unsafe --no-annotate .riot/requirements/222bcd0.in
#
attrs==26.1.0
cloudpickle==3.1.2
coverage[toml]==7.14.3
execnet==2.1.2
gevent==26.5.0
greenlet==3.5.3
httpretty==1.1.4
========
# pip-compile --allow-unsafe --no-annotate .riot/requirements/1857594.in
#
attrs==26.1.0
coverage[toml]==7.13.5
>>>>>>>> 49c1ffeaab (ci(profiling): wire py3.15 into build matrix, riotfile, and CI):.riot/requirements/1857594.txt
hypothesis==6.45.0
iniconfig==2.3.0
mock==5.2.0
opentracing==2.4.0
packaging==26.1
pluggy==1.6.0
pyfakefs==6.2.0
pygments==2.20.0
pytest==8.4.2
pytest-asyncio==0.23.8
pytest-cov==7.1.0
pytest-mock==3.15.1
pytest-randomly==4.1.0
python-json-logger==2.0.7
sortedcontainers==2.4.0
<<<<<<<< HEAD:.riot/requirements/222bcd0.txt
uwsgi==2.0.31
wrapt==2.2.2
========
>>>>>>>> 49c1ffeaab (ci(profiling): wire py3.15 into build matrix, riotfile, and CI):.riot/requirements/1857594.txt
zope-event==5.0
zope-interface==7.2

# The following packages are considered to be unsafe in a requirements file:
setuptools==81.0.0
33 changes: 33 additions & 0 deletions .riot/requirements/95077af.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#
# This file is autogenerated by pip-compile with Python 3.15
# by the following command:
#
# pip-compile --allow-unsafe --no-annotate .riot/requirements/95077af.in
#
attrs==26.1.0
coverage[toml]==7.13.5
gunicorn==25.3.0
hypothesis==6.45.0
iniconfig==2.3.0
jsonschema==4.26.0
jsonschema-specifications==2025.9.1
mock==5.2.0
numpy==2.4.4
opentracing==2.4.0
packaging==26.1
pluggy==1.6.0
protobuf==7.34.1
py-cpuinfo==8.0.0
pygments==2.20.0
pytest==9.0.3
pytest-asyncio==0.21.1
pytest-benchmark==5.2.3
pytest-cov==7.1.0
pytest-cpp==2.6.0
pytest-mock==3.15.1
pytest-randomly==4.1.0
referencing==0.37.0
rpds-py==0.30.0
sortedcontainers==2.4.0
uwsgi==2.0.31
zstandard==0.25.0
37 changes: 37 additions & 0 deletions .riot/requirements/e26245b.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#
# This file is autogenerated by pip-compile with Python 3.15
# by the following command:
#
# pip-compile --allow-unsafe --no-annotate .riot/requirements/e26245b.in
#
attrs==26.1.0
coverage[toml]==7.13.5
gevent==26.4.0
greenlet==3.4.0
gunicorn[gevent]==25.3.0
hypothesis==6.45.0
iniconfig==2.3.0
jsonschema==4.26.0
jsonschema-specifications==2025.9.1
mock==5.2.0
numpy==2.4.4
opentracing==2.4.0
packaging==26.1
pluggy==1.6.0
protobuf==7.34.1
py-cpuinfo==8.0.0
pygments==2.20.0
pytest==9.0.3
pytest-asyncio==0.21.1
pytest-benchmark==5.2.3
pytest-cov==7.1.0
pytest-cpp==2.6.0
pytest-mock==3.15.1
pytest-randomly==4.1.0
referencing==0.37.0
rpds-py==0.30.0
sortedcontainers==2.4.0
uwsgi==2.0.31
zope-event==6.1
zope-interface==8.3
zstandard==0.25.0
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ Use the Skill tool to invoke these. **Always prefer skills over raw commands.**
|-------|---------|
| `run-tests` | Running any tests or validating code changes. **Never run pytest directly.** |
| `lint` | Formatting, style/type/security checks, or before committing. **Never skip before commits.** |
| `type-annotate` | Adding type hints to new or modified Python code; run before commit with `lint typing`. |
| `releasenote` | Creating or updating release notes for the current branch. |
| `find-cpython-usage` | Investigating CPython API dependencies or adding a new Python version. |
| `compare-cpython-versions` | Comparing CPython source between two Python versions. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ endif()
message(WARNING "SOURCE_LIB_DIR: ${SOURCE_LIB_DIR}")
message(WARNING "LIBRARY_NAME: ${LIBRARY_NAME}")

# We expect the native extension to be built and installed the headers in the following directory. It is configured in
# setup.py by setting CARGO_TARGET_DIR environment variable.
set(SOURCE_INCLUDE_DIR
${CMAKE_SOURCE_DIR}/../../../../../src/native/target${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/include)
# Resolves NATIVE_HEADERS_DIR (libdatadog's generated C headers for the active Python minor). See NativeHeaders.cmake
# for the resolution policy.
include(NativeHeaders)
set(SOURCE_INCLUDE_DIR "${NATIVE_HEADERS_DIR}")

set(DEST_LIB_DIR ${CMAKE_CURRENT_BINARY_DIR})
set(DEST_INCLUDE_DIR ${DEST_LIB_DIR}/include)
Expand Down
28 changes: 28 additions & 0 deletions ddtrace/internal/datadog/profiling/cmake/NativeHeaders.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Resolves NATIVE_HEADERS_DIR — the absolute path to libdatadog's generated C headers (produced by the Rust crate under
# src/native/ and written to target<MAJOR>.<MINOR>/include).
#
# Primary source: setup.py passes -DRUST_GENERATED_HEADERS_DIR=<abs path> to every CMake invocation via
# _get_common_cmake_args. Whenever that variable is set, we trust it.
#
# Fallback: build_standalone.sh does NOT pass RUST_GENERATED_HEADERS_DIR, so we compute a path relative to this module's
# own location. Callers must have already invoked find_package(Python3) so that Python3_VERSION_MAJOR/_MINOR are
# defined; the fallback uses those to pick the right per-minor target directory (matching setup.py's CARGO_TARGET_DIR
# layout).
#
# Consumers must have "${CMAKE_CURRENT_SOURCE_DIR}/../cmake" on CMAKE_MODULE_PATH before calling include(NativeHeaders).

if(DEFINED RUST_GENERATED_HEADERS_DIR)
set(NATIVE_HEADERS_DIR "${RUST_GENERATED_HEADERS_DIR}")
else()
if(NOT DEFINED Python3_VERSION_MAJOR OR NOT DEFINED Python3_VERSION_MINOR)
message(
FATAL_ERROR
"NativeHeaders: RUST_GENERATED_HEADERS_DIR is not set and Python3_VERSION_MAJOR/_MINOR are undefined. "
"Call find_package(Python3) before include(NativeHeaders), or pass -DRUST_GENERATED_HEADERS_DIR "
"(as setup.py does).")
endif()
get_filename_component(
NATIVE_HEADERS_DIR
"${CMAKE_CURRENT_LIST_DIR}/../../../../../src/native/target${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/include"
ABSOLUTE)
endif()
10 changes: 7 additions & 3 deletions ddtrace/internal/datadog/profiling/dd_wrapper/src/sample.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
#include "sample.hpp"

// TODO(py-315): Python.h must be included first, before any system or project headers.
// CPython's pyconfig.h defines _POSIX_C_SOURCE and _XOPEN_SOURCE to their current
// POSIX standard values (202405L on 3.15+). If system headers (included transitively
// via libdatadog_helpers.hpp → features.h) are pulled in first, they define older
// values (200809L), and pyconfig.h's later redefinition triggers -Werror on GCC/Clang.
#define PY_SSIZE_T_CLEAN

#include <Python.h>
#include <frameobject.h>

#include "sample.hpp"

#include "libdatadog_helpers.hpp"
#include "profiler_state.hpp"
#include "pymacro.hpp"
Expand Down
13 changes: 7 additions & 6 deletions ddtrace/internal/datadog/profiling/ddup/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,9 @@ add_library(${EXTENSION_NAME} SHARED ${DDUP_CPP_SRC})
add_ddup_config(${EXTENSION_NAME})

# Cython generates code that produces errors for the following, so relax compile options
target_compile_options(${EXTENSION_NAME} PRIVATE -Wno-old-style-cast -Wno-shadow -Wno-address)
# -Wno-missing-field-initializers: Python 3.15 added tp_iteritem to PyTypeObject; Cython doesn't initialize it yet
target_compile_options(${EXTENSION_NAME} PRIVATE -Wno-old-style-cast -Wno-shadow -Wno-address
-Wno-missing-field-initializers)

# cmake may mutate the name of the library (e.g., lib- and -.so for dynamic libraries). This suppresses that behavior,
# which is required to ensure all paths can be inferred correctly by setup.py.
Expand All @@ -87,11 +89,10 @@ elseif(UNIX)
endif()
endif()

target_include_directories(
${EXTENSION_NAME}
PRIVATE ../dd_wrapper/include
../../../../../src/native/target${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/include/
${Datadog_INCLUDE_DIRS} ${Python3_INCLUDE_DIRS})
include(NativeHeaders)

target_include_directories(${EXTENSION_NAME} PRIVATE ../dd_wrapper/include "${NATIVE_HEADERS_DIR}"
${Datadog_INCLUDE_DIRS} ${Python3_INCLUDE_DIRS})

target_link_libraries(${EXTENSION_NAME} PRIVATE dd_wrapper)

Expand Down
10 changes: 6 additions & 4 deletions ddtrace/internal/datadog/profiling/stack/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,16 @@ add_clangtidy_target(${EXTENSION_NAME})
# Never build with native unwinding, since this is not currently used
target_compile_definitions(${EXTENSION_NAME} PRIVATE UNWIND_NATIVE_DISABLE)

# Resolves NATIVE_HEADERS_DIR (libdatadog's generated C headers for the active Python minor). See
# cmake/NativeHeaders.cmake for the resolution policy.
include(NativeHeaders)

# Includes; echion and python are marked "system" to suppress warnings
target_include_directories(
${EXTENSION_NAME} PRIVATE .. # include dd_wrapper from the root in order to make its paths transparent in the code
include)
target_include_directories(
${EXTENSION_NAME} SYSTEM
PRIVATE ${Python3_INCLUDE_DIRS} echion include/vendored include/util
../../../../../src/native/target${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/include/)
target_include_directories(${EXTENSION_NAME} SYSTEM PRIVATE ${Python3_INCLUDE_DIRS} echion include/vendored
include/util "${NATIVE_HEADERS_DIR}")

# Echion sources need to be given the current platform
if(APPLE)
Expand Down
Loading
Loading