Unable to add attributes to a subclass of PlayerEntity in __new__?

Please post any questions about developing your plugin here. Please use the search function before posting!
User avatar
Mahi
Senior Member
Posts: 236
Joined: Wed Aug 29, 2012 8:39 pm
Location: Finland

Unable to add attributes to a subclass of PlayerEntity in __new__?

Postby Mahi » Fri Jul 03, 2015 1:17 pm

Syntax: Select all

from players.entity import PlayerEntity

class MyPlayer(PlayerEntity):
def __new__(cls, index):
inst = super().__new__(cls, index)
inst.just_an_attribute = True
return inst

Raises the following error when used:

Code: Select all

[SP] Caught an Exception:
Traceback (most recent call last):
  File '..\addons\source-python\packages\source-python\events\listener.py', line
 93, in fire_game_event
    callback(game_event)
  File '..\addons\source-python\plugins\test\test.py', line 16, in player_say
    p = MyPlayer(index_from_userid(game_event.get_int('userid')))
  File '..\addons\source-python\plugins\test\test.py', line 7, in __new__
    inst.just_an_attribute = True
  File '..\addons\source-python\packages\source-python\entities\entity.py', line
 123, in __setattr__
    for server_class in self.server_classes:
  File '..\addons\source-python\packages\source-python\entities\entity.py', line
 224, in server_classes
    for server_class in server_classes.get_entity_server_classes(self):
  File '..\addons\source-python\packages\source-python\entities\classes.py', lin
e 147, in get_entity_server_classes
    datamap = entity.datamap

Boost.Python.ArgumentError: Python argument types in
    None.None(Player)
did not match C++ signature:
    None(class CBaseEntityWrapper {lvalue})
Would there be any fix for this? :smile:
User avatar
L'In20Cible
Project Leader
Posts: 1536
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Postby L'In20Cible » Fri Jul 03, 2015 2:59 pm

Check out how Entity overridesoverrides __new__. You need to call __init__ to construct the base class.
User avatar
Mahi
Senior Member
Posts: 236
Joined: Wed Aug 29, 2012 8:39 pm
Location: Finland

Postby Mahi » Fri Jul 03, 2015 3:24 pm

L'In20Cible wrote:Check out how Entity overridesoverrides __new__. You need to call __init__ to construct the base class.
Not sure what you mean by this. I can't define "just_an_attribute" in __init__, since it's something that needs to be defined only once. I ended up using object.__setattr__(inst, 'just_an_attribute', True) for now
User avatar
Mahi
Senior Member
Posts: 236
Joined: Wed Aug 29, 2012 8:39 pm
Location: Finland

Postby Mahi » Sat Jul 04, 2015 1:01 pm

Using object.__setattr__() isn't a feasibule solution, as my class is designed to be subclassed, and I'd have to use object.__setattr__() in all the subclasses too.

Would you guys have any workaround for this? I don't think it's desirable to have to call __init__ before you can set attributes on an instance. I personally need to use __new__ to set my attributes.
User avatar
satoon101
Project Leader
Posts: 2703
Joined: Sat Jul 07, 2012 1:59 am

Postby satoon101 » Sat Jul 04, 2015 1:18 pm

I guess I don't understand what your issue with __init__ is. My only guess is that perhaps you might need to use a metaclass, but I really don't fully understand your issue overall. I would provide further example, but I am not home right now.
Image
User avatar
L'In20Cible
Project Leader
Posts: 1536
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Postby L'In20Cible » Sat Jul 04, 2015 1:53 pm

The CBaseEntityWrapper constructor is linked to its __init__ method. Before setting an attribute, you need to construct it by initializing it. You does nothing into __new__ so your entire class is wrong, imo. You should simply overwrite __init__ to store an attribute.

Syntax: Select all

from players.entity import PlayerEntity

class MyPlayer(PlayerEntity):
def __init__(self, index):
super().__init__(index) # Object is constructed/initialized...
self.just_an_attribute = True # Yessir!
User avatar
Mahi
Senior Member
Posts: 236
Joined: Wed Aug 29, 2012 8:39 pm
Location: Finland

Postby Mahi » Sat Jul 04, 2015 2:05 pm

Here's a quick example of something similar to what I'm trying to do:

Syntax: Select all

from players.entity import PlayerEntity
from players.helpers import index_from_userid
from events import Event

_player_dict = {}


@Event
def player_hurt(game_event):
player = Player(index_from_userid(game_event.get_int('userid')))
damage = game_event.get_int('dmg_health')
player.health += int(damage * player.heal_multiplier)


class Player(PlayerEntity):

def __new__(cls, index):
if index not in _player_dict:
_player_dict[index] = super().__new__(cls, index)
return _player_dict[index]

def __init__(self, index):
self.heal_multiplier = 0.1
Every time I call Player(index), it returns the existing instance as it's supposed to, but every time __new__ returns an instance, Python calls its __init__. This means that heal_multiplier will be reset to 0.1 every time player takes damage, and I need it to stay at whatever value it has been changed to. The most obvious solution to this would be to define heal_multiplier in __new__, but your PlayerEntity class doesn't allow that.
User avatar
satoon101
Project Leader
Posts: 2703
Joined: Sat Jul 07, 2012 1:59 am

