Repository: mikedh/trimesh
Pull request #2197 (2024-03-28 17:12:16)

Release: fstring
- release #2196 
- enable `RUF` rules as [RUF027](https://docs.astral.sh/ruff/rules/missing-f-string-syntax/#missing-f-string-syntax-ruf027) would have caught prior bug. 
- apply `ruff check trimesh --preview --fix --unsafe-fixes`


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 46ab54f1..b46f5d2f 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -20,7 +20,7 @@ jobs:
       run: |
         pip install ruff
     - name: Check Formatting
-      run: ruff .
+      run: ruff check .
   tests:
     name: Run Unit Tests
     needs: formatting
diff --git a/pyproject.toml b/pyproject.toml
index ab8923c3..4d4bdc15 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,7 +5,7 @@ requires = ["setuptools >= 61.0", "wheel"]
 [project]
 name = "trimesh"
 requires-python = ">=3.7"
-version = "4.2.2"
+version = "4.2.3"
 authors = [{name = "Michael Dawson-Haggerty", email = "mikedh@kerfed.com"}]
 license = {file = "LICENSE.md"}
 description = "Import, export, process, analyze and view triangular meshes."
@@ -129,7 +129,7 @@ select = [
     "E", # style errors
     "F", # flakes
     "I", # import sorting
-    "RUF100", # meta
+    "RUF", # ruff specific rules
     "UP", # upgrade
     "W", # style warnings
     "YTT", # sys.version
@@ -143,6 +143,7 @@ ignore = [
   "E501", # Line too long ({width} > {limit} characters)
   "B904", # raise ... from err
   "B905", # zip() without an explicit strict= parameter
+  "RUF005", # recommends non-type-aware expansion
 ]
 
 # don't allow implicit string concatenation
diff --git a/trimesh/__init__.py b/trimesh/__init__.py
index 82986922..7a047208 100644
--- a/trimesh/__init__.py
+++ b/trimesh/__init__.py
@@ -39,8 +39,6 @@ from . import (
     units,
     util,
 )
-
-# geometry objects
 from .base import Trimesh
 
 # general numeric tolerances
@@ -48,6 +46,9 @@ from .constants import tol
 
 # loader functions
 from .exchange.load import available_formats, load, load_mesh, load_path, load_remote
+
+# geometry objects
+from .parent import Geometry
 from .points import PointCloud
 from .scene.scene import Scene
 from .transformations import transform_points
@@ -73,9 +74,10 @@ except BaseException as E:
 
 __all__ = [
     "PointCloud",
+    "Geometry",
     "Trimesh",
     "Scene",
-    "util",
+    "Trimesh",
     "__version__",
     "available_formats",
     "boolean",
@@ -87,6 +89,7 @@ __all__ = [
     "creation",
     "curvature",
     "decomposition",
+    "exceptions",
     "geometry",
     "graph",
     "grouping",
@@ -114,7 +117,7 @@ __all__ = [
     "triangles",
     "unitize",
     "units",
+    "util",
     "utilScene",
     "voxel",
-    "exceptions",
 ]
diff --git a/trimesh/caching.py b/trimesh/caching.py
index 2fbe4f85..05aea98f 100644
--- a/trimesh/caching.py
+++ b/trimesh/caching.py
@@ -654,7 +654,7 @@ class DataStore(Mapping):
                 # will raise if this is not a hashable type
                 hash(data)
             except BaseException:
-                raise ValueError("unhashable `{key}:{type(data)}`")
+                raise ValueError(f"unhashable `{key}:{type(data)}`")
             tracked = data
 
         # apply our mutability setting
diff --git a/trimesh/exchange/binvox.py b/trimesh/exchange/binvox.py
index bef8e9a8..5480f37a 100644
--- a/trimesh/exchange/binvox.py
+++ b/trimesh/exchange/binvox.py
@@ -426,7 +426,7 @@ class Binvoxer:
             raise ValueError("Maximum dimension using exact is 1024, got %d" % dimension)
         if file_type not in Binvoxer.SUPPORTED_OUTPUT_TYPES:
             raise ValueError(
-                f"file_type {file_type} not in set of supported output types {str(Binvoxer.SUPPORTED_OUTPUT_TYPES)}"
+                f"file_type {file_type} not in set of supported output types {Binvoxer.SUPPORTED_OUTPUT_TYPES!s}"
             )
         args = [encoder, "-d", str(dimension), "-t", file_type]
         if exact:
@@ -513,7 +513,7 @@ class Binvoxer:
         ext = ext[1:].lower()
         if ext not in Binvoxer.SUPPORTED_INPUT_TYPES:
             raise ValueError(
-                f"file_type {ext} not in set of supported input types {str(Binvoxer.SUPPORTED_INPUT_TYPES)}"
+                f"file_type {ext} not in set of supported input types {Binvoxer.SUPPORTED_INPUT_TYPES!s}"
             )
         out_path = f"{head}.{self._file_type}"
         if os.path.isfile(out_path) and not overwrite:
diff --git a/trimesh/exchange/load.py b/trimesh/exchange/load.py
index 8922bb05..34f71c0a 100644
--- a/trimesh/exchange/load.py
+++ b/trimesh/exchange/load.py
@@ -214,9 +214,7 @@ def load_mesh(
             loaded = loaded[0]
 
         # show the repr for loaded, loader used, and time
-        log.debug(
-            f"loaded {str(loaded)} using `{loader.__name__}` in {now() - tic:0.4f}s"
-        )
+        log.debug(f"loaded {loaded!s} using `{loader.__name__}` in {now() - tic:0.4f}s")
     finally:
         # if we failed to load close file
         if opened:
diff --git a/trimesh/exchange/stl.py b/trimesh/exchange/stl.py
index d5e5e47a..a82a6d0d 100644
--- a/trimesh/exchange/stl.py
+++ b/trimesh/exchange/stl.py
@@ -309,7 +309,7 @@ def export_stl_ascii(mesh) -> str:
         name = ""
 
     # concatenate the header, data, and footer, and a new line
-    return "\n".join(["solid {name}", formatter.format(*blob.reshape(-1)), "endsolid\n"])
+    return "\n".join([f"solid {name}", formatter.format(*blob.reshape(-1)), "endsolid\n"])
 
 
 _stl_loaders = {"stl": load_stl, "stl_ascii": load_stl}
diff --git a/trimesh/exchange/threemf.py b/trimesh/exchange/threemf.py
index c6a254f1..f1c02449 100644
--- a/trimesh/exchange/threemf.py
+++ b/trimesh/exchange/threemf.py
@@ -29,7 +29,7 @@ def load_3MF(file_obj, postprocess=True, **kwargs):
     model = next(iter(v for k, v in archive.items() if "3d/3dmodel.model" in k.lower()))
 
     # read root attributes only from XML first
-    event, root = next(etree.iterparse(model, tag=("{*}model"), events=("start",)))
+    _event, root = next(etree.iterparse(model, tag=("{*}model"), events=("start",)))
     # collect unit information from the tree
     if "unit" in root.attrib:
         metadata = {"units": root.attrib["unit"]}
diff --git a/trimesh/graph.py b/trimesh/graph.py
index 22f77c1b..f3f670df 100644
--- a/trimesh/graph.py
+++ b/trimesh/graph.py
@@ -487,7 +487,7 @@ def connected_component_labels(edges, node_count=None):
         Component labels for each node
     """
     matrix = edges_to_coo(edges, node_count)
-    body_count, labels = csgraph.connected_components(matrix, directed=False)
+    _body_count, labels = csgraph.connected_components(matrix, directed=False)
 
     if node_count is not None:
         assert len(labels) == node_count
diff --git a/trimesh/nsphere.py b/trimesh/nsphere.py
index 6475557f..a23de86c 100644
--- a/trimesh/nsphere.py
+++ b/trimesh/nsphere.py
@@ -187,6 +187,6 @@ def is_nsphere(points):
     check : bool
       True if input points are on an nsphere
     """
-    center, radius, error = fit_nsphere(points)
+    _center, _radius, error = fit_nsphere(points)
     check = error < tol.merge
     return check
diff --git a/trimesh/path/exchange/load.py b/trimesh/path/exchange/load.py
index b7baaaab..f3d012b2 100644
--- a/trimesh/path/exchange/load.py
+++ b/trimesh/path/exchange/load.py
@@ -66,7 +66,7 @@ def load_path(file_obj, file_type=None, **kwargs):
         raise ValueError("Not a supported object type!")
 
     result = load_kwargs(kwargs)
-    util.log.debug(f"loaded {str(result)} in {util.now() - tic:0.4f}s")
+    util.log.debug(f"loaded {result!s} in {util.now() - tic:0.4f}s")
 
     return result
 
diff --git a/trimesh/path/path.py b/trimesh/path/path.py
index 7d615e18..50ce9702 100644
--- a/trimesh/path/path.py
+++ b/trimesh/path/path.py
@@ -403,7 +403,7 @@ class Path(parent.Geometry):
         graph : networkx.Graph
           Holds vertex indexes
         """
-        graph, closed = traversal.vertex_graph(self.entities)
+        graph, _closed = traversal.vertex_graph(self.entities)
         return graph
 
     @caching.cache_decorator
@@ -606,7 +606,7 @@ class Path(parent.Geometry):
         self.entities: length same or shorter
         """
         entity_hashes = np.array([hash(i) for i in self.entities])
-        unique, inverse = grouping.unique_rows(entity_hashes)
+        unique, _inverse = grouping.unique_rows(entity_hashes)
         if len(unique) != len(self.entities):
             self.entities = self.entities[unique]
 
diff --git a/trimesh/path/segments.py b/trimesh/path/segments.py
index b8a0fc2e..840afb4b 100644
--- a/trimesh/path/segments.py
+++ b/trimesh/path/segments.py
@@ -121,7 +121,7 @@ def colinear_pairs(segments, radius=0.01, angle=0.01, length=None):
     # convert segments to parameterized origins
     # which are the closest point on the line to
     # the actual zero- origin
-    origins, vectors, param = segments_to_parameters(segments)
+    origins, vectors, _param = segments_to_parameters(segments)
 
     # create a kdtree for origins
     tree = spatial.cKDTree(origins)
diff --git a/trimesh/path/simplify.py b/trimesh/path/simplify.py
index 13ca517d..04c5b609 100644
--- a/trimesh/path/simplify.py
+++ b/trimesh/path/simplify.py
@@ -283,7 +283,7 @@ def points_to_spline_entity(points, smooth=None, count=None):
     points = np.asanyarray(points, dtype=np.float64)
     closed = np.linalg.norm(points[0] - points[-1]) < tol.merge
 
-    knots, control, degree = splprep(points.T, s=smooth)[0]
+    knots, control, _degree = splprep(points.T, s=smooth)[0]
     control = np.transpose(control)
     index = np.arange(len(control))
 
diff --git a/trimesh/points.py b/trimesh/points.py
index f5cf44f7..7ff60ec2 100644
--- a/trimesh/points.py
+++ b/trimesh/points.py
@@ -59,7 +59,7 @@ def major_axis(points):
     axis : (dimension,) float
       Vector along approximate major axis
     """
-    U, S, V = np.linalg.svd(points)
+    _U, S, V = np.linalg.svd(points)
     axis = util.unitize(np.dot(S, V))
     return axis
 
@@ -269,7 +269,7 @@ def k_means(points, k, **kwargs):
     points_std = points.std(axis=0)
     points_std[points_std < tol.zero] = 1
     whitened = points / points_std
-    centroids_whitened, distortion = kmeans(whitened, k, **kwargs)
+    centroids_whitened, _distortion = kmeans(whitened, k, **kwargs)
     centroids = centroids_whitened * points_std
 
     # find which centroid each point is closest to
diff --git a/trimesh/proximity.py b/trimesh/proximity.py
index 2332bafc..e7a42316 100644
--- a/trimesh/proximity.py
+++ b/trimesh/proximity.py
@@ -400,7 +400,7 @@ def longest_ray(mesh, points, directions):
     if len(points) != len(directions):
         raise ValueError("number of points must equal number of directions!")
 
-    faces, rays, locations = mesh.ray.intersects_id(
+    _faces, rays, locations = mesh.ray.intersects_id(
         points, directions, return_locations=True, multiple_hits=True
     )
     if len(rays) > 0:
@@ -503,7 +503,7 @@ def max_tangent_sphere(
     n_iter = 0
     while not_converged.sum() > 0 and n_iter < max_iter:
         n_iter += 1
-        n_points, n_dists, n_faces = mesh.nearest.on_surface(centers[not_converged])
+        n_points, n_dists, _n_faces = mesh.nearest.on_surface(centers[not_converged])
 
         # If the distance to the nearest point is the same as the distance
         # to the start point then we are done.
@@ -572,7 +572,7 @@ def thickness(mesh, points, exterior=False, normals=None, method="max_sphere"):
         normals = mesh.face_normals[closest_point(mesh, points)[2]]
 
     if method == "max_sphere":
-        centers, radius = max_tangent_sphere(
+        _centers, radius = max_tangent_sphere(
             mesh=mesh, points=points, inwards=not exterior, normals=normals
         )
         thickness = radius * 2
diff --git a/trimesh/ray/__init__.py b/trimesh/ray/__init__.py
index 373b4ef0..fa8812a9 100644
--- a/trimesh/ray/__init__.py
+++ b/trimesh/ray/__init__.py
@@ -12,4 +12,4 @@ except BaseException as E:
     has_embree = False
 
 # add to __all__ as per pep8
-__all__ = ["ray_triangle", "ray_pyembree"]
+__all__ = ["ray_pyembree", "ray_triangle"]
diff --git a/trimesh/ray/ray_triangle.py b/trimesh/ray/ray_triangle.py
index 805909eb..48ad767e 100644
--- a/trimesh/ray/ray_triangle.py
+++ b/trimesh/ray/ray_triangle.py
@@ -146,7 +146,7 @@ class RayMeshIntersector:
         hit : (m,) bool
           Whether any ray hit any triangle on the mesh
         """
-        index_tri, index_ray = self.intersects_id(ray_origins, ray_directions)
+        _index_tri, index_ray = self.intersects_id(ray_origins, ray_directions)
         hit_any = np.zeros(len(ray_origins), dtype=bool)
         hit_idx = np.unique(index_ray)
         if len(hit_idx) > 0:
diff --git a/trimesh/ray/ray_util.py b/trimesh/ray/ray_util.py
index 292c7c1a..57293eae 100644
--- a/trimesh/ray/ray_util.py
+++ b/trimesh/ray/ray_util.py
@@ -51,7 +51,7 @@ def contains_points(intersector, points, check_direction=None):
         )
 
     # cast a ray both forwards and backwards
-    location, index_ray, c = intersector.intersects_location(
+    _location, index_ray, _c = intersector.intersects_location(
         np.vstack((points[inside_aabb], points[inside_aabb])),
         np.vstack((ray_directions, -ray_directions)),
     )
diff --git a/trimesh/registration.py b/trimesh/registration.py
index 21e476f7..83600ed4 100644
--- a/trimesh/registration.py
+++ b/trimesh/registration.py
@@ -147,7 +147,7 @@ def mesh_other(
         )
 
         # run first pass ICP
-        matrix, junk, cost = icp(
+        matrix, _junk, cost = icp(
             a=points, b=search, initial=a_to_b, max_iterations=int(icp_first), scale=scale
         )
 
@@ -156,7 +156,7 @@ def mesh_other(
         costs[i] = cost
 
     # run a final ICP refinement step
-    matrix, junk, cost = icp(
+    matrix, _junk, cost = icp(
         a=points,
         b=search,
         initial=transforms[np.argmin(costs)],
@@ -268,7 +268,7 @@ def procrustes(
             ((b - bcenter) / bscale).T, ((a - acenter) / ascale) * w.reshape((-1, 1))
         )
 
-    u, s, vh = np.linalg.svd(target)
+    u, _s, vh = np.linalg.svd(target)
 
     if reflection:
         R = np.dot(u, vh)
@@ -349,9 +349,9 @@ def icp(a, b, initial=None, threshold=1e-5, max_iterations=20, **kwargs):
     for _ in range(max_iterations):
         # Closest point in b to each point in a
         if is_mesh:
-            closest, distance, faces = b.nearest.on_surface(a)
+            closest, _distance, _faces = b.nearest.on_surface(a)
         else:
-            distances, ix = btree.query(a, 1)
+            _distances, ix = btree.query(a, 1)
             closest = b[ix]
 
         # align a with closest points
diff --git a/trimesh/typed.py b/trimesh/typed.py
index e8e6bc18..a9561193 100644
--- a/trimesh/typed.py
+++ b/trimesh/typed.py
@@ -41,21 +41,21 @@ Integer = Union[int, integer, unsignedinteger]
 Number = Union[float, floating, Integer]
 
 __all__ = [
-    "NDArray",
-    "ArrayLike",
-    "Optional",
-    "Sequence",
-    "Iterable",
-    "Loadable",
     "IO",
+    "Any",
+    "ArrayLike",
     "BinaryIO",
-    "List",
     "Dict",
-    "Any",
+    "Integer",
+    "Iterable",
+    "List",
+    "Loadable",
+    "NDArray",
+    "Number",
+    "Optional",
+    "Sequence",
+    "Stream",
     "Tuple",
     "float64",
     "int64",
-    "Number",
-    "Integer",
-    "Stream",
 ]
diff --git a/trimesh/util.py b/trimesh/util.py
index 1f3a5331..aaf03d00 100644
--- a/trimesh/util.py
+++ b/trimesh/util.py
@@ -237,10 +237,8 @@ def is_sequence(obj) -> bool:
     is_sequence : bool
         True if object is sequence
     """
-    seq = (
-        not hasattr(obj, "strip")
-        and hasattr(obj, "__getitem__")
-        or hasattr(obj, "__iter__")
+    seq = (not hasattr(obj, "strip") and hasattr(obj, "__getitem__")) or hasattr(
+        obj, "__iter__"
     )
 
     # check to make sure it is not a set, string, or dictionary
diff --git a/trimesh/viewer/__init__.py b/trimesh/viewer/__init__.py
index 78ea0339..e92b649e 100644
--- a/trimesh/viewer/__init__.py
+++ b/trimesh/viewer/__init__.py
@@ -30,10 +30,10 @@ except BaseException as E:
 # explicitly list imports in __all__
 # as otherwise flake8 gets mad
 __all__ = [
-    "SceneWidget",
     "SceneViewer",
-    "render_scene",
+    "SceneWidget",
     "in_notebook",
-    "scene_to_notebook",
+    "render_scene",
     "scene_to_html",
+    "scene_to_notebook",
 ]
diff --git a/trimesh/voxel/creation.py b/trimesh/voxel/creation.py
index c65b4235..7859a3de 100644
--- a/trimesh/voxel/creation.py
+++ b/trimesh/voxel/creation.py
@@ -38,7 +38,7 @@ def voxelize_subdivide(mesh, pitch: float, max_iter: int = 10, edge_factor: floa
 
     # get the same mesh sudivided so every edge is shorter
     # than a factor of our pitch
-    v, f, idx = remesh.subdivide_to_size(
+    v, _f, _idx = remesh.subdivide_to_size(
         mesh.vertices, mesh.faces, max_edge=max_edge, max_iter=max_iter, return_index=True
     )
 
@@ -48,7 +48,7 @@ def voxelize_subdivide(mesh, pitch: float, max_iter: int = 10, edge_factor: floa
     hit = np.round(v / pitch).astype(int)
 
     # remove duplicates
-    unique, inverse = grouping.unique_rows(hit)
+    unique, _inverse = grouping.unique_rows(hit)
 
     # get the voxel centers in model space
     occupied_index = hit[unique]
diff --git a/trimesh/voxel/ops.py b/trimesh/voxel/ops.py
index c96552bf..f29b45ac 100644
--- a/trimesh/voxel/ops.py
+++ b/trimesh/voxel/ops.py
@@ -148,7 +148,7 @@ def matrix_to_marching_cubes(matrix, pitch=1.0):
         vertices, faces = meshed
         normals = None
     elif len(meshed) == 4:
-        vertices, faces, normals, vals = meshed
+        vertices, faces, normals, _vals = meshed
 
     # Return to the origin, add in the pad_width
     vertices = np.subtract(vertices, pad_width * pitch)


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ddc3873e..1defa397 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -16,11 +16,9 @@ jobs:
       with:
         python-version: "3.11"
     - name: Install
-      run: pip install ruff black
+      run: pip install ruff
     - name: Run Ruff
-      run: ruff .
-#    - name: Run Black
-#      run: black --check .
+      run: ruff check .
 
   tests:
     name: Run Unit Tests
diff --git a/tests/test_align.py b/tests/test_align.py
index cc237ea3..2a36075a 100644
--- a/tests/test_align.py
+++ b/tests/test_align.py
@@ -37,7 +37,7 @@ class AlignTests(g.unittest.TestCase):
         unitized = g.trimesh.unitize(vectors)
         for unit_dest, dest in zip(unitized[-10:], vectors[-10:]):
             for unit, vector in zip(unitized, vectors):
-                T, a = align(vector, dest, return_angle=True)
+                T, _a = align(vector, dest, return_angle=True)
                 assert is_rigid(T)
                 assert g.np.isclose(g.np.linalg.det(T), 1.0)
                 # rotate vector with transform
@@ -97,7 +97,7 @@ class AlignTests(g.unittest.TestCase):
 
         vector_1 = g.np.array([7.12106798e-07, -7.43194705e-08, 1.00000000e00])
         vector_2 = g.np.array([0, 0, -1])
-        T, angle = align(vector_1, vector_2, return_angle=True)
+        T, _angle = align(vector_1, vector_2, return_angle=True)
         assert g.np.isclose(g.np.linalg.det(T), 1.0)
 
 
diff --git a/tests/test_creation.py b/tests/test_creation.py
index bc06f9cb..647e51fb 100644
--- a/tests/test_creation.py
+++ b/tests/test_creation.py
@@ -267,7 +267,7 @@ class CreationTest(g.unittest.TestCase):
                     )
                 except BaseException:
                     g.log.error("failed to benchmark triangle", exc_info=True)
-        g.log.info(f"benchmarked triangulation on {len(bench)} polygons: {str(times)}")
+        g.log.info(f"benchmarked triangulation on {len(bench)} polygons: {times!s}")
 
     def test_triangulate_plumbing(self):
         """
diff --git a/tests/test_export.py b/tests/test_export.py
index 1125b5bd..380761d6 100644
--- a/tests/test_export.py
+++ b/tests/test_export.py
@@ -312,7 +312,7 @@ class ExportTest(g.unittest.TestCase):
 
     def test_buffered_random(self):
         """Test writing to non-standard file"""
-        mesh = list(g.get_meshes(1))[0]
+        mesh = next(iter(g.get_meshes(1)))
         with io.BufferedRandom(io.BytesIO()) as rw:
             mesh.export(rw, "STL")
             rw.seek(0)
diff --git a/tests/test_gltf.py b/tests/test_gltf.py
index a44aeb05..95aec463 100644
--- a/tests/test_gltf.py
+++ b/tests/test_gltf.py
@@ -433,7 +433,7 @@ class GLTFTest(g.unittest.TestCase):
         )
         assert len(reloaded.geometry) == 1
         # get meshes back
-        sphere_b = list(reloaded.geometry.values())[0]
+        sphere_b = next(iter(reloaded.geometry.values()))
         assert (sphere_b.visual.material.baseColorFactor == (255, 0, 0, 255)).all()
 
     def test_material_hash(self):
@@ -1047,11 +1047,13 @@ class GLTFTest(g.unittest.TestCase):
 
         # Export the mesh
         export = mesh.export(file_type="glb", unitize_normals=True)
-        reimported_mesh = list(
-            g.trimesh.load(
-                g.trimesh.util.wrap_as_stream(export), file_type="glb"
-            ).geometry.values()
-        )[0]
+        reimported_mesh = next(
+            iter(
+                g.trimesh.load(
+                    g.trimesh.util.wrap_as_stream(export), file_type="glb"
+                ).geometry.values()
+            )
+        )
 
         # Check that the normals are still null
         assert g.np.allclose(reimported_mesh.vertex_normals[0], [0, 0, 0])
diff --git a/tests/test_obj.py b/tests/test_obj.py
index 8000ff32..226781f0 100644
--- a/tests/test_obj.py
+++ b/tests/test_obj.py
@@ -110,7 +110,7 @@ class OBJTest(g.unittest.TestCase):
         m = g.trimesh.load(file_name, process=False)
         # use trivial loading to compare with fancy performant one
         with open(file_name) as f:
-            f, v, vt = simple_load(f.read())
+            f, v, _vt = simple_load(f.read())
         # trimesh loader should return the same face order
         assert g.np.allclose(f, m.faces)
         assert g.np.allclose(v, m.vertices)
@@ -123,7 +123,7 @@ class OBJTest(g.unittest.TestCase):
         m = g.trimesh.load(file_name, process=False, maintain_order=True)
         # use trivial loading to compare with fancy performant one
         with open(file_name) as f:
-            f, v, vt = simple_load(f.read())
+            f, v, _vt = simple_load(f.read())
         # trimesh loader should return the same face order
         assert g.np.allclose(f, m.faces)
         assert g.np.allclose(v, m.vertices)
@@ -448,7 +448,7 @@ class OBJTest(g.unittest.TestCase):
     def test_export_mtl_args(self):
         mesh = g.trimesh.creation.box()
         # check for a crash with no materials defined
-        a, b = g.trimesh.exchange.obj.export_obj(
+        _a, _b = g.trimesh.exchange.obj.export_obj(
             mesh, return_texture=True, mtl_name="hi.mtl"
         )
 
diff --git a/tests/test_packing.py b/tests/test_packing.py
index c5643723..6b500ee0 100644
--- a/tests/test_packing.py
+++ b/tests/test_packing.py
@@ -72,7 +72,7 @@ class PackingTest(g.unittest.TestCase):
         from trimesh.path import packing
 
         nestable = [g.Polygon(i) for i in g.data["nestable"]]
-        inserted, transforms = packing.polygons(nestable)
+        _inserted, _transforms = packing.polygons(nestable)
 
     def test_image(self):
         from trimesh.path import packing
@@ -157,11 +157,11 @@ class PackingTest(g.unittest.TestCase):
         )
 
         # try packing these 3D boxes
-        bounds, consume = packing.rectangles_single(e)
+        _bounds, consume = packing.rectangles_single(e)
         assert consume.all()
 
         # try packing these 3D boxes
-        bounds, consume = packing.rectangles_single(e, size=[14, 14, 1])
+        _bounds, consume = packing.rectangles_single(e, size=[14, 14, 1])
         assert not consume.all()
 
     def test_transform(self):
diff --git a/tests/test_paths.py b/tests/test_paths.py
index 1230e4cc..61c2a44d 100644
--- a/tests/test_paths.py
+++ b/tests/test_paths.py
@@ -282,7 +282,7 @@ class SectionTest(g.unittest.TestCase):
             )
 
             # Path3D -> Path2D
-            planar, T = section.to_planar()
+            planar, _T = section.to_planar()
 
             # tube should have one closed polygon
             assert len(planar.polygons_full) == 1
diff --git a/tests/test_permutate.py b/tests/test_permutate.py
index 47debca7..96298c00 100644
--- a/tests/test_permutate.py
+++ b/tests/test_permutate.py
@@ -22,7 +22,7 @@ class PermutateTest(g.unittest.TestCase):
                 close(test.face_adjacency, mesh.face_adjacency)
                 and len(mesh.faces) > MIN_FACES
             ):
-                g.log.error(f"face_adjacency unchanged: {str(test.face_adjacency)}")
+                g.log.error(f"face_adjacency unchanged: {test.face_adjacency!s}")
                 raise ValueError(
                     "face adjacency of %s the same after permutation!",
                     mesh.metadata["file_name"],
@@ -33,7 +33,7 @@ class PermutateTest(g.unittest.TestCase):
                 and len(mesh.faces) > MIN_FACES
             ):
                 g.log.error(
-                    f"face_adjacency_edges unchanged: {str(test.face_adjacency_edges)}"
+                    f"face_adjacency_edges unchanged: {test.face_adjacency_edges!s}"
                 )
                 raise ValueError(
                     "face adjacency edges of %s the same after permutation!",
diff --git a/tests/test_points.py b/tests/test_points.py
index 4c28953b..1b3724ea 100644
--- a/tests/test_points.py
+++ b/tests/test_points.py
@@ -102,7 +102,7 @@ class PointsTest(g.unittest.TestCase):
             # so the true normal should be Z then rotated
             truth = g.trimesh.transform_points([[0, 0, 1]], matrix, translate=False)[0]
             # run the plane fit
-            C, N = g.trimesh.points.plane_fit(p)
+            _C, N = g.trimesh.points.plane_fit(p)
             # sign of normal is arbitrary on fit so check both
             assert g.np.allclose(truth, N) or g.np.allclose(truth, -N)
         # make sure plane fit works with multiple point sets at once
@@ -130,7 +130,7 @@ class PointsTest(g.unittest.TestCase):
                     [[0, 0, 1]], matrix, translate=False
                 )[0]
             # run the plane fit
-            C, N = g.trimesh.points.plane_fit(p)
+            _C, N = g.trimesh.points.plane_fit(p)
 
             # sign of normal is arbitrary on fit so check both
             cosines = g.np.einsum("ij,ij->i", N, truths)
diff --git a/tests/test_polygons.py b/tests/test_polygons.py
index 9b204567..005923da 100644
--- a/tests/test_polygons.py
+++ b/tests/test_polygons.py
@@ -71,7 +71,7 @@ class PolygonTests(g.unittest.TestCase):
         assert radius < (1.0 + 1e-8)
 
         # try getting OBB of samples
-        T, extents = g.trimesh.path.polygons.polygon_obb(s)
+        _T, extents = g.trimesh.path.polygons.polygon_obb(s)
         # OBB of samples should be less than diameter of circle
         diameter = g.np.reshape(p.bounds, (2, 2)).ptp(axis=0).max()
         assert (extents <= diameter).all()
@@ -260,7 +260,7 @@ class PolygonTests(g.unittest.TestCase):
 
         for p, c in zip(polygons, coords):
             # area will be signed with respect to counter-clockwise
-            ccw, area, centroid = g.trimesh.util.is_ccw(c, return_all=True)
+            _ccw, area, centroid = g.trimesh.util.is_ccw(c, return_all=True)
             assert g.np.allclose(centroid, g.np.array(p.centroid.coords)[0])
             assert g.np.isclose(abs(area), p.area)
 
diff --git a/tests/test_poses.py b/tests/test_poses.py
index 4b183c8c..43dc4add 100644
--- a/tests/test_poses.py
+++ b/tests/test_poses.py
@@ -29,7 +29,7 @@ class PosesTest(g.unittest.TestCase):
                 copied.apply_transform(matrix)
 
                 # Compute the stable poses of the icosahedron
-                trans, probs = copied.compute_stable_poses()
+                _trans, probs = copied.compute_stable_poses()
 
                 # we are only testing primitives with point symmetry
                 # AKA 3 principal components of inertia are the same
@@ -43,8 +43,8 @@ class PosesTest(g.unittest.TestCase):
     def test_round(self):
         mesh = g.trimesh.primitives.Cylinder(radius=1.0, height=10.0)
 
-        transforms, probabilities = mesh.compute_stable_poses()
-        transforms, probabilities = mesh.compute_stable_poses(n_samples=10)
+        _transforms, _probabilities = mesh.compute_stable_poses()
+        _transforms, _probabilities = mesh.compute_stable_poses(n_samples=10)
 
 
 if __name__ == "__main__":
diff --git a/tests/test_proximity.py b/tests/test_proximity.py
index 93bd38db..83d6cdf4 100644
--- a/tests/test_proximity.py
+++ b/tests/test_proximity.py
@@ -20,7 +20,7 @@ class NearestTest(g.unittest.TestCase):
         triangles = sphere.triangles  # NOQA
 
         # do the check
-        closest, distance, tid = g.trimesh.proximity.closest_point_naive(sphere, points)
+        closest, distance, _tid = g.trimesh.proximity.closest_point_naive(sphere, points)
 
         # the distance from a sphere of radius 1.0 to a sphere of radius 2.0
         # should be pretty darn close to 1.0
@@ -98,7 +98,7 @@ class NearestTest(g.unittest.TestCase):
             # create a mesh with one triangle
             mesh = g.Trimesh(**g.trimesh.triangles.to_kwargs([triangle]))
 
-            result, result_distance, result_tid = fun(mesh, query)
+            result, result_distance, _result_tid = fun(mesh, query)
 
             polygon = g.Polygon(triangle[:, 0:2])
             polygon_buffer = polygon.buffer(1e-5)
diff --git a/tests/test_ray.py b/tests/test_ray.py
index 46c4f1d6..37493e95 100644
--- a/tests/test_ray.py
+++ b/tests/test_ray.py
@@ -102,7 +102,7 @@ class RayTests(g.unittest.TestCase):
 
             assert m.ray.intersects_any(ray_origins=origins, ray_directions=vectors).all()
 
-            (locations, index_ray, index_tri) = m.ray.intersects_location(
+            (_locations, index_ray, _index_tri) = m.ray.intersects_location(
                 ray_origins=origins, ray_directions=vectors
             )
 
@@ -146,14 +146,14 @@ class RayTests(g.unittest.TestCase):
 
             # Perform 256 * 256 raycasts, one for each pixel on the image
             # plane. We only want the 'first' hit.
-            index_triangles, index_ray = cube_mesh.ray.intersects_id(
+            index_triangles, _index_ray = cube_mesh.ray.intersects_id(
                 ray_origins=ray_origins,
                 ray_directions=ray_directions,
                 multiple_hits=False,
             )
             assert len(g.np.unique(index_triangles)) == 2
 
-            index_triangles, index_ray = cube_mesh.ray.intersects_id(
+            index_triangles, _index_ray = cube_mesh.ray.intersects_id(
                 ray_origins=ray_origins, ray_directions=ray_directions, multiple_hits=True
             )
             assert len(g.np.unique(index_triangles)) > 2
@@ -192,7 +192,7 @@ class RayTests(g.unittest.TestCase):
             # (n,3) float intersection position in space
             # (n,) int, index of original ray
             # (m,) int, index of mesh.faces
-            pos, ray, tri = mesh.ray.intersects_location(
+            pos, ray, _tri = mesh.ray.intersects_location(
                 ray_origins=origins, ray_directions=vectors
             )
 
@@ -218,7 +218,7 @@ class RayTests(g.unittest.TestCase):
             for kwargs in [{"use_embree": True}, {"use_embree": False}]:
                 mesh = g.get_mesh("broken.STL", **kwargs)
 
-                locations, index_ray, index_tri = mesh.ray.intersects_location(
+                locations, _index_ray, _index_tri = mesh.ray.intersects_location(
                     ray_origins=ray_origins, ray_directions=ray_directions
                 )
 
diff --git a/tests/test_registration.py b/tests/test_registration.py
index a295a9c9..44f422cf 100644
--- a/tests/test_registration.py
+++ b/tests/test_registration.py
@@ -110,7 +110,7 @@ class RegistrationTest(g.unittest.TestCase):
         m = g.trimesh.creation.box()
         X = m.sample(10)
         X = X + [0.1, 0.1, 0.1]
-        matrix, transformed, cost = g.trimesh.registration.icp(X, m, scale=False)
+        _matrix, _transformed, cost = g.trimesh.registration.icp(X, m, scale=False)
         assert cost < 0.01
 
     def test_icp_points(self):
@@ -157,7 +157,7 @@ class RegistrationTest(g.unittest.TestCase):
         truth = g.trimesh.creation.box(extents=extents)
 
         for a, b in [[truth, scan], [scan, truth]]:
-            a_to_b, cost = a.register(b)
+            a_to_b, _cost = a.register(b)
 
             a_check = a.copy()
             a_check.apply_transform(a_to_b)
@@ -172,7 +172,7 @@ class RegistrationTest(g.unittest.TestCase):
         points = g.trimesh.transform_points(
             scan.sample(100), matrix=g.trimesh.transformations.random_rotation_matrix()
         )
-        truth_to_points, cost = truth.register(points)
+        truth_to_points, _cost = truth.register(points)
         truth.apply_transform(truth_to_points)
         distance = truth.nearest.on_surface(points)[1]
 
diff --git a/tests/test_render.py b/tests/test_render.py
index 052fd012..8af7944c 100644
--- a/tests/test_render.py
+++ b/tests/test_render.py
@@ -33,7 +33,7 @@ class RenderTest(g.unittest.TestCase):
                 assert len(args) == 6
                 assert len(args_auto) == len(args)
 
-                P20, T = P30.to_planar()
+                P20, _T = P30.to_planar()
                 args = rendering.path_to_vertexlist(P20)
                 args_auto = rendering.convert_to_vertexlist(P20)
                 assert len(args) == 6
diff --git a/tests/test_sample.py b/tests/test_sample.py
index f9f42f0e..a34e2094 100644
--- a/tests/test_sample.py
+++ b/tests/test_sample.py
@@ -14,7 +14,7 @@ class SampleTest(g.unittest.TestCase):
         distance = m.nearest.signed_distance(samples)
         assert g.np.abs(distance).max() < 1e-4
 
-        even, index = g.trimesh.sample.sample_surface_even(m, 1000)
+        even, _index = g.trimesh.sample.sample_surface_even(m, 1000)
         # check to make sure all samples are on the mesh surface
         distance = m.nearest.signed_distance(even)
         assert g.np.abs(distance).max() < 1e-4
@@ -27,7 +27,7 @@ class SampleTest(g.unittest.TestCase):
         weights[0] = 1.0
 
         # sample with passed weights
-        points, fid = m.sample(count=100, return_index=True, face_weight=weights)
+        _points, fid = m.sample(count=100, return_index=True, face_weight=weights)
         # all faces should be on single face
         assert (fid == 0).all()
 
@@ -42,13 +42,13 @@ class SampleTest(g.unittest.TestCase):
 
         # sample a textured mesh
         m = g.get_mesh("fuze.obj")
-        points, index, color = g.trimesh.sample.sample_surface(m, 100, sample_color=True)
+        points, _index, color = g.trimesh.sample.sample_surface(m, 100, sample_color=True)
         assert len(points) == len(color)
 
         # sample a color mesh
         m = g.get_mesh("machinist.XAML")
         assert m.visual.kind == "face"
-        points, index, color = g.trimesh.sample.sample_surface(m, 100, sample_color=True)
+        points, _index, color = g.trimesh.sample.sample_surface(m, 100, sample_color=True)
         assert len(points) == len(color)
 
     def test_sample_volume(self):
diff --git a/tests/test_section.py b/tests/test_section.py
index 2b8e9171..20c6cb04 100644
--- a/tests/test_section.py
+++ b/tests/test_section.py
@@ -57,7 +57,7 @@ class SectionTest(g.unittest.TestCase):
                 assert g.np.allclose(section.vertices[:, 2], z)
                 assert len(section.centroid) == 3
 
-                planar, to_3D = section.to_planar()
+                planar, _to_3D = section.to_planar()
                 assert planar.is_closed
                 assert len(planar.polygons_full) > 0
                 assert len(planar.centroid) == 2
@@ -72,7 +72,7 @@ class SectionTest(g.unittest.TestCase):
             )
 
             # call the multiplane method directly
-            lines, faces, T = g.trimesh.intersections.mesh_multiplane(
+            lines, _faces, _T = g.trimesh.intersections.mesh_multiplane(
                 mesh=mesh,
                 plane_origin=[0, 0, 0],
                 plane_normal=plane_normal,
diff --git a/tests/test_segments.py b/tests/test_segments.py
index 7a023155..86f44e59 100644
--- a/tests/test_segments.py
+++ b/tests/test_segments.py
@@ -107,7 +107,7 @@ class SegmentsTest(g.unittest.TestCase):
         assert g.np.isclose(length(res), length(seg))
 
         # now try with indexes returned
-        res, index = resample(seg, maxlen=maxlen, return_index=True)
+        res, _index = resample(seg, maxlen=maxlen, return_index=True)
         # check lengths of the resampled result
         assert (length(res, summed=False) < maxlen).all()
         # make sure overall length hasn't changed
diff --git a/tests/test_simplify.py b/tests/test_simplify.py
index f9eacd69..45e4529e 100644
--- a/tests/test_simplify.py
+++ b/tests/test_simplify.py
@@ -64,7 +64,7 @@ class SimplifyTest(g.unittest.TestCase):
         m = scene.geometry["disc_cam_A"]
 
         path_3D = m.outline(m.facets[m.facets_area.argmax()])
-        path_2D, to_3D = path_3D.to_planar()
+        path_2D, _to_3D = path_3D.to_planar()
 
         simple = g.trimesh.path.simplify.simplify_spline(
             path_2D, smooth=0.01, verbose=True
diff --git a/tests/test_smooth.py b/tests/test_smooth.py
index 2f81e138..66e786b5 100644
--- a/tests/test_smooth.py
+++ b/tests/test_smooth.py
@@ -13,7 +13,7 @@ class SmoothTest(g.unittest.TestCase):
         check = g.np.hstack((s.visual.uv, s.vertices))
 
         tree = g.spatial.cKDTree(ori)
-        distances, index = tree.query(check, k=1)
+        distances, _index = tree.query(check, k=1)
         assert distances.max() < 1e-8
 
         # g.texture_equal(m, s)
diff --git a/tests/test_stl.py b/tests/test_stl.py
index 5b9aa7a2..151e0c80 100644
--- a/tests/test_stl.py
+++ b/tests/test_stl.py
@@ -72,6 +72,14 @@ class STLTests(g.unittest.TestCase):
         assert len(s.geometry) == 2
         assert set(s.geometry.keys()) == {"bodyA", "bodyB"}
 
+    def test_ascii_solid_name(self):
+        mesh = g.trimesh.creation.icosphere(subdivisions=1, radius=1.0)
+        mesh.metadata = {"name": "solid_A"}
+        assert (
+            g.trimesh.exchange.stl.export_stl_ascii(mesh).splitlines()[0]
+            == "solid solid_A"
+        )
+
     def test_empty(self):
         # demo files to check
         empty_files = ["stl_empty_ascii.stl", "stl_empty_bin.stl"]
diff --git a/tests/test_visual.py b/tests/test_visual.py
index af8da5b7..01816a73 100644
--- a/tests/test_visual.py
+++ b/tests/test_visual.py
@@ -15,7 +15,7 @@ class VisualTest(g.unittest.TestCase):
         check = m.visual.face_subset(face_index).uv
 
         tree = g.spatial.cKDTree(ori)
-        distances, index = tree.query(check, k=1)
+        distances, _index = tree.query(check, k=1)
         assert distances.max() < 1e-8
 
     def test_face_subset_color_visuals(self):
@@ -33,7 +33,7 @@ class VisualTest(g.unittest.TestCase):
         check = m.visual.face_subset(face_index).vertex_colors
 
         tree = g.spatial.cKDTree(ori)
-        distances, index = tree.query(check, k=1)
+        distances, _index = tree.query(check, k=1)
         assert distances.max() < 1e-8
 
     # def test_face_subset_vertex_color(self):