I'm using ndb.polymodel.PolyModel to model different types of social media accounts. After an account has been deleted some cleanup has to be done (depending on the subclass).
I tried implementing a _pre_delete_hook on the subclass, but it never gets executed. When adding the exact same hook to its parent class it does get executed:
class Account(polymodel.PolyModel):
user = ndb.KeyProperty(kind=User)
#classmethod
def _pre_delete_hook(cls, key):
# Works as expected
pass
class TwitterAccount(Account):
twitter_profile = ndb.StringProperty()
#classmethod
def _pre_delete_hook(cls, key):
# Never gets called
pass
t_account = TwitterAccount.get_by_id(1)
t_account.key.delete()
Seems strange to me as I would expect the subclass hook to override its parent. Or is this expected behavior?
Solution:
I missed the fact that a delete operation happens on a Key (not a model instance). The key itself only knows the name of the topmost class (Account in this case).
I ended up defining a custom _delete_hook instance method on the subclass. When _pre_delete_hook gets called, I fetch the entity and then check if its class has a specific delete_hook to execute:
# In subclass:
def _delete_hook(self):
# Do stuff specific for this subclass
return
# In parent class
#classmethod
def _pre_delete_hook(cls, key):
s = key.get()
if hasattr(s, '_delete_hook'):
s._delete_hook()
Unfortunately this is expected though non-obvious behaviour
When you call key delete with a PolyModel you are only calling delete on the parent class.
Have a look at the Key you are calling delete on, you will see the Kind is the parent Model. It then looks up the class via the Kind -> class map which will give you an Account class. Have a read up on how PolyModel works. It stores all Account sub classes as Account and has an extra property that describes the inheritance heirarchy. So that a query on Account will return all Account subclasses, but a query on TwitterAccount will only return that subclass.
Calling ndb.delete_multi won't work either.
If you want a specific PolyModel subclass pre delete hook to run, you will have to add a delete method to the subclass, call that, that can then call the subclass _pre_delete_hook (and may be call super).
But that will cause issues if you ever call key.delete directly
Related
Short version: For normal Object members, this is simple - you just use self.member in the parent, and the child can override the member and everything is happily object orientated. But what about class members that may get overriden by a child class?
To make that clear:
I know in Python I can just give Classname.membername but how do I allow Class members to be overriden? For example I have
class BaseDB:
user_table = "users"
# Class member function
def make_sql():
return f'SELECT * from {BaseDB.user_table};'
# Boilerplate functions that use "user_table"
def load_users(self):
return db.connection.cursor.execute(BaseDB.make_sql())
class SomeDatabase(BaseDB):
user_table = "the_users_table"
# Somewhere else in the code....
dbwrapper = SomeDatabase()
dbwrapper.load_users() # This does not use the overwridden value in SomeDatabase, it uses the value form the parent class
What I'm looking for something like self, but for the Class, not for the object.
At first I thought I could try and use type(self).class_member .... Not sure if that would even work, but self might not be available, eg in a non-class member method.
My workaround at present is to use normal object members - right now I don't have a requirement to access these from a class method. The only reason why I want to make these into Class members is because the values are constant for the class, not the object.
Edit: Added the somewhat contrived Class member function make_sql and used it in the normal member function.
Note: The derived class only sets the class member - Nothing else is overridden.
Note #2 - This is not a major obstacle. If there is a solution, I am sure I can use it ... if not, I've been getting along without it fine, so I'm just asking to find out if I'm missing out on something.
EDIT #2:
I just thought I should put the code into my IDE and see if I had any errors (I'm lazy). The IDE told me I need to have a "self" on the class member so I annotated it with #classmember. The IDE told me "Well then you should have a cls parameter. VOILA! This solves the problem, of course.
I'm trying to create a class which maps to a mongoDB collection.
My code looks like this:
class Collection:
_collection = get_collection() # This seems not working
#classmethod
def get_collection(cls):
collection_name = cls.Meta.collection_name if cls.Meta.collection_name \
else cls.__name__.lower()
collection = get_collection_by_name(collection_name) # Pseudo code, please ignore
return collection
class Meta:
collection_name = 'my_collection'
I came across a situation where I need to assign the class variable _collection with the return value of get_collection.
I also tried _collection = Collection.get_collection() which also seems not to be working
As a work-around, I subclassed Collection and set value of _collection in the child class.
Would like to know any simple solution for this.
Thanks in advance
As DeepSpace mentions, here:
class Collection:
_collection = get_collection() # This seems not working
#classmethod
def get_collection(cls):
# code that depends on `cls`
the get_collection method is not yet defined when you call it. But moving this line after the method definition won't work either, since the method depends on the Collection class (passed as cls to the method), which itself won't be defined before the end of the class Collection: statement's body.
The solution here is to wait until the class is defined to set this attribute. Since it looks like a base class meant to be subclassed, the better solution would be to use a metaclass:
class CollectionType(type):
def __init__(cls, name, bases, attrs):
super(CollectionType, cls).__init__(name, bases, attrs)
cls._collection = cls.get_collection()
# py3
class Collection(metaclass=CollectionType):
# your code here
# py2.7
class Collection(object):
__metaclass__ = CollectionType
# your code here
Note however that if Collection actually inherit from a another class already having a custom metaclass (ie Django Model class or equivalent) you will need to make CollectionType a subclass of this metaclass instead of a subclass of type.
There are some design/syntax errors in your code.
When the line _collection = get_collection() executes, get_collection is not yet defined. As a matter of fact, the whole Collection class is not yet defined.
get_collection_by_name is not defined anywhere.
EDIT OP updated the question so the below points may not be relevant anymore
collection = get_collection(collection_name) should be collection = cls.get_collection(collection_name)
Sometimes you are passing a parameter to get_collection and sometimes you don't, however get_collection's signature never accepts a parameter.
Calling get_collection will lead to an infinite recursion.
You have to take a step back and reconsider the design of your class.
I have some trouble with Python class creations. My task is to create objects using some parse method. But I want to turn off basic class creation using __init__
For example, I have
class A:
#classmethod
def create_from_file(cls, file):
# Create instance form file...
return self
This gives me an opportunity to create object using command like this
a = A.create_from_file()
But this code provides me a chance to create instance using __init__
a = A() won't raise an exception...
When I try to add own __init__ method, my parse function also raises an exception.
class A:
def __init__(self):
raise NotImplementedError
#classmethod
def create_from_file(cls, file):
# Create instance form file...
return self
How I can fix this trouble, and what is the most Pythonic way to write this classes?
__init__ is not responsible for creating a instance. It is a hook method that Python calls for you after the instance is already created. You can't prevent instance creation from there. Besides, you don't want to prevent all instance creation, even your classmethod has to create an instance at some point.
Since all you want to do is raise an exception when your factory method is not used to create the instance, it's still fine to raise an exception in __init__ method. That'll prevent the new instance from being assigned anywhere. What you need to do then is distinguish between direct access, and your factory method being used.
You could achieve this is several different ways. You could use a "secret" token that only the factory method passes in:
_token = object() # unique token to flag factory use
class A:
def __init__(self, data, _from_factory=None):
if _from_factory is not _token:
raise TypeError(f"Can't create {type(self).__name__!r} objects directly")
self._data = data
#classmethod
def create_from_file(cls, file):
data = file.read()
return cls(data, _from_factory=_token)
The classmethod still creates an instance, the __init__ is still called for that instance, and no exception is raised because the right token was passed in.
You could make your class an implementation detail of the module and only provide a public factory function:
def create_from_file(cls, file):
data = file.read()
return _A(data)
class _A:
def __init__(self, data):
self._data = data
Now the public API only gives you create_from_file(), the leading underscore tells developers that _A() is an internal name and should not be relied on outside of the module.
Actual instance creation is the responsibility of the object.__new__ method; you could also use that method to prevent new instances to be created. You could use the same token approach as I showed above, or you could bypass it altogether by using super() to call the original overridden implementation:
class A:
def __new__(cls, *args, **kwargs):
raise TypeError(f"Can't create {cls.__name__!r} objects directly")
def __init__(self, data):
self._data = data
#classmethod
def create_from_file(cls, file):
data = file.read()
# Don't use __new__ *on this class*, but on the next one in the
# MRO. We'll have to manually apply __init__ now.
instance = super().__new__(cls)
instance.__init__(data)
return instance
Here a direct call to A() will raise an exception, but by using super().__new__ in the classmethod we bypass the A.__new__ implementation.
Note: __new__ is implicitly made a staticmethod, so we have to manually pass in the cls argument when we call it from the classmethod.
If you only have one method to create the object, just make that the constructor. That is, instead of
#classmethod
def create_from_file(cls, file):
# Create instance form file...
return self
you would have
def __init__(self, file):
# Create instance form file...
If there are several different methods of creating an object, it's still typically going to be the case that one of them is more fundamental than any other - in other words, that the arguments to other methods can be "converted" into the arguments to the one method. For example, you might have create_from_file(), create_from_url(), and create_from_string(), where the former two methods basically read the content of the file or URL and then do the same thing with it that create_from_string() does. So just turn create_from_string() into __init__(), and have the other two methods read the file or URL and call the constructor with the content.
If you really don't have a single "fundamental" way of creating the object, it's probably worth considering whether you should have different subclasses of a base class.
I used to register sqlalchemy events in the classmethod __declare_last__.
My code looked like this:
#classmethod
def __declare_last__(cls):
#event.listens_for(cls, 'after_udpate')
def receive_after_update(mapper, conn, target):
...
This worked correctly until I upgraded to SQLAlchemy 1.0, with which this hook was not called and my events were thus not registered.
I've read the 1.0 document about __declare_last__ and discovered nothing related.
After searching the source code of SQLAlchemy1.0.4 for __declare_last__, I've located the place where both __declare_last__ and __declare_first__ is found and registered.
def _setup_declared_events(self):
if _get_immediate_cls_attr(self.cls, '__declare_last__'):
#event.listens_for(mapper, "after_configured")
def after_configured():
self.cls.__declare_last__()
if _get_immediate_cls_attr(self.cls, '__declare_first__'):
#event.listens_for(mapper, "before_configured")
def before_configured():
self.cls.__declare_first__()
Then I used pdb to step through this method and found that _get_immediate_cls_attr(self.cls, '__declare_last__') was returning None for a class with this hook method inherited.
So I jumped to the definition of _get_immediate_cls_attr which contained a docstring that solved my problem:
def _get_immediate_cls_attr(cls, attrname, strict=False):
"""return an attribute of the class that is either present directly
on the class, e.g. not on a superclass, or is from a superclass but
this superclass is a mixin, that is, not a descendant of
the declarative base.
This is used to detect attributes that indicate something about
a mapped class independently from any mapped classes that it may
inherit from.
So I just added a mixin class, moved __declare_last__ to it and made the original base class inherit the mixin, and now __declare_last__ finally got called again.
This is supposed to be a Django-specific, but I guess it's Python anyway.
Basically, I don't want to override the work of the original method in the class I am inheriting (could be a Model class), but I'd like to add additional validation. Is this possible? Any hint?
class MyUserAdminForm(forms.ModelForm):
class Meta:
model = User
def clean(self):
// do some additional work even though it's cleaned by parent's clean method
Call the super classes clean method:
def clean(self):
super(MyUserAdminForm, self).clean()
# more cleaning
This is a common python thing to do when you subclass something and redefine functionaly but want to make sure you keep the super class functionality. Extremely common when you do an init method, as you always need to ensure the super class constructor gets called to set up the instance.
class ContactForm(forms.Form):
message = forms.CharField()
def clean_message(self):
num_words = len(message.split())
if num_words<4:
raise forms.ValidationError("Too short a message!")
return message
This is the way you add a validation method on a field, and this does ensure that the default cleanup happens. There is no need to call the default cleanup method again.
Source: www.djangobook.com
How it works:
When is_valid() is called on the form object, the system looks for any methods in the class that begin with clean_ and ends with an attribute name. If they do, it runs them after running the default cleanup methods.