Postby satoon101 » Sat Jul 04, 2015 2:12 pm

Why don't you use the global dictionary instead of calling Player() constantly?

Syntax: Select all

player = player_dictionary[game_event.get_int('userid')]


You 'could' also set that value at the class level instead of using __new__ or __init__.
Image
User avatar
Mahi
Senior Member
Posts: 236
Joined: Wed Aug 29, 2012 8:39 pm
Location: Finland

Postby Mahi » Sat Jul 04, 2015 2:19 pm

satoon101 wrote:Why don't you use the global dictionary instead of calling Player() constantly?

Syntax: Select all

player = player_dictionary[game_event.get_int('userid')]


You 'could' also set that value at the class level instead of using __new__ or __init__.
What if someone wants to subclass my Player class? They'd have to change the way my player_dictionary works (if it was something like defaultdict, with __missing__ overridden to default to a new Player instance).

Using a class attribute is not an option, as some of my attributes are mutable (dicts, lists, ...).

Edit: Even if there was a similar workaround (some kind of global dict) to this problem, I still think your implementation (forcing __init__ call unlike anything else in Python) is wrong and there's no reason why a class should prevent its subclasses from defining attributes in __new__ (especially when it's still possible using object.__setattr__ and doesn't seem to cause any errors or anything).
User avatar
L'In20Cible
Project Leader
Posts: 1536
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Postby L'In20Cible » Sat Jul 04, 2015 2:56 pm

You can calls your __init__ after __new__ in your own __new__ method. As long as it is called prior setting an attribute so boost can link the instance to its wrapper.
User avatar
satoon101
Project Leader
Posts: 2703
Joined: Sat Jul 07, 2012 1:59 am

Postby satoon101 » Sat Jul 04, 2015 2:57 pm

If someone subclassed your Player class, they would have to overwrite your __new__ anyway and use a different dictionary. The global dictionary only holds one instance per index. I liked your other edits that you now seem to have edited out where you admitted your implementation is wrong. Setting instance attributes should primarily be done in instance based methods, not class methods like __new__. I honestly don't know exactly why that error happens when it happens. I will have to do some testing when I get home.
Image
User avatar
Mahi
Senior Member
Posts: 236
Joined: Wed Aug 29, 2012 8:39 pm
Location: Finland

Postby Mahi » Sat Jul 04, 2015 4:48 pm

L'In20Cible wrote:You can calls your __init__ after __new__ in your own __new__ method. As long as it is called prior setting an attribute so boost can link the instance to its wrapper.
Again didn't quite understand, sorry... :( Can you give an example? What do you mean call my __init__ after __new__?
User avatar
L'In20Cible
Project Leader
Posts: 1536
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Postby L'In20Cible » Sat Jul 04, 2015 5:46 pm

This is how Boost is implemented. The constructor is linked to the __init__ method. So, if you want to construct it correctly, you have to call the initializer yourself.

Syntax: Select all

from players.entity import PlayerEntity

_player_dict = dict()

class Player(PlayerEntity):

def __new__(cls, index):
if index not in _player_dict:

# Get a PlayerEntity instance...
self = PlayerEntity.__new__(cls, index)

# Initialize/construct the BaseEntity object...
super(Player, self).__init__(index)

# Set whatever attributes...
self.foo = None
self.bar = self.foo

# Store the Player instance...
_player_dict[index] = self

# Return the instance...
return self

pl = Player(1)
print(pl.datamap) # BaseEntity attribute...
print(pl.foo, pl.bar) # Our attributes...
print(pl.server_classes) # PlayerEntity attribute...


Code: Select all

_entities._datamaps.DataMap object at 0x00000000)
None None
<generator object server_classes at 0x00000000)
User avatar
L'In20Cible
Project Leader
Posts: 1536
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Postby L'In20Cible » Sat Jul 04, 2015 6:00 pm

User avatar
Mahi
Senior Member
Posts: 236
Joined: Wed Aug 29, 2012 8:39 pm
Location: Finland

Postby Mahi » Sun Jul 05, 2015 9:55 am

satoon101 wrote:If someone subclassed your Player class, they would have to overwrite your __new__ anyway and use a different dictionary. The global dictionary only holds one instance per index. I liked your other edits that you now seem to have edited out where you admitted your implementation is wrong. Setting instance attributes should primarily be done in instance based methods, not class methods like __new__. I honestly don't know exactly why that error happens when it happens. I will have to do some testing when I get home.

I don't think they would have to use a different dictionary. My __new__ handles that, that's the whole reason of the cls parameter; instead of adding an instance of my class into the player dictionary, my player's __new__ would add an instance of their class to the player dictionary. And you'll never need to have an instance of my class AND an instance of their class at the same time; similar to me not needing PlayerEntity anymore, I can always use my class instead (subclasses PlayerEntity -> all the same attributes + more).

