Format class attributes with __getattribute__ overriding

Viewed 98

I try to override the__getattribute__ special method in a dataclass in order to get a formatted version of an attribute.

@dataclass
class _BaseRouteRegistry:
    VERSION = '1'
    USERS = 'users'

    def __getattribute__(self, item):
        route = object.__getattribute__(self, item)
        return f"/api/{route}/v{self.VERSION}"

BaseRouteRegistry = _BaseRouteRegistry()

print(BaseRouteRegistry.USERS)

But I got a RecursionError. Where I expected to get in the output: /api/users/v1

What I don't get if I directly return the object.__getattribute__(self, item) like so :

@dataclass
class _BaseRouteRegistry:
    VERSION = '1'
    USERS = 'users'

    def __getattribute__(self, item):
        return object.__getattribute__(self, item)

BaseRouteRegistry = _BaseRouteRegistry()

print(BaseRouteRegistry.USERS)

But then I just got users in output (of course not formatted since I removed the format string expression)

I saw many other answers on the RecusrionError like those ones :

How do I implement __getattribute__ without an infinite recursion error?

python __getattribute__ RecursionError when returning class variable attribute

In those questions above, they suggest to use the unbounded object.__getattribute__(self, item).

I do understand why, and I tried to use it.

But in my example, since I need to return a formatted version of the attribute, I first need to retrieve it and only then return the formatted version.

How to achieve this ?

2 Answers

You got an infinite recursion when you try to access self.VERSION here f"/api/{route}/v{self.VERSION}" because self.VERSION also invokes __getattribute__ method

So, you should handle VERSION attribute separately

from dataclasses import dataclass


@dataclass
class _BaseRouteRegistry:
    VERSION = '1'
    USERS = 'users'

    def __getattribute__(self, item):
        if item == "VERSION":
            return object.__getattribute__(self, item)

        route = object.__getattribute__(self, item)
        return f"/api/{route}/v{self.VERSION}"

BaseRouteRegistry = _BaseRouteRegistry()

print(BaseRouteRegistry.USERS)
> /api/users/v1

Eugenij's answer explains the issue with your code and how to fix it. However, if the goal is to control access to a specific attribute USERS, then you don't need to override __getattribute__() which affects access to all attributes. Instead, you can make USERS a property:

class _BaseRouteRegistry:
    VERSION = '1'
    _USERS = 'users'
    
    @property
    def USERS(self):
        return f"/api/{self._USERS}/v{self.VERSION}"

BaseRouteRegistry = _BaseRouteRegistry()

print(BaseRouteRegistry.USERS) 
Related