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 3cea87aff..5fe226da3 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,14 @@ 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 +624,10 @@ 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 + from .exporters.vtk import extractEdgesFaces rv: List[vtkProp3D] = [] @@ -675,7 +674,14 @@ 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, + ) + from .exporters.vtk import extractEdgesFaces renderer = vtkRenderer() @@ -725,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 4c52929b7..b4cb60c50 100644 --- a/cadquery/occ_impl/exporters/assembly.py +++ b/cadquery/occ_impl/exporters/assembly.py @@ -4,7 +4,6 @@ from tempfile import TemporaryDirectory from shutil import make_archive from typing import Optional -from typing_extensions import Literal from vtkmodules.vtkIOExport import vtkJSONSceneExporter, vtkVRMLExporter from vtkmodules.vtkRenderingCore import vtkRenderWindow @@ -50,7 +49,7 @@ from ..assembly import AssemblyProtocol, toCAF, toVTK, toFusedCAF from ..geom import Location from ..shapes import Shape, Compound -from ...types import UnitLiterals +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, 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/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/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 new file mode 100644 index 000000000..5d130f414 --- /dev/null +++ b/tests/test_lazy_imports.py @@ -0,0 +1,26 @@ +import os +import subprocess +import sys + +import cadquery + + +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 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, "-u", "-c", code], + cwd=os.path.dirname(os.path.dirname(cadquery.__file__)), + capture_output=True, + text=True, + ) + + assert out.stdout.split() == ["set()", "ok"], out.stdout + out.stderr