hollow can make functions lazy and return HollowObjects

This commit is contained in:
JJJHolscher 2024-11-14 18:15:37 +01:00
parent 0ced06bfc4
commit becd68a14e
3 changed files with 160 additions and 8 deletions

View File

@ -1,7 +1,7 @@
[project] [project]
name = "jo3util" name = "jo3util"
version = "0.0.19" version = "0.0.20"
description = "" description = ""
dependencies = [] dependencies = []
dynamic = ["readme"] dynamic = ["readme"]

146
src/hollow.py Normal file
View 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.

View File

@ -6,6 +6,8 @@ from pathlib import Path
import pickle import pickle
from typing import Tuple from typing import Tuple
from .hollow import HollowObject
def with_filtered_args(fn): def with_filtered_args(fn):
""" """
@ -58,13 +60,14 @@ class load_or_create:
hash_len=8, hash_len=8,
save_args=False, save_args=False,
save_json=False, save_json=False,
plain_text=False plain_text=False,
path=None,
): ):
self.load=with_filtered_args(load)[0] self.load=with_filtered_args(load)[0]
self.load_arg_names = {p.name for p in inspect.signature(load).parameters.values()} self.load_arg_names = {p.name for p in inspect.signature(load).parameters.values()}
self.save=with_filtered_args(save)[0] self.save=with_filtered_args(save)[0]
self.save_arg_names = {p.name for p in inspect.signature(save).parameters.values()} 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.hash_len=hash_len
self.save_args=save_args self.save_args=save_args
self.save_json=save_json self.save_json=save_json
@ -78,7 +81,6 @@ class load_or_create:
def __call__(self, fn): def __call__(self, fn):
return Loc(self, fn) return Loc(self, fn)
def args_to_kwargs(self, args, kwargs, **extra): def args_to_kwargs(self, args, kwargs, **extra):
return extra | kwargs | {self.arg_names[i]: a for i, a in enumerate(args)} 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] return self.obj_to_args[hash]
def path_from_obj(self, obj, *args, **kwargs): def path_from_obj(self, obj, *args, **kwargs):
if isinstance(obj, LoadOrCreateCFG): # If the object passed is a HollowObject, we can infer the args without
return self.path(*obj.args, **obj.kwargs) # 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"] return self.args_from_obj(obj, *args, **kwargs)["path"]
def dir_from_obj(self, obj, *args, **kwargs): 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 to get it's path, you can pass a LoadOrCreateCFG instead, saving you
from loading or creating that object.. from loading or creating that object..
""" """
return LoadOrCreateCFG(*args, **kwargs) return HollowObject(lambda x: x, *args, **kwargs)
class Loc(load_or_create): class Loc(load_or_create):
@ -247,3 +252,4 @@ class Loc(load_or_create):
return obj return obj