snakeoil.obj module

Object trickery including delayed instantiation and proxying

Note that the delayed instantiation/proxying that is in use here goes several steps beyond your average proxy implementation- this functionality will take every step possible to make the proxy appear as if there was _no_ proxy at all.

Specifically this functionality is aware of cpython VM/interpreter semantics- cpython doesn’t use __getattribute__ to pull certain methods (__str__ is an example most people are aware of of, __call__, __getitem__, etc are ones most aren’t). Delayed instantiation is significantly complicated when these methods aren’t properly handled- you wind up having to pay very close attention to exactly where those instances are passed to, what access them, do they trigger any of the slotted special methods, etc. There is a catch to this however- you can’t just define all slotted methods since if the proxy has those slotted methods, it will use them and you can’t just throw TypeErrors- the execution path has differeed.

Part of the purpose of snakeoil is to make things that should just work, work- this implementation exists to do just that via removing those concerns. The proxying knows what the resultant class will be and uses a custom proxy that will appear the same to the python machinery for slotted methods- literally the python VM won’t try to __iadd__ the proxy unless the resultant class would’ve supported that functionality, it will consider it callable if the resultant class would be, etc.

By and large, this proxying is transparent if dealing in python objects (newstyle or old)- for example,

>>> from snakeoil.obj import DelayedInstantiation_kls
>>> class foo:
...   def __init__(self, value):
...     print("instance was created")
...     self.attribute = value
...   pass
>>> delayed = DelayedInstantiation_kls(foo, "bar")
>>> print(isinstance(DelayedInstantiation_kls(foo), foo))
True
>>> print(delayed.attribute)
instance was created
bar

This proxying however cannot cover up certain cpython internal issues- specifically builtins.

>>> from snakeoil.obj import DelayedInstantiation
>>> delayed_tuple = DelayedInstantiation(tuple, lambda x: tuple(x), range(5))
>>> print(delayed_tuple + (5, 6, 7))
(0, 1, 2, 3, 4, 5, 6, 7)
>>> print((5, 6, 7) + delayed_tuple)
Traceback (most recent call last):
TypeError: can only concatenate tuple (not "CustomDelayedObject") to tuple
>>> # the reason this differs comes down to the cpython vm translating the previous
>>> # call into essentially the following-
>>> print((5, 6, 7).__add__(delayed_tuple))
Traceback (most recent call last):
TypeError: can only concatenate tuple (not "CustomDelayedObject") to tuple

Simply put, while we can emulate/proxy at the python VM layer appearing like the target, at the c level (which is where tuples are implemented) they expect a certain object structure, and reach in access it directly in certain cases. This cannot be safely proxied (nor realistically can it be proxied in general without extremely horrible thunking tricks), so it is not attempted.

Essentially, if DelayedInstantiation’s/proxies are transparent when dealing in native python objects- you will not see issues and you don’t have to care where those objects are used, passed to, etc. If you’re trying to proxy a builtin, it’s possible, but you do need to keep an eye on where that instance is passed to since it’s not fully transparent.

As demonstrated above, if you’re trying to proxy a builtin object, the consuming code will have to order its operations appropriately- prefering the proxy’s methods over builtin methods (essentially have the proxy on the left for general ops).

If that doesn’t make sense to the reader, it’s probably best that the reader not try to proxy builtin objects like tuples, lists, dicts, sets, etc.

snakeoil.obj.DelayedInstantiation(resultant_kls, func, *a, **kwd)[source]

Generate an objects that does not get initialized before it is used.

The returned object can be passed around without triggering initialization. The first time it is actually used (an attribute is accessed) it is initialized once.

The returned “fake” object cannot completely reliably mimic a builtin type. It will usually work but some corner cases may fail in confusing ways. Make sure to test if DelayedInstantiation has no unwanted side effects.

Parameters:
  • resultant_kls – type object to fake an instance of.

  • func – callable, the return value is used as initialized object.

All other positional args and keywords are passed to func during instantiation.

snakeoil.obj.DelayedInstantiation_kls(kls, *a, **kwd)[source]

Wrapper for DelayedInstantiation

This just invokes DelayedInstantiation(kls, kls *a, **kwd)

See DelayedInstantiation() for argument specifics.

snakeoil.obj.make_kls(kls, proxy_base=<class 'snakeoil.obj.BaseDelayedObject'>)[source]
snakeoil.obj.popattr(obj, name, default=<object object>)[source]

Remove and return an attribute from an object if it exists.