Search code examples
pythonpygamepython-dataclasses

Dataclass breaks when adding a decorator to it


The error I get is TypeError: must be real number, not Field from the rendering code which tries to rotate an image.

It only does this when I try to decorate live with activated_range.

@dataclass
class EntityZombie(entity_manager.Clickable, entity_manager.Rotatable, entity_manager.BaseEntity):
    max_distance = 125
    max_health = 20.0
    # more class variables go here
    # there's also a field here, which gets initialized to 0 without any issues
    last_attack: int = field(init=False, default=0)

    @utils.activated_range(250)
    def live(self, game) -> None:
        ...

The decorator tries to check the distance from the entity to the player, and only run things if it's close enough to the player:

def activated_range(activate_distance: float):
    def decorator(method):
        def new_func(instance, game):  # self.live(game)
            # if the distance is too large, do not execute the method (live)
            if instance.player_distance(game.player) > activate_distance:
                return
            method(instance, game)
        return new_func

    return decorator

Rotatable is an ABC that specifies the field rotation:

class Rotatable(BaseEntity, abc.ABC):
    rotation: float = field(init=False, default=0.0)
    # some more methods that are not relevant here

Normally, the value of rotation correctly gets set to 0 (as it's the default), but whenever I have the live decorated with activated_range, it will always set the value of rotation to a dataclass Field instead of the number it's supposed to be defaulted to.

I thought this might be an issue with the decorator, but live = utils.activated_range(250)(live) also gives the same error.

Even weirder, on some tries when I run this, one or two of the EntityZombies actually do have their rotations set correctly, but then eventually one of them doesn't and the program crashes. The debugger shows that rotation is set to the actual field, but the dataclass should be making it into the float (as that's what the default is set to). It did that when I didn't decorate that function, so why not now?

enter image description here

Why does adding the decorator to one method break the dataclass and have the fields not default correctly? Any help would be greatly appreciated!

Full error:

Traceback (most recent call last):
  File "/home/greateric/Documents/PYTHON/immatureidiotsimulator/main.py", line 131, in <module>
    Game()()
  File "/home/greateric/Documents/PYTHON/immatureidiotsimulator/main.py", line 43, in __call__
    self.mainloop()
  File "/home/greateric/Documents/PYTHON/immatureidiotsimulator/main.py", line 126, in mainloop
    self.redraw_queue()
  File "/home/greateric/Documents/PYTHON/immatureidiotsimulator/main.py", line 112, in redraw_queue
    entity.blit_me(self.asset_manager)
  File "/home/greateric/Documents/PYTHON/immatureidiotsimulator/entity/entityzombie.py", line 56, in blit_me
    super().blit_me(asset_manager)
  File "/home/greateric/Documents/PYTHON/immatureidiotsimulator/entity/entity_manager.py", line 197, in blit_me
    surface = pygame.transform.rotate(surface, self.rotation)
TypeError: must be real number, not Field

Solution

  • I tried to reproduce it here, with an even more abstract and smaller minimal code, and invariably, I got the field value instead of a number whenever the mixin class was not declared a dataclass.

    So, I am confident there is some code in there that is setting the rotation of your EntityZombie instance to 0 prior to .blit_me() being called - that would explain why some of the instances work. Also, it makes sense that this behavior is prevented to run whenever activate_range is in effect: the decorator effect will prevent that other code to set the instance value to 0.

    The workaround, as you had found out, is just to declare whatever mixins you have with declared field attributes as dataclasses as well - this will "activate" the field descriptor for the most derived classes.