Skip to content

Commit

Permalink
feat: Detect and alert circular imports (#999)
Browse files Browse the repository at this point in the history
  • Loading branch information
tefra committed Mar 25, 2024
1 parent 96d2085 commit 6441877
Show file tree
Hide file tree
Showing 5 changed files with 48 additions and 21 deletions.
11 changes: 11 additions & 0 deletions tests/codegen/test_transformer.py
Expand Up @@ -113,6 +113,17 @@ def test_process_with_circular_dependencies_error(self, mock_process_classes):
with self.assertRaises(CodegenError):
self.transformer.process([])

@mock.patch("xsdata.codegen.transformer.logger.warning")
@mock.patch.object(ResourceTransformer, "process_classes")
def test_process_with_module_not_found_error(
self, mock_process_classes, mock_warning
):
mock_process_classes.side_effect = ModuleNotFoundError({})
self.transformer.process([])
mock_warning.assert_called_once_with(
"Module not found on imports validation, please report it."
)

@mock.patch.object(ResourceTransformer, "convert_schema")
@mock.patch.object(ResourceTransformer, "convert_definitions")
@mock.patch.object(ResourceTransformer, "parse_definitions")
Expand Down
6 changes: 5 additions & 1 deletion tests/formats/dataclass/test_generator.py
Expand Up @@ -16,9 +16,12 @@ def setUp(self):
config = GeneratorConfig()
self.generator = DataclassGenerator(config)

@mock.patch.object(DataclassGenerator, "validate_imports")
@mock.patch.object(DataclassGenerator, "render_package")
@mock.patch.object(DataclassGenerator, "render_module")
def test_render(self, mock_render_module, mock_render_package):
def test_render(
self, mock_render_module, mock_render_package, mock_validate_imports
):
classes = [
ClassFactory.create(package="foo.bar", module="tests"),
ClassFactory.create(package="bar.foo", module="tests"),
Expand Down Expand Up @@ -56,6 +59,7 @@ def test_render(self, mock_render_module, mock_render_package):
mock_render_module.assert_has_calls(
[mock.call(mock.ANY, [x], mock.ANY) for x in classes]
)
mock_validate_imports.assert_called_once()

def test_render_package(self):
classes = [
Expand Down
7 changes: 5 additions & 2 deletions xsdata/codegen/transformer.py
Expand Up @@ -138,10 +138,13 @@ def process(self, uris: List[str], cache: bool = False):

try:
self.process_classes()
except CircularDependencyError:
except ModuleNotFoundError:
logger.warning("Module not found on imports validation, please report it.")
except (CircularDependencyError, ImportError):
raise CodegenError(
"Circular Dependencies Found",
help="Try a different structure style and enabling unnest classes.",
help="Try a different structure style and/or enable unnest classes. "
"Cleanup previously generated packages and modules.",
)

for name, times in stopwatches.items():
Expand Down
27 changes: 27 additions & 0 deletions xsdata/formats/dataclass/generator.py
@@ -1,5 +1,8 @@
import importlib
import pkgutil
import re
import subprocess
import sys
from pathlib import Path
from typing import Iterator, List, Optional

Expand All @@ -10,6 +13,7 @@
from xsdata.codegen.resolver import DependenciesResolver
from xsdata.formats.dataclass.filters import Filters
from xsdata.formats.mixins import AbstractGenerator, GeneratorResult
from xsdata.logger import logger
from xsdata.models.config import GeneratorConfig


Expand Down Expand Up @@ -82,6 +86,29 @@ def render(self, classes: List[Class]) -> Iterator[GeneratorResult]:
source=src_code,
)

self.validate_imports()

def validate_imports(self):
"""Recursively import all generated packages.
Raises:
ImportError: On circular imports
"""

def import_package(package_name):
logger.debug(f"Importing: {package_name}")
module = importlib.import_module(package_name)
if hasattr(module, "__path__"):
for _, name, _ in pkgutil.walk_packages(
module.__path__, module.__name__ + "."
):
logger.debug(f"Importing: {name}")
importlib.import_module(name)

sys.path.insert(0, str(Path.cwd().absolute()))
package = self.config.output.package
import_package(self.package_name(package))

def render_package(self, classes: List[Class], module: str, filename: Path) -> str:
"""Render the package for the given classes.
Expand Down
18 changes: 0 additions & 18 deletions xsdata/formats/dataclass/models/builders.py
Expand Up @@ -562,24 +562,6 @@ def resolve_namespaces(

return tuple(result)

@classmethod
def default_namespace(cls, namespaces: Sequence[str]) -> Optional[str]:
"""Return the first valid namespace uri or None.
Args:
namespaces: A list of namespace options which may include
valid uri(s) or a placeholder e.g. ##any, ##other,
##targetNamespace, ##local
Returns:
A namespace uri or None if there isn't any.
"""
for namespace in namespaces:
if namespace and not namespace.startswith("#"):
return namespace

return None

@classmethod
def is_any_type(cls, types: Sequence[Type], xml_type: str) -> bool:
"""Return whether the given xml type supports generic values."""
Expand Down

0 comments on commit 6441877

Please sign in to comment.