From 488639ce8b9c50e64b2b3fa9c0dd727221d2b872 Mon Sep 17 00:00:00 2001 From: Haozhe Zhang Date: Sun, 12 Jul 2026 23:08:03 -0700 Subject: [PATCH] Accept os.PathLike wherever a str path is accepted (#2016) Every exporter and importer entry point took str only, so passing a pathlib.Path raised AttributeError ('PosixPath' object has no attribute 'split') or a TypeError from the OCP/VTK call underneath. Widen the annotations to str | os.PathLike[str] and normalize with os.fspath() where the value reaches a string operation, OCCT, or VTK. --- cadquery/assembly.py | 16 ++++--- cadquery/cq.py | 6 +-- cadquery/occ_impl/exporters/__init__.py | 6 ++- cadquery/occ_impl/exporters/assembly.py | 26 ++++++++---- cadquery/occ_impl/exporters/dxf.py | 4 +- cadquery/occ_impl/exporters/svg.py | 3 +- cadquery/occ_impl/exporters/vtk.py | 10 ++++- cadquery/occ_impl/importers/__init__.py | 21 ++++++---- cadquery/occ_impl/importers/assembly.py | 12 +++--- cadquery/occ_impl/shapes.py | 43 ++++++++++++------- cadquery/sketch.py | 6 +-- cadquery/types.py | 3 ++ tests/test_assembly.py | 31 ++++++++++++++ tests/test_exporters.py | 55 +++++++++++++++++++++++++ tests/test_importers.py | 36 +++++++++++++++- 15 files changed, 224 insertions(+), 54 deletions(-) diff --git a/cadquery/assembly.py b/cadquery/assembly.py index fdf98dd73..f7f86e9dd 100644 --- a/cadquery/assembly.py +++ b/cadquery/assembly.py @@ -1,3 +1,5 @@ +import os + from functools import reduce from typing import ( Union, @@ -37,7 +39,7 @@ ) from .occ_impl.importers.assembly import importStep as _importStep, importXbf, importXml -from .types import UnitLiterals +from .types import PathLike, UnitLiterals from .selectors import _expression_grammar as _selector_grammar from .utils import deprecate, BiDict, instance_of @@ -536,7 +538,7 @@ def solve(self, verbosity: int = 0) -> Self: @deprecate() def save( self, - path: str, + path: PathLike, exportType: Optional[ExportLiterals] = None, mode: STEPExportModeLiterals = "default", tolerance: float = 0.1, @@ -563,7 +565,7 @@ def save( def export( self, - path: str, + path: PathLike, exportType: Optional[ExportLiterals] = None, mode: STEPExportModeLiterals = "default", tolerance: float = 0.1, @@ -596,6 +598,8 @@ def export( if mode not in get_args(STEPExportModeLiterals): raise ValueError(f"Unknown assembly export mode {mode} for STEP") + path = os.fspath(path) + if exportType is None: t = path.split(".")[-1].upper() if t in ("STEP", "XML", "XBF", "VRML", "VTKJS", "GLTF", "GLB", "STL"): @@ -628,7 +632,7 @@ def export( return self @classmethod - def importStep(cls, path: str, unit: UnitLiterals = "MM") -> Self: + def importStep(cls, path: PathLike, unit: UnitLiterals = "MM") -> Self: """ Reads an assembly from a STEP file. @@ -642,7 +646,7 @@ def importStep(cls, path: str, unit: UnitLiterals = "MM") -> Self: @classmethod def load( cls, - path: str, + path: PathLike, importType: Optional[ImportLiterals] = None, unit: UnitLiterals = "MM", ) -> Self: @@ -650,6 +654,8 @@ def load( Load step, xbf or xml. Only STEP supports unit conversion on loading. """ + path = os.fspath(path) + if importType is None: t = path.split(".")[-1].upper() if t in ("STEP", "XML", "XBF"): diff --git a/cadquery/cq.py b/cadquery/cq.py index 8d5a4cd25..936b58070 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -41,7 +41,7 @@ from .occ_impl.exporters import export from .utils import deprecate, deprecate_kwarg_name, get_arity -from .types import UnitLiterals +from .types import PathLike, UnitLiterals from .selectors import ( Selector, @@ -1040,7 +1040,7 @@ def toSvg(self, opts: Any = None) -> str: """ return getSVG(self.val(), opts) - def exportSvg(self, fileName: str) -> None: + def exportSvg(self, fileName: PathLike) -> None: """ Exports the first item on the stack as an SVG file @@ -4525,7 +4525,7 @@ def invoke( def export( self: T, - fname: str, + fname: PathLike, tolerance: float = 0.1, angularTolerance: float = 0.1, unit: UnitLiterals = "MM", diff --git a/cadquery/occ_impl/exporters/__init__.py b/cadquery/occ_impl/exporters/__init__.py index d27bc7a57..9db36ec87 100644 --- a/cadquery/occ_impl/exporters/__init__.py +++ b/cadquery/occ_impl/exporters/__init__.py @@ -9,7 +9,7 @@ from ...utils import deprecate from ..shapes import Shape, compound -from ...types import UnitLiterals +from ...types import PathLike, UnitLiterals from .svg import getSVG from .json import JsonMesh @@ -40,7 +40,7 @@ class ExportTypes: def export( w: Union[Shape, Iterable[Shape]], - fname: str, + fname: PathLike, exportType: Optional[ExportLiterals] = None, tolerance: float = 0.1, angularTolerance: float = 0.1, @@ -69,6 +69,8 @@ def export( shape: Shape f: IO + fname = os.fspath(fname) + if not opt: opt = {} diff --git a/cadquery/occ_impl/exporters/assembly.py b/cadquery/occ_impl/exporters/assembly.py index 05f9e8a89..6a13e93cb 100644 --- a/cadquery/occ_impl/exporters/assembly.py +++ b/cadquery/occ_impl/exporters/assembly.py @@ -48,7 +48,7 @@ from ..assembly import AssemblyProtocol, toCAF, toVTK, toFusedCAF from ..geom import Location from ..shapes import Shape, Compound -from ...types import UnitLiterals +from ...types import PathLike, UnitLiterals class ExportModes: @@ -61,7 +61,7 @@ class ExportModes: def exportAssembly( assy: AssemblyProtocol, - path: str, + path: PathLike, mode: STEPExportModeLiterals = "default", unit: UnitLiterals = "MM", outputUnit: Optional[UnitLiterals] = None, @@ -99,6 +99,8 @@ def exportAssembly( :type name_geometries: bool """ + path = os.fspath(path) + # Handle the extra settings for the STEP export pcurves = 1 if "write_pcurves" in kwargs and not kwargs["write_pcurves"]: @@ -168,7 +170,7 @@ def exportAssembly( def exportStepMeta( assy: AssemblyProtocol, - path: str, + path: PathLike, write_pcurves: bool = True, precision_mode: int = 0, unit: UnitLiterals = "MM", @@ -202,6 +204,8 @@ def exportStepMeta( # Initialize the XCAF document that will allow the STEP export app = XCAFApp_Application.GetApplication_s() + path = os.fspath(path) + doc = TDocStd_Document(TCollection_ExtendedString("XmlOcaf")) app.InitDocument(doc) @@ -344,11 +348,13 @@ def _process_assembly( return status == IFSelect_ReturnStatus.IFSelect_RetDone -def exportCAF(assy: AssemblyProtocol, path: str, binary: bool = False) -> bool: +def exportCAF(assy: AssemblyProtocol, path: PathLike, binary: bool = False) -> bool: """ Export an assembly to an XCAF xml or xbf file (internal OCCT formats). """ + path = os.fspath(path) + folder, fname = os.path.split(path) name, ext = os.path.splitext(fname) ext = ext[1:] if ext[0] == "." else ext @@ -405,11 +411,13 @@ def _vtkRenderWindow( return renderWindow -def exportVTKJS(assy: AssemblyProtocol, path: str): +def exportVTKJS(assy: AssemblyProtocol, path: PathLike): """ Export an assembly to a zipped vtkjs. NB: .zip extensions is added to path. """ + path = os.fspath(path) + renderWindow = _vtkRenderWindow(assy) with TemporaryDirectory() as tmpdir: @@ -423,7 +431,7 @@ def exportVTKJS(assy: AssemblyProtocol, path: str): def exportVRML( assy: AssemblyProtocol, - path: str, + path: PathLike, tolerance: float = 1e-3, angularTolerance: float = 0.1, ): @@ -431,6 +439,8 @@ def exportVRML( Export an assembly to a vrml file using vtk. """ + path = os.fspath(path) + exporter = vtkVRMLExporter() exporter.SetFileName(path) exporter.SetRenderWindow(_vtkRenderWindow(assy, tolerance, angularTolerance)) @@ -439,7 +449,7 @@ def exportVRML( def exportGLTF( assy: AssemblyProtocol, - path: str, + path: PathLike, binary: Optional[bool] = None, tolerance: float = 1e-3, angularTolerance: float = 0.1, @@ -448,6 +458,8 @@ def exportGLTF( Export an assembly to a gltf file. """ + path = os.fspath(path) + # If the caller specified the binary option, respect it if binary is None: # Handle the binary option for GLTF export based on file extension diff --git a/cadquery/occ_impl/exporters/dxf.py b/cadquery/occ_impl/exporters/dxf.py index 41ae15224..79614c3a9 100644 --- a/cadquery/occ_impl/exporters/dxf.py +++ b/cadquery/occ_impl/exporters/dxf.py @@ -14,6 +14,8 @@ ) import ezdxf + +from ...types import PathLike from ezdxf import units, zoom from ezdxf.entities import factory from OCP.GeomConvert import GeomConvert @@ -366,7 +368,7 @@ def _dxf_spline(cls, edge: Edge, plane: Plane) -> DxfEntityAttributes: def exportDXF( w: Union[WorkplaneLike, Shape, Iterable[Shape]], - fname: str, + fname: PathLike, approx: Optional[ApproxOptions] = None, tolerance: float = 1e-3, *, diff --git a/cadquery/occ_impl/exporters/svg.py b/cadquery/occ_impl/exporters/svg.py index b41dee7b0..c40131e7f 100644 --- a/cadquery/occ_impl/exporters/svg.py +++ b/cadquery/occ_impl/exporters/svg.py @@ -1,6 +1,7 @@ import io as StringIO from ..shapes import Shape, Compound, TOLERANCE +from ...types import PathLike from ..geom import BoundBox @@ -308,7 +309,7 @@ def getSVG(shape, opts=None): return svg -def exportSVG(shape, fileName: str, opts=None): +def exportSVG(shape, fileName: PathLike, opts=None): """ Accept a cadquery shape, and export it to the provided file TODO: should use file-like objects, not a fileName, and/or be able to return a string instead diff --git a/cadquery/occ_impl/exporters/vtk.py b/cadquery/occ_impl/exporters/vtk.py index c613fdd7a..9d3d58001 100644 --- a/cadquery/occ_impl/exporters/vtk.py +++ b/cadquery/occ_impl/exporters/vtk.py @@ -10,7 +10,10 @@ VTK_POLY_LINE, ) +import os + from ..shapes import Shape +from ...types import PathLike def extractEdgesFaces(data: vtkPolyData) -> tuple[vtkPolyData, vtkPolyData]: @@ -41,11 +44,14 @@ def extractEdgesFaces(data: vtkPolyData) -> tuple[vtkPolyData, vtkPolyData]: def exportVTP( - shape: Shape, fname: str, tolerance: float = 0.1, angularTolerance: float = 0.1 + shape: Shape, + fname: PathLike, + tolerance: float = 0.1, + angularTolerance: float = 0.1, ): writer = vtkXMLPolyDataWriter() - writer.SetFileName(fname) + writer.SetFileName(os.fspath(fname)) writer.SetInputData(shape.toVtkPolyData(tolerance, angularTolerance)) writer.Write() diff --git a/cadquery/occ_impl/importers/__init__.py b/cadquery/occ_impl/importers/__init__.py index 51eeb26e3..58e4cf06f 100644 --- a/cadquery/occ_impl/importers/__init__.py +++ b/cadquery/occ_impl/importers/__init__.py @@ -1,3 +1,5 @@ +import os + from math import pi from typing import List, Literal @@ -8,7 +10,7 @@ from ... import cq from ..shapes import Shape from .dxf import _importDXF -from ...types import UnitLiterals +from ...types import PathLike, UnitLiterals RAD2DEG = 360.0 / (2 * pi) @@ -27,7 +29,7 @@ class UNITS: def importShape( importType: Literal["STEP", "DXF", "BREP", "BIN"], - fileName: str, + fileName: PathLike, unit: UnitLiterals = "MM", *args, **kwargs, @@ -54,7 +56,7 @@ def importShape( raise RuntimeError("Unsupported import type: {!r}".format(importType)) -def importBrep(fileName: str) -> "cq.Workplane": +def importBrep(fileName: PathLike) -> "cq.Workplane": """ Loads the BREP file as a single shape into a cadquery Workplane. @@ -71,7 +73,7 @@ def importBrep(fileName: str) -> "cq.Workplane": return cq.Workplane("XY").newObject([shape]) -def importBin(fileName: str) -> "cq.Workplane": +def importBin(fileName: PathLike) -> "cq.Workplane": """ Loads the binary BREP file as a single shape into a cadquery Workplane. @@ -83,7 +85,7 @@ def importBin(fileName: str) -> "cq.Workplane": return cq.Workplane("XY").newObject([shape]) -def _importStep(fileName: str, unit: UnitLiterals = "MM") -> list["Shape"]: +def _importStep(fileName: PathLike, unit: UnitLiterals = "MM") -> list["Shape"]: """ Private helper for implementing different STEP importers. """ @@ -93,7 +95,7 @@ def _importStep(fileName: str, unit: UnitLiterals = "MM") -> list["Shape"]: # Now read and return the shape reader = STEPControl_Reader() - readStatus = reader.ReadFile(fileName) + readStatus = reader.ReadFile(os.fspath(fileName)) if readStatus != OCP.IFSelect.IFSelect_RetDone: raise ValueError("STEP File could not be loaded") for i in range(reader.NbRootsForTransfer()): @@ -105,7 +107,7 @@ def _importStep(fileName: str, unit: UnitLiterals = "MM") -> list["Shape"]: # Loads a STEP file into a CQ.Workplane object -def importStep(fileName: str, unit: UnitLiterals = "MM") -> "cq.Workplane": +def importStep(fileName: PathLike, unit: UnitLiterals = "MM") -> "cq.Workplane": """ Accepts a file name and loads the STEP file into a cadquery Workplane @@ -119,7 +121,10 @@ def importStep(fileName: str, unit: UnitLiterals = "MM") -> "cq.Workplane": def importDXF( - filename: str, tol: float = 1e-6, exclude: List[str] = [], include: List[str] = [] + filename: PathLike, + tol: float = 1e-6, + exclude: List[str] = [], + include: List[str] = [], ) -> "cq.Workplane": """ Loads a DXF file into a Workplane. diff --git a/cadquery/occ_impl/importers/assembly.py b/cadquery/occ_impl/importers/assembly.py index 9bff94922..9d3d775ef 100644 --- a/cadquery/occ_impl/importers/assembly.py +++ b/cadquery/occ_impl/importers/assembly.py @@ -1,3 +1,5 @@ +import os + from typing import cast from pathlib import Path @@ -26,7 +28,7 @@ from ..assembly import AssemblyProtocol, Color, Material from ..geom import Location from ..shapes import Shape -from ...types import UnitLiterals +from ...types import PathLike, UnitLiterals def _get_name(label: TDF_Label) -> str: @@ -130,7 +132,7 @@ def _get_shape_color(s: TopoDS_Shape, color_tool: XCAFDoc_ColorTool) -> Color | return rv -def importStep(assy: AssemblyProtocol, path: str, unit: UnitLiterals = "MM"): +def importStep(assy: AssemblyProtocol, path: PathLike, unit: UnitLiterals = "MM"): """ Import a step file into an assembly. @@ -154,7 +156,7 @@ def importStep(assy: AssemblyProtocol, path: str, unit: UnitLiterals = "MM"): Interface_Static.SetCVal_s("xstep.cascade.unit", unit.upper()) # Read the STEP file - status = step_reader.ReadFile(path) + status = step_reader.ReadFile(os.fspath(path)) if status != IFSelect_RetDone: raise ValueError(f"Error reading STEP file: {path}") @@ -167,7 +169,7 @@ def importStep(assy: AssemblyProtocol, path: str, unit: UnitLiterals = "MM"): _importDoc(doc, assy) -def importXbf(assy: AssemblyProtocol, path: str): +def importXbf(assy: AssemblyProtocol, path: PathLike): """ Import an xbf file into an assembly. @@ -197,7 +199,7 @@ def importXbf(assy: AssemblyProtocol, path: str): _importDoc(doc, assy) -def importXml(assy: AssemblyProtocol, path: str): +def importXml(assy: AssemblyProtocol, path: PathLike): """ Import an xcaf xml file into an assembly. diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index d7791cc55..7bc404290 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -14,6 +14,8 @@ overload, ) +import os + from io import BytesIO from warnings import warn @@ -32,7 +34,7 @@ ) from ..utils import multimethod, multidispatch, mypyclassmethod -from ..types import UnitLiterals +from ..types import PathLike, UnitLiterals # change default OCCT logging level from OCP.Message import Message, Message_Gravity @@ -343,6 +345,15 @@ TOLERANCE = 1e-6 + +def _fspath(f: PathLike | BytesIO) -> str | BytesIO: + """ + Convert a path-like object to str, pass file objects through unchanged. + """ + + return os.fspath(f) if isinstance(f, os.PathLike) else f + + shape_LUT = { ta.TopAbs_VERTEX: "Vertex", ta.TopAbs_EDGE: "Edge", @@ -489,7 +500,7 @@ def cast(cls, obj: TopoDS_Shape, forConstruction: bool = False) -> Shape: def exportStl( self, - fileName: str, + fileName: PathLike, tolerance: float = 1e-3, angularTolerance: float = 0.1, ascii: bool = False, @@ -518,11 +529,11 @@ def exportStl( writer = StlAPI_Writer() writer.ASCIIMode = ascii - return writer.Write(self.wrapped, fileName) + return writer.Write(self.wrapped, os.fspath(fileName)) def exportStep( self, - fileName: str, + fileName: PathLike, unit: UnitLiterals = "MM", outputUnit: UnitLiterals | None = None, **kwargs: Any, @@ -532,7 +543,7 @@ def exportStep( kwargs is used to provide additional optional keyword arguments to configure the exporter. :param fileName: Path and filename for writing. - :type fileName: str + :type fileName: str or os.PathLike :param unit: The internal unit of the model's geometry values. Default "MM". :type unit: UnitLiterals :param outputUnit: The unit to use in the STEP file header. If None, defaults to the value of ``unit``. @@ -562,54 +573,54 @@ def exportStep( ) writer.Transfer(self.wrapped, STEPControl_AsIs) - return writer.Write(fileName) + return writer.Write(os.fspath(fileName)) - def exportBrep(self, f: str | BytesIO) -> bool: + def exportBrep(self, f: PathLike | BytesIO) -> bool: """ Export this shape to a BREP file """ - rv = BRepTools.Write_s(self.wrapped, f) + rv = BRepTools.Write_s(self.wrapped, _fspath(f)) return True if rv is None else rv @classmethod - def importBrep(cls, f: str | BytesIO) -> Shape: + def importBrep(cls, f: PathLike | BytesIO) -> Shape: """ Import shape from a BREP file """ s = TopoDS_Shape() builder = BRep_Builder() - BRepTools.Read_s(s, f, builder) + BRepTools.Read_s(s, _fspath(f), builder) if s.IsNull(): raise ValueError(f"Could not import {f}") return cls.cast(s) - def exportBin(self, f: str | BytesIO) -> bool: + def exportBin(self, f: PathLike | BytesIO) -> bool: """ Export this shape to a binary BREP file. """ - rv = BinTools.Write_s(self.wrapped, f) + rv = BinTools.Write_s(self.wrapped, _fspath(f)) return True if rv is None else rv @classmethod - def importBin(cls, f: str | BytesIO) -> Shape: + def importBin(cls, f: PathLike | BytesIO) -> Shape: """ Import shape from a binary BREP file. """ s = TopoDS_Shape() - BinTools.Read_s(s, f) + BinTools.Read_s(s, _fspath(f)) return cls.cast(s) @classmethod - def importStep(cls, fileName: str, unit: UnitLiterals = "MM") -> Shape: + def importStep(cls, fileName: PathLike, unit: UnitLiterals = "MM") -> Shape: """ Import shape from a STEP file. @@ -1854,7 +1865,7 @@ def __truediv__(self, other: Shape) -> Shape: def export( self: T, - fname: str, + fname: PathLike, tolerance: float = 0.1, angularTolerance: float = 0.1, unit: UnitLiterals = "MM", diff --git a/cadquery/sketch.py b/cadquery/sketch.py index e66749053..caeb0ddf9 100644 --- a/cadquery/sketch.py +++ b/cadquery/sketch.py @@ -22,7 +22,7 @@ from .hull import find_hull from .selectors import StringSyntaxSelector, Selector -from .types import Real, UnitLiterals +from .types import PathLike, Real, UnitLiterals from .utils import get_arity, instance_of from .occ_impl.shapes import ( @@ -220,7 +220,7 @@ def face( def importDXF( self: T, - filename: str, + filename: PathLike, tol: float = 1e-6, exclude: List[str] = [], include: List[str] = [], @@ -1346,7 +1346,7 @@ def invoke( def export( self: T, - fname: str, + fname: PathLike, tolerance: float = 0.1, angularTolerance: float = 0.1, unit: UnitLiterals = "MM", diff --git a/cadquery/types.py b/cadquery/types.py index a178526bc..e816a364a 100644 --- a/cadquery/types.py +++ b/cadquery/types.py @@ -1,4 +1,7 @@ +import os + from typing import Union, Literal UnitLiterals = Literal["MM", "CM", "M", "KM", "INCH", "FT", "MI", "UM", "NM"] Real = Union[int, float] +PathLike = Union[str, "os.PathLike[str]"] diff --git a/tests/test_assembly.py b/tests/test_assembly.py index 08d4a5f21..92888e4b5 100644 --- a/tests/test_assembly.py +++ b/tests/test_assembly.py @@ -2603,3 +2603,34 @@ def test_name_geometries(tmpdir): assert len([l for l in lines if "top_face" in l]) == 2 assert len([l for l in lines if "plane_" in l]) == 2 assert len([l for l in lines if "seg_" in l]) == 3 + + +def test_pathlike(simple_assy, tmpdir): + """ + Assembly exporters and loaders should accept os.PathLike objects, not only str. + """ + + for ext in ("step", "xml", "xbf", "vrml", "gltf", "glb", "stl"): + fname = Path(tmpdir) / f"pathlike.{ext}" + simple_assy.export(fname) + assert fname.exists() + + # exportStepMeta is not reachable through Assembly.export + fname = Path(tmpdir) / "pathlike_meta.step" + exportStepMeta(simple_assy, fname) + assert fname.exists() + + # NB: .zip is appended by the vtkjs exporter + fname = Path(tmpdir) / "pathlike_vtkjs" + exportVTKJS(simple_assy, fname) + assert fname.with_suffix(".zip").exists() + + # loading from a Path gives the same result as loading from a str + fname = Path(tmpdir) / "pathlike.step" + + assert cq.Assembly.load(fname).toCompound().Volume() == approx( + cq.Assembly.load(str(fname)).toCompound().Volume() + ) + assert cq.Assembly.importStep(fname).toCompound().Volume() == approx( + cq.Assembly.importStep(str(fname)).toCompound().Volume() + ) diff --git a/tests/test_exporters.py b/tests/test_exporters.py index e6bdd555c..02d40477c 100644 --- a/tests/test_exporters.py +++ b/tests/test_exporters.py @@ -1052,3 +1052,58 @@ def test_toVTK(): edge_data = edge_actor.GetMapper().GetInput() assert edge_data.GetNumberOfLines() > 0 assert edge_data.GetNumberOfPolys() == 0 + + +def test_export_pathlike(tmpdir, box123): + """ + Exporters should accept os.PathLike objects, not only str. + """ + + for ext in ( + "step", + "stl", + "brep", + "bin", + "svg", + "vtp", + "3mf", + "amf", + "vrml", + "tjs", + ): + fname = Path(tmpdir) / f"pathlike.{ext}" + exporters.export(box123, fname) + assert fname.exists() + + # DXF export works on 2D sections + fname = Path(tmpdir) / "pathlike.dxf" + exporters.export(Workplane().rect(1, 1), fname) + assert fname.exists() + + shape = box123.val() + + for name, method in ( + ("step", shape.exportStep), + ("stl", shape.exportStl), + ("brep", shape.exportBrep), + ("bin", shape.exportBin), + ): + fname = Path(tmpdir) / f"pathlike_shape.{name}" + method(fname) + assert fname.exists() + + fname = Path(tmpdir) / "pathlike_shape_export.step" + shape.export(fname) + assert fname.exists() + + fname = Path(tmpdir) / "pathlike_workplane.step" + box123.export(fname) + assert fname.exists() + + fname = Path(tmpdir) / "pathlike_workplane.svg" + box123.exportSvg(fname) + assert fname.exists() + + fname = Path(tmpdir) / "pathlike_sketch.dxf" + Sketch().rect(1, 1).export(fname) + assert fname.exists() diff --git a/tests/test_importers.py b/tests/test_importers.py index ebb51fe6d..1f1e289f5 100644 --- a/tests/test_importers.py +++ b/tests/test_importers.py @@ -4,8 +4,9 @@ # core modules import tempfile import os +from pathlib import Path -from cadquery import importers, Workplane, Compound +from cadquery import exporters, importers, Workplane, Compound from tests import BaseTest from pytest import approx, raises @@ -311,6 +312,39 @@ def testImportDXF(self): threed = tmp.extrude(extrusion_value) self.assertEqual(threed.findSolid().BoundingBox().zlen, extrusion_value) + def testPathLike(self): + """ + Importers should accept os.PathLike objects, not only str. + """ + box = Workplane().box(1, 2, 3) + + for importType, ext in ( + (importers.ImportTypes.STEP, "step"), + (importers.ImportTypes.BREP, "brep"), + (importers.ImportTypes.BIN, "bin"), + ): + fname = Path(OUTDIR) / f"pathlike.{ext}" + exporters.export(box, fname) + + imported = importers.importShape(importType, fname) + self.assertAlmostEqual(imported.val().Volume(), 6, 4) + + self.assertAlmostEqual( + importers.importStep(Path(OUTDIR) / "pathlike.step").val().Volume(), 6, 4 + ) + self.assertAlmostEqual( + importers.importBrep(Path(OUTDIR) / "pathlike.brep").val().Volume(), 6, 4 + ) + self.assertAlmostEqual( + importers.importBin(Path(OUTDIR) / "pathlike.bin").val().Volume(), 6, 4 + ) + + # DXF import works on 2D profiles + fname = Path(OUTDIR) / "pathlike.dxf" + exporters.export(Workplane().rect(2, 1), fname) + + self.assertEqual(len(importers.importDXF(fname).wires().vals()), 1) + if __name__ == "__main__": import unittest