Skip to content
Open
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
4 changes: 4 additions & 0 deletions .github/workflows/test-integrations-misc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ jobs:
run: |
set -x # print commands that are executed
./scripts/runtox.sh "py${{ matrix.python-version }}-integration_deactivation"
- name: Test shadowed_module
run: |
set -x # print commands that are executed
./scripts/runtox.sh "py${{ matrix.python-version }}-shadowed_module"
- name: Generate coverage XML (Python 3.6)
if: ${{ !cancelled() && matrix.python-version == '3.6' }}
run: |
Expand Down
6 changes: 3 additions & 3 deletions scripts/populate_tox/package_dependencies.jsonl

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions scripts/populate_tox/populate_tox.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"cloud_resource_context",
"common",
"integration_deactivation",
"shadowed_module",
"gcp",
"gevent",
"opentelemetry",
Expand Down
28 changes: 14 additions & 14 deletions scripts/populate_tox/releases.jsonl

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions scripts/populate_tox/tox.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ envlist =
# === Integration Deactivation ===
{py3.9,py3.10,py3.11,py3.12,py3.13,py3.14}-integration_deactivation

# === Shadowed Module ===
{py3.7,py3.8,py3.9,py3.10,py3.11,py3.12,py3.13,py3.14,py3.14t}-shadowed_module

# === Integrations ===

# Asgi
Expand Down Expand Up @@ -157,10 +160,15 @@ setenv =
django: DJANGO_SETTINGS_MODULE=tests.integrations.django.myapp.settings
spark-v{3.0.3,3.5.6}: JAVA_HOME=/usr/lib/jvm/temurin-11-jdk-amd64

# Avoid polluting test suite with imports
common: PYTEST_ADDOPTS="--ignore=tests/test_shadowed_module.py"
gevent: PYTEST_ADDOPTS="--ignore=tests/test_shadowed_module.py"

# TESTPATH definitions for test suites not managed by toxgen
common: TESTPATH=tests
gevent: TESTPATH=tests
integration_deactivation: TESTPATH=tests/test_ai_integration_deactivation.py
shadowed_module: TESTPATH=tests/test_shadowed_module.py
asgi: TESTPATH=tests/integrations/asgi
aws_lambda: TESTPATH=tests/integrations/aws_lambda
cloud_resource_context: TESTPATH=tests/integrations/cloud_resource_context
Expand Down
1 change: 1 addition & 0 deletions scripts/split_tox_gh_actions/split_tox_gh_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
"trytond",
"typer",
"integration_deactivation",
"shadowed_module",
],
}

Expand Down
109 changes: 109 additions & 0 deletions tests/test_shadowed_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import sys
import ast
import types
import pkgutil
import importlib
import pathlib
import pytest

from sentry_sdk import integrations
from sentry_sdk.integrations import _DEFAULT_INTEGRATIONS, Integration


def pytest_generate_tests(metafunc):
"""
All submodules of sentry_sdk.integrations are picked up, so modules
without a subclass of sentry_sdk.integrations.Integration are also tested
for poorly gated imports.

This approach was chosen to keep the implementation simple.
"""
if "integration_submodule_name" in metafunc.fixturenames:
submodule_names = {
submodule_name
for _, submodule_name, _ in pkgutil.walk_packages(integrations.__path__)
}

metafunc.parametrize(
"integration_submodule_name",
# Temporarily skip some integrations
submodule_names
- {
"clickhouse_driver",
"grpc",
"litellm",
"opentelemetry",
"pure_eval",
"ray",
"trytond",
"typer",
},
)


def find_unrecognized_dependencies(tree):
"""
Finds unrecognized imports in the AST for a Python module. In an empty
environment the set of non-standard library modules is returned.
"""
unrecognized_dependencies = set()
package_name = lambda name: name.split(".")[0]

for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
root = package_name(alias.name)

try:
if not importlib.util.find_spec(root):
unrecognized_dependencies.add(root)
except ValueError:
continue

elif isinstance(node, ast.ImportFrom):
# if node.level is not 0 the import is relative
if node.level > 0 or node.module is None:
continue

root = package_name(node.module)

try:
if not importlib.util.find_spec(root):
unrecognized_dependencies.add(root)
except ValueError:
continue

return unrecognized_dependencies


@pytest.mark.skipif(
sys.version_info < (3, 7), reason="asyncpg imports __future__.annotations"
)
def test_shadowed_modules_when_importing_integrations(
sentry_init, integration_submodule_name
):
"""
Check that importing integrations for third-party module raises an
DidNotEnable exception when the associated module is shadowed by an empty
module.

An integration is determined to be for a third-party module if it cannot
be imported in the environment in which the tests run.
"""
module_path = f"sentry_sdk.integrations.{integration_submodule_name}"
try:
importlib.import_module(module_path)
return
except integrations.DidNotEnable:
spec = importlib.util.find_spec(module_path)
source = pathlib.Path(spec.origin).read_text(encoding="utf-8")
tree = ast.parse(source, filename=spec.origin)
integration_dependencies = find_unrecognized_dependencies(tree)

for dependency in integration_dependencies:
sys.modules[dependency] = types.ModuleType(dependency)
with pytest.raises(integrations.DidNotEnable):
importlib.import_module(module_path)

for dependency in integration_dependencies:
del sys.modules[dependency]
Loading