Repository: hgrecco/pint

Issue:
Regression: Units from derived unit registry no longer instance of pint.Unit
Hi,

when using Pint 0.20, a unit from a subclass of pint.UnitRegistry is no longer considered an instance of pint.Unit. Hard to explain, this code shows it:

```python
import pint

class MyUnitRegistry(pint.UnitRegistry):
    pass

print(isinstance(MyUnitRegistry().Unit("m"), pint.Unit))
```
With Pint 0.19.2, this prints `True`, with Pint 0.20, it prints `False`.

This leads to weird behaviour in e.g. pint_xarray when trying to check if something is a unit (done with `isinstance(pint.Unit)`) when the unit is derived from a subclass derived from pint.UnitRegistry. In my specific use case, I'm combining [pint_xarray](https://pint-xarray.readthedocs.io/en/stable/) and [openscm-units](https://openscm-units.readthedocs.io/en/latest/), and the combination breaks if Pint 0.20 is used.

I'm not quite sure if this can be solved / worked around downstream. Wanting to check if something is a unit via `isinstance` sounds reasonable, and subclassing also doesn't sound super wrong.

Discussion:

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e70fd13..8e44023 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -92,18 +92,8 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        python-version: [3.8, 3.9, "3.10"]
+        python-version: [3.8, 3.9, "3.10", "3.11"]
         numpy: [ "numpy>=1.19,<2.0.0" ]
-        # uncertainties: [null, "uncertainties==3.1.6", "uncertainties>=3.1.6,<4.0.0"]
-        # extras: [null]
-        # include:
-        #   - python-version: 3.8 # Minimal versions
-        #     numpy: numpy==1.19.5
-        #     extras: matplotlib==2.2.5
-        #   - python-version: 3.8
-        #     numpy: "numpy"
-        #     uncertainties: "uncertainties"
-        #     extras: "sparse xarray netCDF4 dask[complete] graphviz babel==2.8"
     runs-on: windows-latest
 
     env:
@@ -159,6 +149,68 @@ jobs:
       - name: Run tests
         run: pytest ${env:TEST_OPTS}
 
+  test-macos:
+    strategy:
+      fail-fast: false
+      matrix:
+        python-version: [3.8, 3.9, "3.10", "3.11"]
+        numpy: [ "numpy>=1.19,<2.0.0" ]
+    runs-on: macos-latest
+
+    env:
+      TEST_OPTS: "-rfsxEX -s --cov=pint --cov-config=.coveragerc"
+
+    steps:
+      - uses: actions/checkout@v2
+        with:
+          fetch-depth: 100
+
+      - name: Get tags
+        run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
+
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      - name: Get pip cache dir
+        id: pip-cache
+        run: echo "::set-output name=dir::$(pip cache dir)"
+
+      - name: Setup caching
+        uses: actions/cache@v2
+        with:
+          path: ${{ steps.pip-cache.outputs.dir }}
+          key: pip-${{ matrix.python-version }}
+          restore-keys: |
+            pip-${{ matrix.python-version }}
+
+      - name: Install numpy
+        if: ${{ matrix.numpy != null }}
+        run: pip install "${{matrix.numpy}}"
+
+      - name: Install dependencies
+        run: |
+          pip install pytest pytest-cov pytest-subtests
+          pip install .
+
+      - name: Run Tests
+        run: |
+          pytest $TEST_OPTS
+
+      - name: Coverage report
+        run: coverage report -m
+
+      - name: Coveralls Parallel
+        env:
+          COVERALLS_FLAG_NAME: ${{ matrix.test-number }}
+          COVERALLS_PARALLEL: true
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+          COVERALLS_SERVICE_NAME: github
+        run: |
+          pip install coveralls
+          coveralls
+
   coveralls:
     needs: test-linux
     runs-on: ubuntu-latest
diff --git a/CHANGES b/CHANGES
index 03932a4..04bfc8e 100644
--- a/CHANGES
+++ b/CHANGES
@@ -4,7 +4,7 @@ Pint Changelog
 0.21 (unreleased)
 -----------------
 
-- Nothing changed yet.
+- Simplify registry subclassing and document it.
 
 
 0.20 (2022-10-25)
diff --git a/docs/advanced_guides.rst b/docs/advanced_guides.rst
index cc2c495..580ddb7 100644
--- a/docs/advanced_guides.rst
+++ b/docs/advanced_guides.rst
@@ -12,3 +12,4 @@ Advanced Guides
     user/measurement
     user/pitheorem
     user/currencies
