Add public functions to inspect dataclass parameters and methods

So, I’ve done some thinking on this and have some modifications to my original proposal:

  1. Add a require_decorator[1] parameter to is_dataclass (default as False), this will check that __dataclass_fields__ is in the class dict and not just inherited.

  2. Make _is_dataclass_instance public and add is_dataclass_type.

    • I think these are just useful helpers, I don’t see why _is_dataclass_instance needs to be private, I’ve seen it duplicated enough and it’s not something we’re likely to change.
  3. Add a __generated_for_dataclass__ attribute to dataclass methods that is a reference to the class the method was actually created for, potentially along with a function get_method_class[2] to retrieve this that returns None if the attribute doesn’t exist.

  4. Add an is_frozen function to check if a class/instance is actually itself frozen, meaning you can’t set arbitrary attributes.

    • Essentially shorthand for checking the class __setattr__ and __delattr__ were generated for the exact class being examined.
    • Potentially we could also have has_frozen_fields to indicate the presence of some inherited frozen fields, but I’m not sure this is that useful and it can get pretty messy if you start to consider multiple inheritance cases (like frozen ‘fields’ that aren’t in fields(cls)).
  5. Don’t provide params to get the parameters

    • I’ve changed my mind here after changing the attribute attached to the method to give the class the method was generated for which covers the original use case.
    • Many of the parameters will be ignored[3] depending on other class features, so the features should be checked directly instead of relying on parameters that may not reflect the state of the actual class.

With these changes:

  1. Checking or replacing methods

To check if a __repr__ has been generated for a specific class you can use:

get_method_class(t.__repr__) is t

To check if you can replace a __repr__ you’d now use something like:

if (repr_cls := get_method_class(cls.__repr__))): ...

As repr_cls (if it is not None) is the class used to generate the original __repr__ that would otherwise be used, dataclasses.fields(repr_cls) can be used to construct a replacement without needing any other requirements.

The behaviour of enum __repr__ replacement would change slightly so that if __repr__ has been replaced it would no longer make a custom enum repr, rather than doing so based solely on whether repr=True is declared in the dataclass params. The dataclasses documentation states that if the __repr__ is defined, the repr parameter should be ignored and the enum behaviour should really respect this.

  1. Distinguishing decorated and undecorated classes
>>> from dataclasses import _FIELDS, dataclass
>>> def is_dataclass(obj, *, require_decorator=False):
...     cls = obj if isinstance(obj, type) else type(obj)
...     if require_decorator:
...         return _FIELDS in cls.__dict__
...     return hasattr(cls, _FIELDS)
...
>>> @dataclass
... class A:
...     a: int = 1
...
>>> @dataclass
... class B:
...     b: int = 2
...
>>> @dataclass
... class C(B, A):
...     pass
...
>>> class D(B, A):
...     pass
...
>>> is_dataclass(C), is_dataclass(D)
(True, True)
>>> is_dataclass(C, require_decorator=True), is_dataclass(D, require_decorator=True)
(True, False)

  1. decorated_class or is_decorated seem nicer but imply that setting it to False should exclude decorated classes which isn’t the intention. Open to other possible names though. ↩︎

  2. open to suggestions for better names here ↩︎

  3. in practice this isn’t completely accurate currently, but it should be ↩︎

2 Likes