hollow can make functions lazy and return HollowObjects
This commit is contained in:
parent
0ced06bfc4
commit
becd68a14e
|
@ -1,7 +1,7 @@
|
|||
|
||||
[project]
|
||||
name = "jo3util"
|
||||
version = "0.0.19"
|
||||
version = "0.0.20"
|
||||
description = ""
|
||||
dependencies = []
|
||||
dynamic = ["readme"]
|
||||
|
|
146
src/hollow.py
Normal file
146
src/hollow.py
Normal file
|
@ -0,0 +1,146 @@
|
|||
from typing import TypeVar, Generic, Callable, Type, Union
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def HollowObject(init_fn: Union[T, Callable[..., T]], *args, **kwargs) -> T:
|
||||
""" Return an object that only gets initialized when any of its attributes are accessed.
|
||||
|
||||
:param init_fn: Function that initializes the real object.
|
||||
:param args: Positional arguments for the init_fn.
|
||||
:param kwargs: Keyword arguments for the init_fn.
|
||||
"""
|
||||
|
||||
# Case for types.
|
||||
if isinstance(init_fn, type):
|
||||
base_class = init_fn
|
||||
# Case for functions.
|
||||
elif hasattr(init_fn, "__annotations__") and "return" in init_fn.__annotations__:
|
||||
base_class = init_fn.__annotations__["return"]
|
||||
# Case for callable class instances.
|
||||
elif hasattr(init_fn.__call__, "__annotations__") and "return" in init_fn.__call__.__annotations__:
|
||||
base_class = init_fn.__call__.__annotations__["return"]
|
||||
# Case for unnannotated non-type callables.
|
||||
else:
|
||||
base_class = object
|
||||
|
||||
def initialize(hollow_object):
|
||||
real_obj = init_fn(*args, **kwargs)
|
||||
|
||||
try:
|
||||
# Change the type of `self` to match the real object
|
||||
hollow_object.__class__ = real_obj.__class__
|
||||
hollow_object.__dict__ = real_obj.__dict__ # Copy the real object's attributes
|
||||
return False
|
||||
|
||||
# For certain builtin immutable types, we can't change the __class__ or __dict__.
|
||||
# This means the instance will always stay a HollowObject subclass and the below
|
||||
# wrapper will keep firing for all calls.
|
||||
except TypeError:
|
||||
hollow_object._real_obj = real_obj
|
||||
return True
|
||||
|
||||
def wrap(fn):
|
||||
def inner(self, *args, **kwargs):
|
||||
if hasattr(self, "_real_obj") or initialize(self):
|
||||
return object.__getattribute__(self._real_obj, fn.__name__)(*args, **kwargs)
|
||||
else:
|
||||
return object.__getattribute__(self, fn.__name__)(*args, **kwargs)
|
||||
return inner
|
||||
|
||||
class HollowObject(base_class):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __getattribute__(self, name):
|
||||
# Avoid infinite recursions.
|
||||
if name in ["__dict__", "__class__", "_real_obj", "_get_call"]:
|
||||
return super().__getattribute__(name)
|
||||
|
||||
if hasattr(self, "_real_obj") or initialize(self):
|
||||
return getattr(self._real_obj, name)
|
||||
else:
|
||||
return getattr(self, name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
# Avoid infinite recursions.
|
||||
if name in ["__dict__", "__class__", "_real_obj", "_get_call"]:
|
||||
return super().__setattr__(name, value)
|
||||
|
||||
if hasattr(self, "_real_obj") or initialize(self):
|
||||
setattr(self._real_obj, name, value)
|
||||
else:
|
||||
setattr(self, name, value)
|
||||
|
||||
@staticmethod
|
||||
def _get_call():
|
||||
return init_fn, args, kwargs
|
||||
|
||||
@wrap
|
||||
def __str__(self):
|
||||
return ""
|
||||
|
||||
@wrap
|
||||
def __repr__(self):
|
||||
return ""
|
||||
|
||||
@wrap
|
||||
def __add__(self, _):
|
||||
pass
|
||||
|
||||
@wrap
|
||||
def __iter__(self, _):
|
||||
pass
|
||||
|
||||
# todo: add all the magic functions
|
||||
|
||||
return HollowObject()
|
||||
|
||||
|
||||
def lazy(fn):
|
||||
""" Replace fn with a lazy function.
|
||||
|
||||
A lazy function does not immediately fire, but instead returns an object
|
||||
that contains the function call. If anything of the object is accessed,
|
||||
it is created in-place and the accessed thing is returned."""
|
||||
def lazy_(*args, **kwargs):
|
||||
return HollowObject(fn, *args, **kwargs)
|
||||
return lazy_
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
# Example usage of an object with type hints
|
||||
class FullObject:
|
||||
def __init__(self, value: int):
|
||||
self.value = value
|
||||
print("initialized")
|
||||
|
||||
def __str__(self):
|
||||
return f"Value is {self.value}"
|
||||
|
||||
hollow = HollowObject(FullObject, value=42)
|
||||
print(type(hollow))
|
||||
# Linters will know that `hollow` behaves like `FullObject`.
|
||||
#
|
||||
# Only now does hollow get initialized.
|
||||
print(hollow)
|
||||
# The type of hollow is now FullObject, so no future calls will be made through the wrapper.
|
||||
|
||||
# Example usage of the lazy decorator.
|
||||
|
||||
print("\nstarting string tests")
|
||||
|
||||
@lazy
|
||||
def lazy_greeting(name) -> str:
|
||||
print("preparing to greet")
|
||||
return "hello " + name
|
||||
|
||||
message = lazy_greeting("john")
|
||||
print(type(message))
|
||||
print([char for char in message])
|
||||
# because message is a subclass of str, we can't change the class's type.
|
||||
# this means that all future calls on it, will be through the wrapper
|
||||
# and message stays an HollowObject type.
|
||||
|
18
src/store.py
18
src/store.py
|
@ -6,6 +6,8 @@ from pathlib import Path
|
|||
import pickle
|
||||
from typing import Tuple
|
||||
|
||||
from .hollow import HollowObject
|
||||
|
||||
|
||||
def with_filtered_args(fn):
|
||||
"""
|
||||
|
@ -58,13 +60,14 @@ class load_or_create:
|
|||
hash_len=8,
|
||||
save_args=False,
|
||||
save_json=False,
|
||||
plain_text=False
|
||||
plain_text=False,
|
||||
path=None,
|
||||
):
|
||||
self.load=with_filtered_args(load)[0]
|
||||
self.load_arg_names = {p.name for p in inspect.signature(load).parameters.values()}
|
||||
self.save=with_filtered_args(save)[0]
|
||||
self.save_arg_names = {p.name for p in inspect.signature(save).parameters.values()}
|
||||
self.path_fn=path_fn
|
||||
self.path_fn=path if path is not None else path_fn
|
||||
self.hash_len=hash_len
|
||||
self.save_args=save_args
|
||||
self.save_json=save_json
|
||||
|
@ -78,7 +81,6 @@ class load_or_create:
|
|||
def __call__(self, fn):
|
||||
return Loc(self, fn)
|
||||
|
||||
|
||||
def args_to_kwargs(self, args, kwargs, **extra):
|
||||
return extra | kwargs | {self.arg_names[i]: a for i, a in enumerate(args)}
|
||||
|
||||
|
@ -206,8 +208,11 @@ class load_or_create:
|
|||
return self.obj_to_args[hash]
|
||||
|
||||
def path_from_obj(self, obj, *args, **kwargs):
|
||||
if isinstance(obj, LoadOrCreateCFG):
|
||||
return self.path(*obj.args, **obj.kwargs)
|
||||
# If the object passed is a HollowObject, we can infer the args without
|
||||
# calling it.
|
||||
if hasattr(obj, "_get_call"):
|
||||
_, args, kwargs = obj._get_call()
|
||||
return self.path(*args, **kwargs)
|
||||
return self.args_from_obj(obj, *args, **kwargs)["path"]
|
||||
|
||||
def dir_from_obj(self, obj, *args, **kwargs):
|
||||
|
@ -219,7 +224,7 @@ class load_or_create:
|
|||
to get it's path, you can pass a LoadOrCreateCFG instead, saving you
|
||||
from loading or creating that object..
|
||||
"""
|
||||
return LoadOrCreateCFG(*args, **kwargs)
|
||||
return HollowObject(lambda x: x, *args, **kwargs)
|
||||
|
||||
|
||||
class Loc(load_or_create):
|
||||
|
@ -247,3 +252,4 @@ class Loc(load_or_create):
|
|||
|
||||
return obj
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user