+    user/custom-registry-class
diff --git a/docs/user/custom-registry-class.rst b/docs/user/custom-registry-class.rst
new file mode 100644
index 0000000..31f3d76
--- /dev/null
+++ b/docs/user/custom-registry-class.rst
@@ -0,0 +1,83 @@
+.. _custom_registry_class:
+
+Custom registry class
+=====================
+
+Pay as you go
+-------------
+
+Pint registry functionality is divided into facets. The default
+UnitRegistry inherits from all of them, providing a full fledged
+and feature rich registry. However, in certain cases you might want
+to have a simpler and light registry. Just pick what you need
+and create your own.
+
+- FormattingRegistry: adds the capability to format quantities and units into string.
+- SystemRegistry: adds the capability to work with system of units.
+- GroupRegistry: adds the capability to group units.
+- MeasurementRegistry: adds the capability to handle measurements (quantities with uncertainties).
+- NumpyRegistry: adds the capability to interoperate with NumPy.
+- DaskRegistry: adds the capability to interoperate with Dask.
+- ContextRegistry: the capability to contexts: predefined conversions
+  between incompatible dimensions.
+- NonMultiplicativeRegistry: adds the capability to handle nonmultiplicative units (offset, logarithmic).
+- PlainRegistry: base implementation for registry, units and quantities.
+
+The only required one is `PlainRegistry`, the rest are completely
+optional.
+
+For example:
+
+.. doctest::
+
+    >>> import pint
+    >>> class MyRegistry(pint.facets.NonMultiplicativeRegistry, pint.facets.PlainRegistry):
+    ...     pass
+
+
+Subclassing
+-----------
+
+If you want to add the default registry class some specific functionality,
+you can subclass it:
+
+.. doctest::
+
+    >>> import pint
+    >>> class MyRegistry(pint.UnitRegistry):
+    ...
+    ...     def my_specific_function(self):
+    ...         """Do something
+    ...         """
+
+
+If you want to create your own Quantity class, you must tell
+your registry about it:
+
+.. doctest::
+
+    >>> import pint
+    >>> class MyQuantity:
+    ...
+    ...     # Notice that subclassing pint.Quantity
+    ...     # is not necessary.
+    ...     # Pint will inspect the Registry class and create
+    ...     # a Quantity class that contains all the
+    ...     # required parents.
+    ...
+    ...     def to_my_desired_format(self):
+    ...         """Do something else
+    ...         """
+    >>>
+    >>> class MyRegistry(pint.UnitRegistry):
+    ...
+    ...     _quantity_class = MyQuantity
+    ...
+    ...     # The same you can be done with
+    ...     # _unit_class
+    ...     # _measurement_class
+
+
+While these examples demonstrate how to add functionality to the default
+registry class, you can actually subclass just the PlainRegistry or any
+combination of facets.
diff --git a/pint/util.py b/pint/util.py
index eba747e..3d00175 100644
--- a/pint/util.py
+++ b/pint/util.py
@@ -10,6 +10,7 @@
 
 from __future__ import annotations
 
+import functools
 import inspect
 import logging
 import math
@@ -1021,6 +1022,13 @@ def sized(y) -> bool:
     return True
 
 
+@functools.lru_cache(
+    maxsize=None
+)  # TODO: replace with cache when Python 3.8 is dropped.
+def _build_type(class_name: str, bases):
+    return type(class_name, bases, dict())
+
+
 def build_dependent_class(registry_class, class_name: str, attribute_name: str) -> Type:
     """Creates a class specifically for the given registry that
     subclass all the classes named by the registry bases in a
@@ -1036,9 +1044,10 @@ def build_dependent_class(registry_class, class_name: str, attribute_name: str)
         for base in inspect.getmro(registry_class)
         if attribute_name in base.__dict__
     )
-    bases = dict.fromkeys(bases, None)
-    newcls = type(class_name, tuple(bases.keys()), dict())
-    return newcls
+    bases = tuple(dict.fromkeys(bases, None).keys())
+    if len(bases) == 1 and bases[0].__name__ == class_name:
+        return bases[0]
+    return _build_type(class_name, bases)
 
 
 def create_class_with_registry(registry, base_class) -> Type:
diff --git a/setup.cfg b/setup.cfg
index cfa031f..9ce8b48 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -75,3 +75,4 @@ line_length=88
 
 [zest.releaser]
 python-file-with-version = version.py
+create-wheel = yes


Test patch:
diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py
index 80e2ebb..a07e850 100644
--- a/pint/testsuite/test_issues.py
+++ b/pint/testsuite/test_issues.py
@@ -1014,3 +1014,24 @@ def test_backcompat_speed_velocity(func_registry):
     get = func_registry.get_dimensionality
     assert get("[velocity]") == UnitsContainer({"[length]": 1, "[time]": -1})
     assert get("[speed]") == UnitsContainer({"[length]": 1, "[time]": -1})
+
+
+def test_issue1631():
+    import pint
+
+    # Test registry subclassing
+    class MyRegistry(pint.UnitRegistry):
+        pass
+
+    assert MyRegistry.Quantity is pint.UnitRegistry.Quantity
+    assert MyRegistry.Unit is pint.UnitRegistry.Unit
+
+    ureg = MyRegistry()
+
+    u = ureg.meter
+    assert isinstance(u, ureg.Unit)
+    assert isinstance(u, pint.Unit)
+
+    q = 2 * ureg.meter
+    assert isinstance(q, ureg.Quantity)
+    assert isinstance(q, pint.Quantity)