Repository: hgrecco/pint

Issue:
Import from a tuple with invalid units raises no exception
# Versions
* pint [from current master](https://github.com/hgrecco/pint/commit/40034593b803ce4fb48d755d3ac55f14fa8cd1dd)
* python 3.12.2 (can reproduce with 3.11 and 3.10 too)

# Example to reproduce:

```python
In [1]: import pint

In [2]: ureg = pint.UnitRegistry()

In [4]: q1 = ureg.Quantity(1, 'm')

In [5]: q1.to_tuple()
Out[5]: (1, (('meter', 1),))

In [6]: q1 = ureg.Quantity.from_tuple((1, (('meter', 1),)))

In [7]: q1.dimensionality
Out[7]: <UnitsContainer({'[length]': 1})>

In [8]: q2 = ureg.Quantity.from_tuple((1, (('wrong', 1),)))

In [9]: q2.dimensionality
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
File /lib/python3.12/site-packages/pint/facets/numpy/quantity.py:248, in NumpyQuantity.__getattr__(self, item)
    247 try:
--> 248     return getattr(self._magnitude, item)
    249 except AttributeError:

AttributeError: 'int' object has no attribute 'dimensionality'

During handling of the above exception, another exception occurred:

AttributeError                            Traceback (most recent call last)
Cell In[9], line 1
----> 1 q2.dimensionality

File /lib/python3.12/site-packages/pint/facets/numpy/quantity.py:250, in NumpyQuantity.__getattr__(self, item)
    248     return getattr(self._magnitude, item)
    249 except AttributeError:
--> 250     raise AttributeError(
    251         "Neither Quantity object nor its magnitude ({}) "
    252         "has attribute '{}'".format(self._magnitude, item)
    253     )

AttributeError: Neither Quantity object nor its magnitude (1) has attribute 'dimensionality'
```

# Observed behavior

The line  `q2 = ureg.Quantity.from_tuple((1, (('wrong', 1),)))` does not raise any exception and creates a Quantity object. The object is not usable, e.g. `q2.dimensionality` raises an exception. In addition, any operation on the object raises an exception.

# Expected behavior

With the line `q2 = ureg.Quantity.from_tuple((1, (('wrong', 1),)))` pint should raise this exception:

```python
UndefinedUnitError: 'wrong' is not defined in the unit registry
```

as with the line

```python
q2 = ureg.Quantity(1, 'wrong')
```

# Workaround

The user can add this line (or similar) before loading from the tuple:

```python
ureg.get_name('wrong')
```

to get the exception.

# Solution

not known



Discussion:
I have not seen `from_tuple` used before. It looks like it is intended to be used in conjunction with `to_tuple`. It doesn't have any error checking, perhaps it should. The `get_name('wrong')` check could be included within `from_tuple`, ie loop over each `unit` in the `unit, power` tuples.

Out of interest , what makes you use `from_tuple` rather than some other method?
@andrewgsavage Thank you very much for your prompt reply!

> I have not seen `from_tuple` used before. It looks like it is intended to be used in conjunction with `to_tuple`. It doesn't have any error checking, perhaps it should. The `get_name('wrong')` check could be included within `from_tuple`, ie loop over each `unit` in the `unit, power` tuples.

I understand. I will try to do this and open a pull request. I have built such a loop to perform this low-level check before loading from the tuple and it works.

> Out of interest , what makes you use `from_tuple` rather than some other method?

We use pint in [this project](https://gitlab.kit.edu/kit/virtmat-tools/vre-language) as a backend (among other packages) of our language interpreter for all numeric types. Because we use JSON for serialization and have read [this user guide](https://pint.readthedocs.io/en/stable/advanced/serialization.html), we have chosen the pair `to_tuple()`/`from_tuple()` for the purpose. Another way would be to serialize to string but later it usually takes longer to parse. Serialization with pickle/dill has potential security and portability issues especially when the data come from external sources.

Of course, pint will maybe never create the tuple with wrong units using the same registry. But we prefer to check everything from external source (file, database, url, etc.) because we cannot always know who/what/how has created the tuple.



Patch:
diff --git a/CHANGES b/CHANGES
index ab1f4bbb6..ada310912 100644
--- a/CHANGES
+++ b/CHANGES
@@ -4,6 +4,7 @@ Pint Changelog
 0.26.0 (unreleased)
 -------------------
 
+- Fix raising exception in `Quantity.from_tuple` with invalid units (#2199)
 - Add devcontainer.json to add GitHub Codespace support (#2208)
 
 
diff --git a/pint/facets/plain/quantity.py b/pint/facets/plain/quantity.py
index 0e79dc5ec..fa2c3c4f1 100644
--- a/pint/facets/plain/quantity.py
+++ b/pint/facets/plain/quantity.py
@@ -420,6 +420,8 @@ def from_sequence(
 
     @classmethod
     def from_tuple(cls, tup):
+        for units_tup in tup[1]:
+            cls._REGISTRY.get_name(units_tup[0])
         return cls(tup[0], cls._REGISTRY.UnitsContainer(tup[1]))
 
     def to_tuple(self) -> tuple[MagnitudeT, tuple[tuple[str, ...]]]:


Test patch:
diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py
index c6bed595e..48cad1acc 100644
--- a/pint/testsuite/test_issues.py
+++ b/pint/testsuite/test_issues.py
@@ -10,6 +10,7 @@
 from pint import (
     Context,
     DimensionalityError,
+    UndefinedUnitError,
     UnitRegistry,
     get_application_registry,
 )
@@ -1368,3 +1369,9 @@ def mass_to_volume(u, value, *, density=None):
 
     mass.ito("m**3", density=958.05 * ureg.kilogram / ureg.meter**3)
     assert mass.m == pytest.approx(1.0437868587234487)  # 100 C
+
+
+def test_issue2199(func_registry):
+    msg = "is not defined in the unit registry"
+    with pytest.raises(UndefinedUnitError, match=msg):
+        func_registry.Quantity.from_tuple((1, (("wrong", 1),)))