pint icon indicating copy to clipboard operation
pint copied to clipboard

Python 3.13 error: cannot inherit frozen dataclass from a non-frozen one

Open simonw opened this issue 1 year ago • 15 comments

Running Pint against the current Python 3.13 developer preview throws this error:

pint/__init__.py:18: in <module>
    from .delegates.formatter._format_helpers import formatter
pint/delegates/__init__.py:12: in <module>
    from . import txt_defparser
pint/delegates/txt_defparser/__init__.py:12: in <module>
    from .defparser import DefParser
pint/delegates/txt_defparser/defparser.py:10: in <module>
    from . import block, common, context, defaults, group, plain, system
pint/delegates/txt_defparser/common.py:23: in <module>
    @dataclass(frozen=True)
../../../.pyenv/versions/3.13-dev/lib/python3.13/dataclasses.py:1247: in wrap
    return _process_class(cls, init, repr, eq, order, unsafe_hash,
../../../.pyenv/versions/3.13-dev/lib/python3.13/dataclasses.py:1015: in _process_class
    raise TypeError('cannot inherit frozen dataclass from a '
E   TypeError: cannot inherit frozen dataclass from a non-frozen one

simonw avatar Apr 14 '24 21:04 simonw

Steps to reproduce:

cd /tmp
git clone https://github.com/hgrecco/pint
cd pint
python3.13 -m venv venv
# Or for me that was:
# ~/.pyenv/versions/3.13-dev/bin/python -m venv venv
source venv/bin/activate
python -m pip install pytest
python -m pip install -e .
pytest

simonw avatar Apr 14 '24 21:04 simonw

It's possible this issue is related:

  • https://github.com/python/cpython/pull/109437

See cPython commit: https://github.com/python/cpython/commit/b6000d287407cbccfbb1157dc1fc6128497badc7

simonw avatar Apr 14 '24 22:04 simonw

I think this is the code that the trackback complains about: https://github.com/hgrecco/pint/blob/f2e4081aee38f850938048beac7fb69c4908bc5e/pint/delegates/txt_defparser/common.py#L15-L25

And yes, DefinitionSyntaxError() there is marked as frozen=True - but the errors.DefinitionSyntaxError class it extends is marked frozen=False here:

https://github.com/hgrecco/pint/blob/f2e4081aee38f850938048beac7fb69c4908bc5e/pint/errors.py#L105-L115

simonw avatar Apr 14 '24 22:04 simonw

Modifying pint/errors.py and replacing every instance of @dataclass(frozen=False) with @dataclass(frozen=True) addresses this problem, but I don't know if it's the right solution.

diff --git a/pint/errors.py b/pint/errors.py
index 59d3b45..f080f52 100644
--- a/pint/errors.py
+++ b/pint/errors.py
@@ -81,12 +81,12 @@ class WithDefErr:
         return DefinitionError(self.name, self.__class__, msg)
 
 
-@dataclass(frozen=False)
+@dataclass(frozen=True)
 class PintError(Exception):
     """Base exception for all Pint errors."""
 
 
-@dataclass(frozen=False)
+@dataclass(frozen=True)
 class DefinitionError(ValueError, PintError):
     """Raised when a definition is not properly constructed."""
 
@@ -102,7 +102,7 @@ class DefinitionError(ValueError, PintError):
         return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
 
 
-@dataclass(frozen=False)
+@dataclass(frozen=True)
 class DefinitionSyntaxError(ValueError, PintError):
     """Raised when a textual definition has a syntax error."""
 
@@ -115,7 +115,7 @@ class DefinitionSyntaxError(ValueError, PintError):
         return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
 
 
-@dataclass(frozen=False)
+@dataclass(frozen=True)
 class RedefinitionError(ValueError, PintError):
     """Raised when a unit or prefix is redefined."""
 
@@ -130,7 +130,7 @@ class RedefinitionError(ValueError, PintError):
         return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
 
 
-@dataclass(frozen=False)
+@dataclass(frozen=True)
 class UndefinedUnitError(AttributeError, PintError):
     """Raised when the units are not defined in the unit registry."""
 
@@ -150,13 +150,13 @@ class UndefinedUnitError(AttributeError, PintError):
         return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
 
 
-@dataclass(frozen=False)
+@dataclass(frozen=True)
 class PintTypeError(TypeError, PintError):
     def __reduce__(self):
         return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
 
 
-@dataclass(frozen=False)
+@dataclass(frozen=True)
 class DimensionalityError(PintTypeError):
     """Raised when trying to convert between incompatible units."""
 
@@ -183,7 +183,7 @@ class DimensionalityError(PintTypeError):
         return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
 
 
-@dataclass(frozen=False)
+@dataclass(frozen=True)
 class OffsetUnitCalculusError(PintTypeError):
     """Raised on ambiguous operations with offset units."""
 
@@ -208,7 +208,7 @@ class OffsetUnitCalculusError(PintTypeError):
         return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
 
 
-@dataclass(frozen=False)
+@dataclass(frozen=True)
 class LogarithmicUnitCalculusError(PintTypeError):
     """Raised on inappropriate operations with logarithmic units."""
 
@@ -233,7 +233,7 @@ class LogarithmicUnitCalculusError(PintTypeError):
         return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
 
 
-@dataclass(frozen=False)
+@dataclass(frozen=True)
 class UnitStrippedWarning(UserWarning, PintError):
     msg: str
 
@@ -241,13 +241,13 @@ class UnitStrippedWarning(UserWarning, PintError):
         return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
 
 
-@dataclass(frozen=False)
+@dataclass(frozen=True)
 class UnexpectedScaleInContainer(Exception):
     def __reduce__(self):
         return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
 
 
-@dataclass(frozen=False)
+@dataclass(frozen=True)
 class UndefinedBehavior(UserWarning, PintError):
     msg: str

simonw avatar Apr 14 '24 22:04 simonw

I've also run into this today. I applied @simonw's suggested change and the tests all passed. Is that the appropriate fix?

bryanwweber avatar Jun 20 '24 19:06 bryanwweber

Can we please get this fixed so that I can start testing with Python 3.13 before it is released?

bje- avatar Jun 21 '24 07:06 bje-

In trying to ship a PR for this I found the following problem:

Document: getting/tutorial
--------------------------
**********************************************************************
File "getting/tutorial.rst", line 78, in default
Failed example:
    speed = 23 * ureg.snail_speed
Expected:
    Traceback (most recent call last):
    ...
    UndefinedUnitError: 'snail_speed' is not defined in the unit registry
Got:
    Traceback (most recent call last):
      File "/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/doctest.py", line 1350, in __run
        exec(compile(example.source, filename, "single",
      File "<doctest default[0]>", line 1, in <module>
        speed = 23 * ureg.snail_speed
      File "<string>", line 4, in __setattr__
    dataclasses.FrozenInstanceError: cannot assign to field 'name'
**********************************************************************

I'm surprised that only showed up in doctest and not in the rest of the unit test suite, but it helps me understand that there's a reason for the frozen=False argument used here.

simonw avatar Jul 13 '24 00:07 simonw

I've stopped working on this - I hope someone else can figure it out.

simonw avatar Jul 15 '24 16:07 simonw

@simonw Your change is going to be problematic because it will revert 08b9807e5e197b7b94bcad00585f0aaa3b5fe19e by @hgrecco. Hernan, can you elaborate on why you made this change in the first place?

bje- avatar Sep 12 '24 09:09 bje-

@simonw Your change is going to be problematic because it will revert 08b9807 by @hgrecco. Hernan, can you elaborate on why you made this change in the first place?

Ah, I've just read #1766 so now understand.

bje- avatar Sep 12 '24 10:09 bje-

Python 3.13 was released today - https://docs.python.org/3.13/whatsnew/3.13.html - any chance of a compatible Pint release?

simonw avatar Oct 07 '24 16:10 simonw

The root cause here appears to be this bug fix in Python:

  • https://github.com/python/cpython/issues/109409

The dataclass inheritance hierarchy is supposed to require all classes to be either frozen or non frozen, this works properly for checking that an unfrozen class does not inherit from any frozen classes, but it allows frozen classes to inherit from unfrozen ones as long as there's at least one frozen class in the MI

Fixing that bug exposed that Pint was relying on the buggy behaviour.

simonw avatar Oct 07 '24 19:10 simonw

When is this fix planned to be pushed to pypi? I'm unable to test my software stack in 3.13 without this library.

Erotemic avatar Oct 18 '24 22:10 Erotemic

When is this fix planned to be pushed to pypi? I'm unable to test my software stack in 3.13 without this library.

Likewise. Maybe a fork is needed in the meantime?

bje- avatar Oct 18 '24 22:10 bje-

Sorry for the long delay. This will be fixed by next week and a minor release will be produced

hgrecco avatar Oct 18 '24 22:10 hgrecco

Hey. Any news or schedule regarding the fix ? Thanks !

ahabersaat avatar Nov 06 '24 10:11 ahabersaat

I merged a PR in Flexparser to undataclass exceptions. I am testing if nothing broke downstream or if the design has a problem that I have overlooked. If everythin is ok, I merge a fix in Pint as well, test and release.

hgrecco avatar Nov 06 '24 10:11 hgrecco

I just a pushed a fix (d592aff209a0eeae9383042368906dfb5edc5f54) and added python 3.13 to the ci (a8bcb6ee1d0d61278bf17e332bc1aa473672e273). It implements undataclassing exceptions.

If active users in this thread do not report any problem, I will release a new version of pint tomorrow.

hgrecco avatar Nov 07 '24 02:11 hgrecco

What time are you planning on pushing the fix? Seems like a known issue with later python versions that's been going on for a while.

Your fix works for me!

Ealameda31 avatar Nov 07 '24 03:11 Ealameda31

If active users in this thread do not report any problem, I will release a new version of pint tomorrow.

It works for me here. Thank you!

bje- avatar Nov 07 '24 03:11 bje-

This has started hitting us in our CI today (no problem yesterday) for python 3.12.7... could it be related? If interested, here is the gh actions log: https://github.com/pytroll/satpy/actions/runs/11718529478/job/32640722715?pr=2969

mraspaud avatar Nov 07 '24 08:11 mraspaud

Releasing flexparser==0.4 without the corresponding adjustment in pint has caused havoc in various CI

When do you plan to make new pint available?

epenet avatar Nov 07 '24 08:11 epenet

I can confirm pinning flexparser<0.4 works in our CI

mraspaud avatar Nov 07 '24 09:11 mraspaud

Should pint have upper flexparser e.g. flexparser>=0.4,<0.5?

ndevenish avatar Nov 07 '24 10:11 ndevenish

Should pint have upper flexparser e.g. flexparser>=0.4,<0.5?

Literally read the previous reply

Kattemageren avatar Nov 07 '24 11:11 Kattemageren

Should pint have upper flexparser e.g. flexparser>=0.4,<0.5?

Literally read the previous reply

I think the point is that pint should already have been using an upper constraint on flexparser. If pint had been using an upper constaint previously, the impact would have been greatly reduced...

epenet avatar Nov 07 '24 11:11 epenet

I think the point is that pint should already have been using an upper constraint on flexparser. If pint had been using an upper constaint previously, the impact would have been greatly reduced...

I mean, it would have avoided this issue here, but my point isn’t that it should have been restricted, and more that going forward, should it now be restricted, so it doesn’t happen again?

ndevenish avatar Nov 07 '24 11:11 ndevenish

I apologize for the inconvenience. In relation to upper version limits, I think the comment here also makes sense:

https://github.com/hgrecco/flexparser/issues/12#issuecomment-2462226462

While I am open for discussion, I think this type of changes will not happen very often. This change was needed due to a modification in Python 3.13 of the semantics of frozen dataclasses. Dataclasses appeared in Python 3.7, 6 years ago, and I considered this stable enough to be used in something as fundamental as exceptions. This was not the case and came back to bite me!.

Anyway, I will release a bug fix of Pint and we tackle this in another issue.

hgrecco avatar Nov 07 '24 16:11 hgrecco

While I am open for discussion

Upper bounds on a library's dependencies are usually not a good idea, so I'd be -1 on that: without the upper bound people can do something about the breaking change by pinning flexparser, but issues caused by upper bounds means that we can't do anything other than waiting for a new release.

So I'd much rather follow the advice quoted in the TL;DR of the article I linked to:

Libraries/packages should be setting a floor, and if necessary excluding known buggy versions, but otherwise don’t cap the maximum version as you can’t predict future compatibility

keewis avatar Nov 07 '24 16:11 keewis

I just pushed Pint 0.24.4. If everything is fine, It will be in PyPI shortly.

hgrecco avatar Nov 07 '24 16:11 hgrecco