Skip to content
87 changes: 87 additions & 0 deletions tests/test_shadowed_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
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 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):
"""
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.
"""
for _, submodule_name, _ in pkgutil.walk_packages(integrations.__path__):
module_path = f"sentry_sdk.integrations.{submodule_name}"

# Temporary skip list
if submodule_name in (
"clickhouse_driver",
"grpc",
"litellm",
"opentelemetry",
"pure_eval",
"ray",
"trytond",
"typer",
):
continue
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we merge this test, my plan is to remove integrations from the skip list one-by-one.


try:
importlib.import_module(module_path)
continue
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)

This comment was marked as outdated.

Copy link
Contributor Author

@alexander-alderman-webb alexander-alderman-webb Nov 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to parametrize the test and fork it because messing with sys.modules and importing can cause side-effects and influence other tests otherwise.

edit: I don't like adding more forked tests. I'll move the test into it's own tox environment so there's no risk of messing with the behavior of other tests when removing the forking.