And I do agree that primarily instance attributes should be set in __init__, but as I explained, it's not possible in this case. Setting them in __init__ for cached instances like the one I'm doing will only result in the attributes resetting every time; which is not good. I even asked a question about this on stackoverflow if there'd be a workaround or way to prevent the __init__ from being called after __new__, but there's none. My question didn't get any answers, just Martijn Pieter's one comment "Why would you not use __new__ to define the attributes here?" (it was an abstract example with cats and what not, they didn't see your PlayerEntity class obviously).


Thank you very much, looks good! :) And thanks to satoon too for "wasting" your time with me, once again. :D
User avatar
satoon101
Project Leader
Posts: 2703
Joined: Sat Jul 07, 2012 1:59 am

Postby satoon101 » Sun Jul 05, 2015 11:58 am

You're completely missing my point. Yes, they would have to have a separate dictionary. The very first one that gets added for any index is the class that will be used no matter what. Here is an example:

Syntax: Select all

>>> my_dict = dict()
>>> class Base(object):
def __new__(cls, index):
self = super(Base, cls).__new__(cls)
return self
def __init__(self, index):
self.index = index


>>> class Test1(Base):
def __new__(cls, index):
if index not in my_dict:
my_dict[index] = super(Test1, cls).__new__(cls, index)
return my_dict[index]

>>> class Test2(Test1):
...


>>> class Test3(Test1):
...


>>> one = Test2(1)
>>> two = Test3(1)
>>> one
<__main__.Test2 object at 0x02A8E3B0>
>>> two
<__main__.Test2 object at 0x02A8E3B0>


The variable two "should" be (is expected to be) a Test3 instance and not a Test2 instance. However, since Test2's instance for index 1 was created first, and therefor added to the dictionary first, it is the one and only instance from that dictionary.


*Edit: Sorry, just read this part of your post:
Mahi wrote:And you'll never need to have an instance of my class AND an instance of their class at the same time

This was where the confusion came in. So, you are saying that your class is supposed to be inherited and that each player will only have 1 class that is used to inherit from the main class? I guess that makes 'some' sense, but was not clear until this line I quoted.
Image
User avatar
Mahi
Senior Member
Posts: 236
Joined: Wed Aug 29, 2012 8:39 pm
Location: Finland

Postby Mahi » Sun Jul 05, 2015 2:29 pm

Yes, your edit is correct. :) You wouldn't need to use my Player class for anything if you were to subclass your own :) Sorry if I explained it badly.
User avatar
Mahi
Senior Member
Posts: 236
Joined: Wed Aug 29, 2012 8:39 pm
Location: Finland

Postby Mahi » Mon Jul 06, 2015 9:36 am

Syntax: Select all

class P(PlayerEntity):
_instances = {}

def __new__(cls, index):
if index not in cls._instances:
instance = cls._instances[index] = super().__new__(cls, index)
instance._effects = collections.defaultdict(list)
instance.restrictions = set()
return cls._instances[index]
Would this work? Seems like it'd solve all my problems (now that L'In20Cible updated SP's files), and it'd fix the issue you were talking about.

I might need a metaclass to create the _instances dict separately for all classes though?

Syntax: Select all

class _Meta(type):
def __new__(cls, name, bases, attrs):
attrs['_instances'] = dict()
return super().__new__(cls, name, bases, attrs)
User avatar
satoon101
Project Leader
Posts: 2703
Joined: Sat Jul 07, 2012 1:59 am

Postby satoon101 » Mon Jul 06, 2015 12:40 pm

Correct, you would need to use the metaclass to create the _instances attribute on a per-class instance. Either that, or every sub-class would have to define their own _instances attribute, but that wouldn't be ideal.
Image
User avatar
Mahi
Senior Member
Posts: 236
Joined: Wed Aug 29, 2012 8:39 pm
Location: Finland

Postby Mahi » Mon Jul 06, 2015 2:24 pm

Indeed, it wouldn't be ideal, which is why I tried metaclasses. However, there seems to be a metaclass conflict, apparently PlayerEntity already has a custom metaclass (coming from BaseEntity, couldn't find it elsewhere? I could be blind though):

Code: Select all

[SP] Caught an Exception:
Traceback (most recent call last):
  File '..\addons\source-python\packages\source-python\plugins\manager.py', line
 72, in __missing__
    instance = self.instance(plugin_name, self.base_import)
  File '..\addons\source-python\packages\source-python\plugins\instance.py', lin
e 82, in __init__
    self._plugin = import_module(import_name)
  File '..\addons\source-python\plugins\test\test.py', line 1, in <module>
    from testplayer import TestPlayer
  File '..\addons\source-python\packages\custom\testplayer.py', line 98, in <mod
ule>
    class TestPlayer(PlayerEntity, metaclass=_TestPlayerMeta):

TypeError: metaclass conflict: the metaclass of a derived class must be a (non-s
trict) subclass of the metaclasses of all its bases
Any suggestions? Where does the other metaclass even come from?

Return to “Plugin Development Support”

Who is online

Users browsing this forum: No registered users and 69 guests