6

I have been looking a the source code for peewee, specifically Model and the update function: https://github.com/coleifer/peewee/blob/a33e8ccbd5b1e49f0a781d38d40eb5e8f344eee5/peewee.py#L4718

I don't like the fact that this method can be called from a row instance, when any update operation affects every row in the model if the statement is not correctly coupled with a where clause. Thus, I want to find some way to disallow calling this classmethod from the model instances.

Some googling leads me to believe that this may be quite difficult. delattr from __init__ did not seem to work. And running isclass(self) from the uppdate function always returns True since it appears that when we are inside the classmethod we actually are the class and not the instance.

Any suggestions?

Harald Nordgren
  • 11,693
  • 6
  • 41
  • 65
  • I don't think you can do it with `classmethod`. You would have to write your own descriptor similar to `classmethod` that checks whether its being bound on an instance and raises an exception. – BrenBarn Feb 19 '17 at 02:26

2 Answers2

4

You can customize the class __getattribute__ as in Schwobaseggl's answer - but you could also use a custom metaclass.

When we mention "metaclass" in Python, one ordinarily thinks of overriding its __new__ method and doing complicated things at class creation time (in contrast with instance creation time). However, if you leave all special dunder (__these__ __methods__) aside, a metaclas is just a class's class - and all its methods will be visible from the class itself, but won't be visible from the class's instances. That means, they won't show up when one "dir"s an instance, but will show up when one "dir" the class - and won't be directly retrievable through the instance. (Although, of course, one can always do self.__class__.method)

Moreover, despite metaclasse's justified bad-fame of complexity, overriding __getattribute__ itself can have some pitfalls.

In this specific case, the classs you want to protect alreayd use a metaclass - but this particular use, unlike "ordinary" metaclass uses, can be freely composable just like an ordinary class hierarchy:

class ClsMethods(BaseModel):  
     # inherit from `type` if there is no metaclass already

     # now, just leave __new__, __init__, __prepare__ , alone
     # and write your class methods as ordinary methods:
     def update(cls, *args, **kw):
          ...

     def fetch_rows_from(self, ...):
          ...

class Model(with_metaclass(ClsMethods)):
      # This really socks. Do you really still need Py2 support? :-) 

      ...

(It should be obvious, but perceive you don't need to declare the methods in the metaclass as classmethods: all of them are classmethods for the metaclass instance, which is the class)

And a quick demo at the console:

In [37]: class M(type):
    ...:     def secret(cls): print("At class only")
    ...:     

In [38]: class A(metaclass=M):
    ...:     pass
    ...: 

In [39]: A.secret()
At class only

In [40]: A().secret()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-40-06355f714f97> in <module>()
----> 1 A().secret()

AttributeError: 'A' object has no attribute 'secret'
Community
  • 1
  • 1
jsbueno
  • 99,910
  • 10
  • 151
  • 209
0

You can override __getattribute__ which is called for every attribute access and only for instances and inspect the stuff that is being returned for classmethodicity. Alternatively, you can just refuse a certain item:

import inspect

class A(object):  # aka Model
    @classmethod
    def f(cls, *args, **kwargs):
        print(args, kwargs)

class B(A):  # your Model subclass
    def __getattribute__(self, item):
        # if item == 'update':
        #     raise TypeError
        obj = super(B, self).__getattribute__(item)
        # classmethod check
        if inspect.ismethod(obj) and obj.__self__ is B:
            raise TypeError
        return obj

> a = A()
> b = B()

> A.f(5, p=7)
(5,) {'p': 7}

> B.f(5, p=7)
(5,) {'p': 7}

> a.f(5, p=7)
(5,) {'p': 7}

> b.f(5, p=7)
# TypeError

The classmethod check is taken from this answer by Martijn Pieters.

Community
  • 1
  • 1
user2390182
  • 72,016
  • 6
  • 67
  • 89