Source code for pybosl2.exceptions
# Copyright (c) 2026, pinkfish
#
# Licensed under the BSD 2-Clause License. See the LICENSE file in the project
# root for the full license text.
# SPDX-License-Identifier: BSD-2-Clause
# DocCategory: internal
"""Exceptions for the dual-backend (CSG / SDF) solid system."""
# Exceptions for the dual-backend (CSG / SDF) solid system. pybosl2 realizes solids through either
# the exact-CSG backend (PythonSCAD, the default) or the F-Rep/signed-distance backend (libfive).
# These errors make the two failure modes of a two-backend world explicit: asking a backend for a
# feature it cannot express (:class:`UnsupportedByBackendError`), and combining solids that live on
# different backends (:class:`CrossBackendError`, which tells you how to convert).
#
from __future__ import annotations
__all__ = [
"Bosl2Error",
"Bosl2ValueError",
"Bosl2NotImplementedError",
"UnsupportedByBackendError",
"CrossBackendError",
]
class Bosl2Error(Exception):
"""Base class for pybosl2's own errors.
Every error the library raises derives from this (SPEC E-1), including the argument-validation
errors -- see :class:`Bosl2ValueError` for how that coexists with E-4's requirement that bad
input raises a :class:`ValueError`.
"""
class Bosl2ValueError(Bosl2Error, ValueError):
"""Bad input to a pybosl2 call: a ``ValueError`` *and* a :class:`Bosl2Error`.
SPEC E-4 requires argument validation to raise ``ValueError`` -- what a Python caller expects
for a bad argument -- and SPEC E-1 requires every library error to derive from one base so the
family can be caught with a single ``except``. Deriving from both is what makes the two
compatible: code that already catches ``ValueError`` is unaffected, and
``except Bosl2Error`` starts catching the ~570 validation sites that it previously missed.
Raised for a bad argument value; a bad argument *type* or arity is still Python's own
``TypeError``.
Examples:
Both spellings catch it::
from pybosl2 import Bosl2Error, cyl
try:
cyl(height=10, radius=5, diameter=10)
except Bosl2Error as err:
print(err) # give radius or diameter, not both (radius=5, diameter=10)
"""
class Bosl2NotImplementedError(Bosl2Error, NotImplementedError):
"""A capability this port advertises but does not yet build: a ``NotImplementedError`` *and* a :class:`Bosl2Error`.
The same reasoning as :class:`Bosl2ValueError`, for the same reason. Four public callables
raised a bare ``NotImplementedError`` -- ``cyl(texture=...)``, ``cuboid(teardrop=...)``,
``CapType.CIRCLE`` and ``VNF.from_field`` with a range -- so ``except Bosl2Error`` missed
them, and none named an alternative, which SPEC E-2 asks of every refusal.
A parameter that raises this is one the signature advertises and the port does not honour. It
is a gap, not a design decision, and the message says what to do meanwhile.
"""
[docs]
class UnsupportedByBackendError(Bosl2Error, AttributeError):
"""A feature the active backend cannot express.
Also an :class:`AttributeError`, because a backend refuses most often from ``__getattr__`` and
Python's attribute protocol is defined in terms of that type (SPEC E-6): ``hasattr()`` and
``getattr(obj, name, default)`` catch ``AttributeError`` and nothing else, so any other type
turns a capability probe into a traceback and breaks ``copy``, ``pickle``, ``inspect`` and
every REPL completion. The extra base is invisible to a refusal raised from a call.
Raised, rather than silently producing different geometry, when a call needs something the chosen
backend has no faithful equivalent for -- e.g. the BOSL2 attachment/anchor system on the ``"sdf"``
backend, or a smooth-blend union on the ``"csg"`` backend.
Args:
feature: short name of the unsupported operation (e.g. ``"attach"``, ``"smooth_union"``).
backend: the active backend that cannot do it (``"csg"`` / ``"sdf"``).
hint: optional guidance (an alternative call, or which backend does support it).
"""
def __init__(self, feature: str, backend: str, hint: str | None = None) -> None:
"""Initialize the instance."""
self.feature = feature
self.backend = backend
self.hint = hint
msg = f"{feature!r} is not supported by the {backend!r} backend"
if hint:
msg += f". {hint}"
super().__init__(msg)
[docs]
class CrossBackendError(Bosl2Error):
"""A boolean/transform combined solids from two different backends.
Solids must share a backend to be combined. Convert one first: meshing an SDF solid into a CSG
polyhedron is exact (``sdf_solid.to_csg()``); voxel-sampling a CSG solid into an SDF is lossy and
opt-in (``csg_solid.to_sdf(voxel_size=...)``).
Args:
left: backend of the left operand.
right: backend of the right operand.
"""
def __init__(self, left: str, right: str) -> None:
"""Initialize the instance."""
self.left = left
self.right = right
super().__init__(
f"cannot combine a {left!r}-backend solid with a {right!r}-backend solid -- operands "
f"must share a backend. Bring the SDF solid into the CSG world with `.to_csg()` (an "
f"exact mesh->polyhedron) and combine there. (Direct CSG->SDF conversion is not supported.)"
)