Repository: mikedh/trimesh
Pull request #2075 (2023-11-18 22:03:48)

Release: load_remote to httpx
- switch `trimesh.exchange.load_remote` to `httpx` from `requests` mainly because they had a long-standing version warning with `chardet` that was impossible to get rid of. Also httpx seems seems like a good choice for new work (Python 3.8+) as they have 100% test coverage, and a number of nice features.
- adds `trimesh.units.keys()` to produce a list of unit systems available. 


Patch:
diff --git a/pyproject.toml b/pyproject.toml
index d5e7d3fd..47e1a8b6 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.0.4"
+version = "4.0.5"
 authors = [{name = "Michael Dawson-Haggerty", email = "mikedh@kerfed.com"}]
 license = {file = "LICENSE.md"}
 description = "Import, export, process, analyze and view triangular meshes."
@@ -73,7 +73,7 @@ easy = [
     "shapely",
     "xxhash",
     "rtree",
-    "requests",
+    "httpx",
     "scipy",
     "embreex",
     "pillow",
diff --git a/trimesh/exchange/load.py b/trimesh/exchange/load.py
index 80ca60bb..8c9e2238 100644
--- a/trimesh/exchange/load.py
+++ b/trimesh/exchange/load.py
@@ -344,10 +344,12 @@ def load_remote(url, **kwargs):
       Loaded result
     """
     # import here to keep requirement soft
-    import requests
+    import httpx
 
     # download the mesh
-    response = requests.get(url)
+    response = httpx.get(url, follow_redirects=True)
+    response.raise_for_status()
+
     # wrap as file object
     file_obj = util.wrap_as_stream(response.content)
 
diff --git a/trimesh/resolvers.py b/trimesh/resolvers.py
index e2395f3d..7edab5e3 100644
--- a/trimesh/resolvers.py
+++ b/trimesh/resolvers.py
@@ -354,7 +354,7 @@ class WebResolver(Resolver):
           Asset name, i.e. 'quadknot.obj.mtl'
         """
         # do import here to keep soft dependency
-        import requests
+        import httpx
 
         # remove leading and trailing whitespace
         name = name.strip()
@@ -363,16 +363,16 @@ class WebResolver(Resolver):
         # base url has been carefully formatted
         url = self.base_url + name
 
-        response = requests.get(url)
+        response = httpx.get(url, follow_redirects=True)
 
-        if response.status_code != 200:
+        if response.status_code >= 300:
             # try to strip off filesystem crap
             if name.startswith("./"):
                 name = name[2:]
-            response = requests.get(self.base_url + name)
+            response = httpx.get(self.base_url + name, follow_redirects=True)
 
-        if response.status_code == "404":
-            raise ValueError(response.content)
+        # now raise if we don't have
+        response.raise_for_status()
 
         # return the bytes of the response
         return response.content
diff --git a/trimesh/units.py b/trimesh/units.py
index edafd206..1cf9ab37 100644
--- a/trimesh/units.py
+++ b/trimesh/units.py
@@ -34,6 +34,18 @@ def unit_conversion(current: str, desired: str) -> float:
     return to_inch(current.strip().lower()) / to_inch(desired.strip().lower())
 
 
+def keys() -> set:
+    """
+    Return a set containing all currently valid units.
+
+    Returns
+    --------
+    keys
+      All units with conversions i.e. {'in', 'm', ...}
+    """
+    return set(_lookup.keys())
+
+
 def to_inch(unit: str) -> float:
     """
     Calculate the conversion to an arbitrary common unit.
diff --git a/trimesh/visual/gloss.py b/trimesh/visual/gloss.py
index 8aa42bc7..8756c7f3 100644
--- a/trimesh/visual/gloss.py
+++ b/trimesh/visual/gloss.py
@@ -180,9 +180,7 @@ def specular_to_pbr(
                 or specularGlossinessTexture.shape[-1]
             ) == 1:
                 # use the one channel as a multiplier for specular and glossiness
-                specularTexture = glossinessTexture = specularGlossinessTexture.reshape(
-                    (-1, -1, 1)
-                )
+                specularTexture = glossinessTexture = specularGlossinessTexture[..., np.newaxis]
             elif specularGlossinessTexture.shape[-1] == 3:
                 # all channels are specular, glossiness is only a factor
                 specularTexture = specularGlossinessTexture[..., :3]


Test patch:
diff --git a/tests/test_dae.py b/tests/test_dae.py
index 0fb2638b..36b7fe11 100644
--- a/tests/test_dae.py
+++ b/tests/test_dae.py
@@ -29,7 +29,7 @@ class DAETest(g.unittest.TestCase):
         assert len(scene.graph.nodes_geometry) == 1
 
         conv = scene.convert_units("inch")
-        assert conv.units == 'inch'
+        assert conv.units == "inch"
 
     def test_shoulder(self):
         if collada is None:
@@ -40,9 +40,9 @@ class DAETest(g.unittest.TestCase):
         assert len(scene.geometry) == 3
         assert len(scene.graph.nodes_geometry) == 3
 
-        assert scene.units != 'mm'
+        assert scene.units != "mm"
         conv = scene.convert_units("mm")
-        assert conv.units == 'mm'
+        assert conv.units == "mm"
 
     def test_export(self):
         if collada is None:
@@ -67,8 +67,7 @@ class DAETest(g.unittest.TestCase):
         assert s.visual.material.baseColorTexture.size == rec.visual.material.image.size
 
         conv = s.convert_units("inch")
-        assert conv.units == 'inch'
-
+        assert conv.units == "inch"
 
     def test_material_round(self):
         """
diff --git a/tests/test_units.py b/tests/test_units.py
index bd734fe7..90481dd9 100644
--- a/tests/test_units.py
+++ b/tests/test_units.py
@@ -59,6 +59,11 @@ class UnitsTest(g.unittest.TestCase):
         # extents should scale exactly with unit conversion
         assert g.np.allclose(p.extents / extents_pre, 25.4, atol=0.01)
 
+    def test_keys(self):
+        units = g.trimesh.units.keys()
+        assert isinstance(units, set)
+        assert "in" in units
+
     def test_arbitrary(self):
         ac = g.np.allclose
         to_inch = g.trimesh.units.to_inch