diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 938c6a3b7..47a9e3b03 100644 --- a/netbox/utilities/testing/api.py +++ b/netbox/utilities/testing/api.py @@ -16,7 +16,6 @@ from django.contrib.postgres.fields import ArrayField from django.db import models from django.test import override_settings from django.urls import reverse -from django.utils.module_loading import import_string from rest_framework import status from rest_framework.test import APIClient from strawberry.types.base import StrawberryList, StrawberryOptional @@ -915,6 +914,7 @@ class APIViewTestCases: return None lazy_module = None + lazy_package = None # Cap iterations at 8: typical NetBox annotations nest at most 3 layers # (Union > Annotated > ForwardRef). 8 is a generous safety net to # prevent infinite loops on pathological / future annotation shapes. @@ -934,6 +934,9 @@ class APIViewTestCases: module_name = getattr(meta, 'module', None) if module_name: lazy_module = module_name + # strawberry.lazy('.relative') records the anchor package + # needed to resolve the leading-dot module path. + lazy_package = getattr(meta, 'package', None) break inner = args[0] if args else None if inner is None: @@ -947,9 +950,13 @@ class APIViewTestCases: if lazy_module is None: return None name = annotation.__forward_arg__ if isinstance(annotation, typing.ForwardRef) else annotation + # Resolve via import_module(module, package) rather than import_string() + # so relative lazy modules (e.g. strawberry.lazy('.filters')) resolve + # against their anchor package, as strawberry itself does. try: - return import_string(f'{lazy_module}.{name}') - except ImportError: + module = importlib.import_module(lazy_module, lazy_package) + return getattr(module, name) + except (ImportError, AttributeError): return None return annotation