From 21df307db9a4e2213de7be04c91480a6c37a6cc6 Mon Sep 17 00:00:00 2001 From: kimstik Date: Tue, 15 Sep 2026 15:26:06 +0200 Subject: [PATCH 1/3] 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/3] 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) From 1c4d0e9c1e82de7bf994d935068bafaccc7f90bb Mon Sep 17 00:00:00 2001 From: kimstik Date: Mon, 28 Sep 2026 19:19:29 +0200 Subject: [PATCH 3/3] Import ezdxf and vtk in the top-level modules As suggested in review, the I/O modules keep plain imports and the functions that export or import load them. STEPExportModeLiterals moves to types, so that Assembly.export needs no exporter import. --- .coveragerc | 1 - cadquery/assembly.py | 18 +++++++-------- cadquery/occ_impl/assembly.py | 5 ++++- cadquery/occ_impl/exporters/__init__.py | 24 +++++++++++++++++--- cadquery/occ_impl/exporters/assembly.py | 22 +++++-------------- cadquery/occ_impl/exporters/dxf.py | 16 +++++--------- cadquery/occ_impl/exporters/vtk.py | 29 ++++++++++--------------- cadquery/occ_impl/importers/__init__.py | 11 +++++++++- cadquery/occ_impl/importers/dxf.py | 3 ++- cadquery/sketch.py | 3 ++- cadquery/types.py | 1 + tests/test_lazy_imports.py | 11 ++++++---- 12 files changed, 80 insertions(+), 64 deletions(-) diff --git a/.coveragerc b/.coveragerc index 96696344f..00a03428b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -11,4 +11,3 @@ omit = exclude_lines = # Ignore stub body \.\.\. - if TYPE_CHECKING: diff --git a/cadquery/assembly.py b/cadquery/assembly.py index fdf98dd73..5e8f671a2 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -27,17 +27,9 @@ UnaryConstraintKind, BinaryConstraintKind, ) -from .occ_impl.exporters.assembly import ( - exportAssembly, - exportCAF, - exportVTKJS, - exportVRML, - exportGLTF, - STEPExportModeLiterals, -) from .occ_impl.importers.assembly import importStep as _importStep, importXbf, importXml -from .types import UnitLiterals +from .types import UnitLiterals, STEPExportModeLiterals from .selectors import _expression_grammar as _selector_grammar from .utils import deprecate, BiDict, instance_of @@ -596,6 +588,14 @@ def export( if mode not in get_args(STEPExportModeLiterals): raise ValueError(f"Unknown assembly export mode {mode} for STEP") + from .occ_impl.exporters.assembly import ( + exportAssembly, + exportCAF, + exportVTKJS, + exportVRML, + exportGLTF, + ) + if exportType is None: t = path.split(".")[-1].upper() if t in ("STEP", "XML", "XBF", "VRML", "VTKJS", "GLTF", "GLB", "STL"): diff --git a/cadquery/occ_impl/assembly.py b/cadquery/occ_impl/assembly.py index 967b79221..5fe226da3 100644 --- a/cadquery/occ_impl/assembly.py +++ b/cadquery/occ_impl/assembly.py @@ -44,7 +44,6 @@ 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 @@ -628,6 +627,7 @@ def toVTKAssy( ) -> "List[vtkProp3D]": from vtkmodules.vtkRenderingCore import vtkActor, vtkPolyDataMapper as vtkMapper + from .exporters.vtk import extractEdgesFaces rv: List[vtkProp3D] = [] @@ -681,6 +681,7 @@ def toVTK( vtkPolyDataMapper as vtkMapper, vtkRenderer, ) + from .exporters.vtk import extractEdgesFaces renderer = vtkRenderer() @@ -730,6 +731,8 @@ def toJSON( Export an object to a structure suitable for converting to VTK.js JSON. """ + from .exporters.vtk import toString + rv = [] for shape, _, loc, col_ in assy: diff --git a/cadquery/occ_impl/exporters/__init__.py b/cadquery/occ_impl/exporters/__init__.py index 266dd4ac9..16a29e6cc 100644 --- a/cadquery/occ_impl/exporters/__init__.py +++ b/cadquery/occ_impl/exporters/__init__.py @@ -1,4 +1,5 @@ -from typing import IO, Optional, Union, cast, Dict, Any, Iterable +from importlib import import_module +from typing import IO, Optional, Union, cast, Dict, Any, Iterable, TYPE_CHECKING from typing_extensions import Literal from OCP.VrmlAPI import VrmlAPI @@ -10,8 +11,10 @@ from .json import JsonMesh from .amf import AmfWriter from .threemf import ThreeMFWriter -from .dxf import exportDXF, exportDXFProjection, DxfDocument -from .vtk import exportVTP + +if TYPE_CHECKING: + from .dxf import exportDXF, exportDXFProjection, DxfDocument + from .vtk import exportVTP class ExportTypes: @@ -110,6 +113,8 @@ def export( tmfw.write3mf(f) elif exportType == ExportTypes.DXF: + from .dxf import exportDXF + exportDXF(w, fname, **opt) elif exportType == ExportTypes.STEP: @@ -128,6 +133,8 @@ def export( VrmlAPI.Write_s(shape.wrapped, fname) elif exportType == ExportTypes.VTP: + from .vtk import exportVTP + exportVTP(shape, fname, tolerance, angularTolerance) elif exportType == ExportTypes.BREP: @@ -138,3 +145,14 @@ def export( else: raise ValueError("Unknown export type") + + +# lazy: importing these here would load ezdxf and vtk with cadquery +def __getattr__(name): + if name in ("dxf", "vtk", "assembly"): + return import_module(f".{name}", __name__) + if name in ("exportDXF", "exportDXFProjection", "DxfDocument"): + return getattr(import_module(".dxf", __name__), name) + if name == "exportVTP": + return import_module(".vtk", __name__).exportVTP + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/cadquery/occ_impl/exporters/assembly.py b/cadquery/occ_impl/exporters/assembly.py index b278c99c5..b4cb60c50 100644 --- a/cadquery/occ_impl/exporters/assembly.py +++ b/cadquery/occ_impl/exporters/assembly.py @@ -3,8 +3,10 @@ from tempfile import TemporaryDirectory from shutil import make_archive -from typing import Optional, TYPE_CHECKING -from typing_extensions import Literal +from typing import Optional + +from vtkmodules.vtkIOExport import vtkJSONSceneExporter, vtkVRMLExporter +from vtkmodules.vtkRenderingCore import vtkRenderWindow from OCP.XSControl import XSControl_WorkSession from OCP.STEPCAFControl import STEPCAFControl_Writer @@ -47,10 +49,7 @@ from ..assembly import AssemblyProtocol, toCAF, toVTK, toFusedCAF from ..geom import Location from ..shapes import Shape, Compound -from ...types import UnitLiterals - -if TYPE_CHECKING: - from vtkmodules.vtkRenderingCore import vtkRenderWindow +from ...types import UnitLiterals, STEPExportModeLiterals class ExportModes: @@ -58,9 +57,6 @@ class ExportModes: FUSED = "fused" -STEPExportModeLiterals = Literal["default", "fused"] - - def exportAssembly( assy: AssemblyProtocol, path: str, @@ -392,13 +388,11 @@ 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) @@ -414,8 +408,6 @@ 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: @@ -437,8 +429,6 @@ 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/dxf.py b/cadquery/occ_impl/exporters/dxf.py index 6a13aa932..1106fef01 100644 --- a/cadquery/occ_impl/exporters/dxf.py +++ b/cadquery/occ_impl/exporters/dxf.py @@ -14,6 +14,9 @@ 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 @@ -24,8 +27,6 @@ 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] @@ -84,7 +85,7 @@ def __init__( self, dxfversion: str = "AC1027", setup: Union[bool, List[str]] = False, - doc_units: int = _MM, + doc_units: int = units.MM, *, metadata: Union[Dict[str, str], None] = None, approx: Optional[ApproxOptions] = None, @@ -110,8 +111,6 @@ def __init__( :param tolerance: Approximation tolerance for converting :class:`cadquery.Workplane` objects to DXF entities. """ - import ezdxf - if metadata is None: metadata = {} @@ -154,8 +153,6 @@ 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 @@ -375,7 +372,7 @@ def exportDXF( approx: Optional[ApproxOptions] = None, tolerance: float = 1e-3, *, - doc_units: int = _MM, + doc_units: int = units.MM, ) -> None: """ Export Workplane content to DXF. Works with 2D sections. @@ -388,7 +385,6 @@ 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) @@ -411,7 +407,7 @@ def exportDXFProjection( tolerance: float = 1e-3, *, up: Optional[VectorLike] = None, - doc_units: int = _MM, + doc_units: int = units.MM, ) -> None: """ Export to DXF using projections. Works with 3D objects. diff --git a/cadquery/occ_impl/exporters/vtk.py b/cadquery/occ_impl/exporters/vtk.py index 29750e143..c613fdd7a 100644 --- a/cadquery/occ_impl/exporters/vtk.py +++ b/cadquery/occ_impl/exporters/vtk.py @@ -1,22 +1,21 @@ -from typing import TYPE_CHECKING +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 ..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) @@ -45,8 +44,6 @@ 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)) @@ -57,8 +54,6 @@ 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/importers/__init__.py b/cadquery/occ_impl/importers/__init__.py index 51eeb26e3..d55ec8cec 100644 --- a/cadquery/occ_impl/importers/__init__.py +++ b/cadquery/occ_impl/importers/__init__.py @@ -1,3 +1,4 @@ +from importlib import import_module from math import pi from typing import List, Literal @@ -7,7 +8,6 @@ from ... import cq from ..shapes import Shape -from .dxf import _importDXF from ...types import UnitLiterals RAD2DEG = 360.0 / (2 * pi) @@ -133,6 +133,15 @@ def importDXF( :param include: a list of layer names to import """ + from .dxf import _importDXF + faces = _importDXF(filename, tol, exclude, include) return cq.Workplane("XY").newObject(faces) + + +# lazy: importing dxf here would load ezdxf with cadquery +def __getattr__(name): + if name == "dxf": + return import_module(".dxf", __name__) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/cadquery/occ_impl/importers/dxf.py b/cadquery/occ_impl/importers/dxf.py index ea9fe5037..ab1d8b05f 100644 --- a/cadquery/occ_impl/importers/dxf.py +++ b/cadquery/occ_impl/importers/dxf.py @@ -6,6 +6,8 @@ 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 @@ -170,7 +172,6 @@ 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/cadquery/sketch.py b/cadquery/sketch.py index e66749053..ad5b4e961 100644 --- a/cadquery/sketch.py +++ b/cadquery/sketch.py @@ -38,7 +38,6 @@ ) from .occ_impl.geom import Location, Vector from .occ_impl.exporters import export -from .occ_impl.importers.dxf import _importDXF from .occ_impl.sketch_solver import ( SketchConstraintSolver, ConstraintKind, @@ -232,6 +231,8 @@ def importDXF( Import a DXF file and construct face(s) """ + from .occ_impl.importers.dxf import _importDXF + res = Compound.makeCompound(_importDXF(filename, tol, exclude, include)) return self.face(res, angle, mode, tag) diff --git a/cadquery/types.py b/cadquery/types.py index a178526bc..625b9f154 100644 --- a/cadquery/types.py +++ b/cadquery/types.py @@ -1,4 +1,5 @@ from typing import Union, Literal UnitLiterals = Literal["MM", "CM", "M", "KM", "INCH", "FT", "MI", "UM", "NM"] +STEPExportModeLiterals = Literal["default", "fused"] Real = Union[int, float] diff --git a/tests/test_lazy_imports.py b/tests/test_lazy_imports.py index d9d6718aa..5d130f414 100644 --- a/tests/test_lazy_imports.py +++ b/tests/test_lazy_imports.py @@ -8,16 +8,19 @@ 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', 'vtkmodules'})" + "import sys, cadquery as cq;" + "print({m.split('.')[0] for m in sys.modules} & {'ezdxf', 'vtkmodules'});" + # the submodules stay reachable through the packages + "cq.exporters.assembly.ExportModes, cq.exporters.dxf, cq.exporters.vtk, cq.importers.dxf;" + "print('ok')" ) # 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], + [sys.executable, "-u", "-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 + assert out.stdout.split() == ["set()", "ok"], out.stdout + out.stderr