From 21df307db9a4e2213de7be04c91480a6c37a6cc6 Mon Sep 17 00:00:00 2001 From: kimstik Date: Tue, 15 Sep 2026 15:26:06 +0200 Subject: [PATCH 1/2] Import ezdxf on use --- cadquery/occ_impl/exporters/dxf.py | 16 ++++++++++------ cadquery/occ_impl/importers/dxf.py | 3 +-- tests/test_lazy_imports.py | 23 +++++++++++++++++++++++ 3 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 tests/test_lazy_imports.py diff --git a/cadquery/occ_impl/exporters/dxf.py b/cadquery/occ_impl/exporters/dxf.py index 1106fef01..6a13aa932 100644 --- a/cadquery/occ_impl/exporters/dxf.py +++ b/cadquery/occ_impl/exporters/dxf.py @@ -14,9 +14,6 @@ runtime_checkable, ) -import ezdxf -from ezdxf import units, zoom -from ezdxf.entities import factory from OCP.GeomConvert import GeomConvert from OCP.gp import gp_Dir from OCP.GC import GC_MakeArcOfEllipse @@ -27,6 +24,8 @@ from ..geom import Plane, VectorLike +_MM = 4 # ezdxf.units.MM, without importing ezdxf at module level + ApproxOptions = Literal["spline", "arc"] DxfEntityAttributes = Tuple[ Literal["ARC", "CIRCLE", "ELLIPSE", "LINE", "SPLINE",], Dict[str, Any] @@ -85,7 +84,7 @@ def __init__( self, dxfversion: str = "AC1027", setup: Union[bool, List[str]] = False, - doc_units: int = units.MM, + doc_units: int = _MM, *, metadata: Union[Dict[str, str], None] = None, approx: Optional[ApproxOptions] = None, @@ -111,6 +110,8 @@ def __init__( :param tolerance: Approximation tolerance for converting :class:`cadquery.Workplane` objects to DXF entities. """ + import ezdxf + if metadata is None: metadata = {} @@ -153,6 +154,8 @@ def add_shape(self, shape: Union[WorkplaneLike, Shape], layer: str = "") -> Self :param s: CadQuery Workplane or Shape :param layer: layer definition name """ + import ezdxf + from ezdxf.entities import factory if isinstance(shape, WorkplaneLike): plane = shape.plane @@ -372,7 +375,7 @@ def exportDXF( approx: Optional[ApproxOptions] = None, tolerance: float = 1e-3, *, - doc_units: int = units.MM, + doc_units: int = _MM, ) -> None: """ Export Workplane content to DXF. Works with 2D sections. @@ -385,6 +388,7 @@ def exportDXF( :param tolerance: Approximation tolerance. :param doc_units: ezdxf document/modelspace :doc:`units ` (in. = ``1``, mm = ``4``). """ + from ezdxf import zoom dxf = DxfDocument(approx=approx, tolerance=tolerance, doc_units=doc_units) @@ -407,7 +411,7 @@ def exportDXFProjection( tolerance: float = 1e-3, *, up: Optional[VectorLike] = None, - doc_units: int = units.MM, + doc_units: int = _MM, ) -> None: """ Export to DXF using projections. Works with 3D objects. diff --git a/cadquery/occ_impl/importers/dxf.py b/cadquery/occ_impl/importers/dxf.py index ab1d8b05f..ea9fe5037 100644 --- a/cadquery/occ_impl/importers/dxf.py +++ b/cadquery/occ_impl/importers/dxf.py @@ -6,8 +6,6 @@ from ..geom import Vector from ..shapes import Shape, Edge, Face, sortWiresByBuildOrder -import ezdxf - from OCP.ShapeAnalysis import ShapeAnalysis_FreeBounds from OCP.collections import HSequence_TopoDS_Shape as TopTools_HSequenceOfShape from OCP.gp import gp_Pnt @@ -172,6 +170,7 @@ def _importDXF( :param exclude: a list of layer names not to import :param include: a list of layer names to import """ + import ezdxf if exclude and include: raise ValueError("you may specify either 'include' or 'exclude' but not both") diff --git a/tests/test_lazy_imports.py b/tests/test_lazy_imports.py new file mode 100644 index 000000000..d454a9663 --- /dev/null +++ b/tests/test_lazy_imports.py @@ -0,0 +1,23 @@ +import os +import subprocess +import sys + +import cadquery + + +def test_import_does_not_load_ezdxf(): + # a fresh interpreter: this process has ezdxf loaded by other tests + code = ( + "import sys, cadquery;" + "print({m.split('.')[0] for m in sys.modules} & {'ezdxf'})" + ) + # run next to the package under test; only stdout is checked, since the + # interpreter may crash on exit on Windows (#1911) + out = subprocess.run( + [sys.executable, "-c", code], + cwd=os.path.dirname(os.path.dirname(cadquery.__file__)), + capture_output=True, + text=True, + ) + + assert out.stdout.strip().endswith("set()"), out.stdout + out.stderr From 87ca6f537f5c6a56417356f2743aac085c2290d2 Mon Sep 17 00:00:00 2001 From: kimstik Date: Tue, 15 Sep 2026 15:29:38 +0200 Subject: [PATCH 2/2] Import vtk on use --- .coveragerc | 1 + cadquery/occ_impl/assembly.py | 23 ++++++++++++-------- cadquery/occ_impl/exporters/assembly.py | 16 +++++++++----- cadquery/occ_impl/exporters/vtk.py | 29 +++++++++++++++---------- cadquery/occ_impl/shapes.py | 10 ++++++--- tests/test_lazy_imports.py | 6 ++--- 6 files changed, 53 insertions(+), 32 deletions(-) diff --git a/.coveragerc b/.coveragerc index 00a03428b..96696344f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,3 +11,4 @@ omit = exclude_lines = # Ignore stub body \.\.\. + if TYPE_CHECKING: diff --git a/cadquery/occ_impl/assembly.py b/cadquery/occ_impl/assembly.py index 3cea87aff..967b79221 100644 --- a/cadquery/occ_impl/assembly.py +++ b/cadquery/occ_impl/assembly.py @@ -9,6 +9,7 @@ Any, List, cast, + TYPE_CHECKING, ) from typing_extensions import Protocol, Self from math import degrees, radians @@ -41,19 +42,15 @@ from OCP.TopoDS import TopoDS_Shape from OCP.gp import gp_EulerSequence -from vtkmodules.vtkRenderingCore import ( - vtkActor, - vtkPolyDataMapper as vtkMapper, - vtkRenderer, - vtkProp3D, -) - from .geom import Location from .shapes import Shape, Solid, Compound, GlueLiteral, _set_glue, _set_builder_options from .exporters.vtk import toString, extractEdgesFaces from ..cq import Workplane from ..utils import BiDict +if TYPE_CHECKING: + from vtkmodules.vtkRenderingCore import vtkRenderer, vtkProp3D + # type definitions AssemblyObjects = Union[Shape, Workplane, None] @@ -628,7 +625,9 @@ def toVTKAssy( linewidth: float = 2, tolerance: float = 1e-3, angularTolerance: float = 0.1, -) -> List[vtkProp3D]: +) -> "List[vtkProp3D]": + + from vtkmodules.vtkRenderingCore import vtkActor, vtkPolyDataMapper as vtkMapper rv: List[vtkProp3D] = [] @@ -675,7 +674,13 @@ def toVTK( color: Tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0), tolerance: float = 1e-3, angularTolerance: float = 0.1, -) -> vtkRenderer: +) -> "vtkRenderer": + + from vtkmodules.vtkRenderingCore import ( + vtkActor, + vtkPolyDataMapper as vtkMapper, + vtkRenderer, + ) renderer = vtkRenderer() diff --git a/cadquery/occ_impl/exporters/assembly.py b/cadquery/occ_impl/exporters/assembly.py index 4c52929b7..b278c99c5 100644 --- a/cadquery/occ_impl/exporters/assembly.py +++ b/cadquery/occ_impl/exporters/assembly.py @@ -3,12 +3,9 @@ from tempfile import TemporaryDirectory from shutil import make_archive -from typing import Optional +from typing import Optional, TYPE_CHECKING from typing_extensions import Literal -from vtkmodules.vtkIOExport import vtkJSONSceneExporter, vtkVRMLExporter -from vtkmodules.vtkRenderingCore import vtkRenderWindow - from OCP.XSControl import XSControl_WorkSession from OCP.STEPCAFControl import STEPCAFControl_Writer from OCP.STEPControl import STEPControl_StepModelType @@ -52,6 +49,9 @@ from ..shapes import Shape, Compound from ...types import UnitLiterals +if TYPE_CHECKING: + from vtkmodules.vtkRenderingCore import vtkRenderWindow + class ExportModes: DEFAULT = "default" @@ -392,11 +392,13 @@ def exportCAF(assy: AssemblyProtocol, path: str, binary: bool = False) -> bool: def _vtkRenderWindow( assy: AssemblyProtocol, tolerance: float = 1e-3, angularTolerance: float = 0.1 -) -> vtkRenderWindow: +) -> "vtkRenderWindow": """ Convert an assembly to a vtkRenderWindow. Used by vtk based exporters. """ + from vtkmodules.vtkRenderingCore import vtkRenderWindow + renderer = toVTK(assy, tolerance=tolerance, angularTolerance=angularTolerance) renderWindow = vtkRenderWindow() renderWindow.AddRenderer(renderer) @@ -412,6 +414,8 @@ def exportVTKJS(assy: AssemblyProtocol, path: str): Export an assembly to a zipped vtkjs. NB: .zip extensions is added to path. """ + from vtkmodules.vtkIOExport import vtkJSONSceneExporter + renderWindow = _vtkRenderWindow(assy) with TemporaryDirectory() as tmpdir: @@ -433,6 +437,8 @@ def exportVRML( Export an assembly to a vrml file using vtk. """ + from vtkmodules.vtkIOExport import vtkVRMLExporter + exporter = vtkVRMLExporter() exporter.SetFileName(path) exporter.SetRenderWindow(_vtkRenderWindow(assy, tolerance, angularTolerance)) diff --git a/cadquery/occ_impl/exporters/vtk.py b/cadquery/occ_impl/exporters/vtk.py index c613fdd7a..29750e143 100644 --- a/cadquery/occ_impl/exporters/vtk.py +++ b/cadquery/occ_impl/exporters/vtk.py @@ -1,21 +1,22 @@ -from vtkmodules.vtkIOXML import vtkXMLPolyDataWriter -from vtkmodules.vtkFiltersCore import vtkAppendPolyData -from vtkmodules.vtkCommonDataModel import vtkPolyData -from vtkmodules.vtkFiltersExtraction import vtkExtractCellsByType - -from vtkmodules.vtkCommonDataModel import ( - VTK_TRIANGLE, - VTK_LINE, - VTK_VERTEX, - VTK_POLY_LINE, -) +from typing import TYPE_CHECKING from ..shapes import Shape +if TYPE_CHECKING: + from vtkmodules.vtkCommonDataModel import vtkPolyData -def extractEdgesFaces(data: vtkPolyData) -> tuple[vtkPolyData, vtkPolyData]: + +def extractEdgesFaces(data: "vtkPolyData") -> "tuple[vtkPolyData, vtkPolyData]": """Helper for edges and faces extraction""" + from vtkmodules.vtkFiltersExtraction import vtkExtractCellsByType + from vtkmodules.vtkCommonDataModel import ( + VTK_TRIANGLE, + VTK_LINE, + VTK_VERTEX, + VTK_POLY_LINE, + ) + # extract edges extr = vtkExtractCellsByType() extr.SetInputDataObject(data) @@ -44,6 +45,8 @@ def exportVTP( shape: Shape, fname: str, tolerance: float = 0.1, angularTolerance: float = 0.1 ): + from vtkmodules.vtkIOXML import vtkXMLPolyDataWriter + writer = vtkXMLPolyDataWriter() writer.SetFileName(fname) writer.SetInputData(shape.toVtkPolyData(tolerance, angularTolerance)) @@ -54,6 +57,8 @@ def toString( shape: Shape, tolerance: float = 1e-3, angularTolerance: float = 0.1 ) -> tuple[str, str]: + from vtkmodules.vtkIOXML import vtkXMLPolyDataWriter + writer_edges = vtkXMLPolyDataWriter() writer_edges.SetWriteToOutputString(True) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 7eb28caca..5aeb10c60 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -11,6 +11,7 @@ Self, TypeAlias, TypeVar, + TYPE_CHECKING, cast as tcast, overload, Type, @@ -20,9 +21,6 @@ from warnings import warn -from vtkmodules.vtkCommonDataModel import vtkPolyData -from vtkmodules.vtkFiltersCore import vtkTriangleFilter, vtkPolyDataNormals - from OCP.ShapeBuild import ShapeBuild_ReShape from .geom import Vector, VectorLike, BoundBox, Plane, Location, Matrix @@ -36,6 +34,9 @@ from ..utils import multimethod, multidispatch, mypyclassmethod from ..types import UnitLiterals +if TYPE_CHECKING: + from vtkmodules.vtkCommonDataModel import vtkPolyData + # change default OCCT logging level from OCP.Message import Message, Message_Gravity @@ -1711,6 +1712,9 @@ def toVtkPolyData( Convert shape to vtkPolyData """ + # vtk must be loaded before getVtkPolyData(), which returns None otherwise + from vtkmodules.vtkFiltersCore import vtkTriangleFilter, vtkPolyDataNormals + vtk_shape = IVtkOCC_Shape(self.wrapped) shape_data = IVtkVTK_ShapeData() shape_mesher = IVtkOCC_ShapeMesher() diff --git a/tests/test_lazy_imports.py b/tests/test_lazy_imports.py index d454a9663..d9d6718aa 100644 --- a/tests/test_lazy_imports.py +++ b/tests/test_lazy_imports.py @@ -5,11 +5,11 @@ import cadquery -def test_import_does_not_load_ezdxf(): - # a fresh interpreter: this process has ezdxf loaded by other tests +def test_import_does_not_load_ezdxf_or_vtk(): + # a fresh interpreter: this process has ezdxf and vtk loaded by other tests code = ( "import sys, cadquery;" - "print({m.split('.')[0] for m in sys.modules} & {'ezdxf'})" + "print({m.split('.')[0] for m in sys.modules} & {'ezdxf', 'vtkmodules'})" ) # run next to the package under test; only stdout is checked, since the # interpreter may crash on exit on Windows (#1911)