0

I have the following class, which i want to be represented as a string (only after setting body value on __init__):

class Component():
    body = ''
    template = None
    context = {}

    def __init__(self):
        if self.template:
            self.template = 'dashboards/' + self.template
            self.body += render_to_string(self.template, context = self.context)

    def __repr__(self):
        return str(self.body)

So for example, if i print an instance of Title class down below..

class Title(Component):
    template = 'basic_components/title.html'
    defaults = {
        'text': 'Title',
    }

    def __init__(self, context=None):
        self.context = {**self.defaults, **(context or {})}
        super().__init__()

.. i'll get a string object, which is what i wanted/expected due to the parent __repr__ method.

However, i want every Component to behave like a string, and not only represent as a string. For example, if i sum two Title instances, i want the same behavior as summing two str objects (concat)

I could go over to Component class and include..

    def __add__(self, obj):
        return self.__repr__() + obj.__repr__()

..and it would work. However, i want most of the string methods available. For example, i want to be able to pick a Title instance and do .join([..]) with it.

I tried inherit str on Component like so: class Component(str): but with no sucess.

Also, i've tried to manipulate the __new__ constructor on Component, but that left me even more confused, since i need self.body to be calculated before creating the string object, and __new__ is executed before __init__..

How can i make Component objects totally behave like str objects with the value of body?

EDIT:

class Component(str):
    def __new__(cls, body=None, template=None, context=None):
        if not body:
            body = ''

        if template:
            template = 'dashboards/' + template
            body += render_to_string(template, context = context)

        return super().__new__(body)

class Title(Component):
    template = 'basic_components/title.html'
    defaults = {
        'text': 'Title',
    }

    def __init__(self, context=None):
        self.context = {**self.defaults, **(context or {})}
        super().__init__()

t = Title()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 8, in __new__
TypeError: str.__new__(X): X is not a type object (str)
Ricardo Vilaça
  • 846
  • 1
  • 7
  • 18
  • 1
    You should make it a subclass of `str`. – Barmar Jul 13 '21 at 17:13
  • `body` is a class variable, not an instance variable. So all your `Compoment` objects have the same `body`. – Barmar Jul 13 '21 at 17:14
  • It's hard to tell if your class attributes are really supposed to be class attributes, or if you are incorrectly using them as instance variable default values. – chepner Jul 13 '21 at 17:15
  • Related: https://stackoverflow.com/questions/7255655/how-to-subclass-str-in-python – jarmod Jul 13 '21 at 17:16
  • @Barmar `Component` `__init__` overrides `self.body` so each `Component` instance has a different `body` - The reason i put `body = ''` is to always have that attr even if the child class does not set it – Ricardo Vilaça Jul 13 '21 at 17:20
  • @chepner which ones exactly? They might be incorrectly assigned.. Tell me which ones are looking the weirdest and i'll try to explain the logic behind it – Ricardo Vilaça Jul 13 '21 at 17:21
  • @jarmod like i said, i've seen those examples but i don't really know how to use `__new__` here, since i want to initalize `str` with the value of `self.body` (and to use self, i need `__init__`, which comes after `__new__`) – Ricardo Vilaça Jul 13 '21 at 17:22
  • If `body` is an instance attribute, then initialize `self.body = ''` in `__init__`. Don't make it a class attribute because you think you are being less repetitive in some meaningful sense. – chepner Jul 13 '21 at 17:24
  • @chepner True. Got it! gonna change that – Ricardo Vilaça Jul 13 '21 at 17:25
  • The same goes for `context`: the fact that `Title` makes it an instance attribute suggests it shouldn't be a class attribute in `Component` either. – chepner Jul 13 '21 at 17:25
  • If you subclass `str` as Barmar recommends, then `body` goes away: the value of the object itself (as an instance of `str`) is the body. – chepner Jul 13 '21 at 17:26
  • Ok i think i know what to do now.. I was being silly. I'll be right back – Ricardo Vilaça Jul 13 '21 at 17:26

1 Answers1

1

I suspect you want something like

class Component(str):
   
    def __new__(cls, value=None, *, template=None, context=None):
        if context is None:
            context = {}
        if template is not None:
            template = 'dashboards/' + template
            body = render_to_string(template, context)
        else:
            body = value

        return super().__new__(cls, value)

Component is basically a string whose value can be taken from a positional argument or constructed from two optional keyword arguments template and context. In this case, the positional argument is effectively ignored if template is used.

class Title(Component):
    
    def __new__(cls, value=None, **kwargs):
        kwargs.setdefault('context', {}).update(text='Title')
        kwargs['template'] = 'basic_components/title.html'
        return super().__new__(cls, value, **kwargs)

t = Title("title of the song")

Title is just a subclass that forces a particular value in the context and overrides any explicitly passed template argument.

Then repr(t) == "'title of the song'" (like any other str value).

All "class attributes" have been replaced by default argument values or hard-coded constants. __new__ is used instead of __init__, as you can only create new instances of str, not modify them. The object's value itself replaces the body attribute.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • Can you check the edit section on my original post? I've changed the `__new__` constructor as you mentioned, it makes more sense. However i did not include `__new__` on the `Title`class.. Shouldn't just `__init__` on the `Title` class be enough? – Ricardo Vilaça Jul 13 '21 at 17:59
  • `str.__init__` doesn't really do anything, so setting `self.context` in `__init__` happens too late for `Component.__new__` to do anything with it. – chepner Jul 13 '21 at 18:08
  • `__new__` is special-cased to be a static method, even if not declared as such, so `cls` needs to be passed explicitly, even with `super()`. Fixed. – chepner Jul 13 '21 at 18:08