Added extension support
This commit is contained in:
parent
68a5cc7d30
commit
0ebca05144
|
|
@ -1,3 +1,7 @@
|
|||
from .shortcuts import task, run_on_model_change
|
||||
from .shortcuts import task, run_on_model_change, extension
|
||||
|
||||
__all__ = ['task', 'run_on_model_change']
|
||||
__all__ = [
|
||||
'task',
|
||||
'run_on_model_change',
|
||||
'extension'
|
||||
]
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ class AsyncronConfig(AppConfig):
|
|||
except (KeyError, AttributeError): pass
|
||||
else: self.import_per_app( names )
|
||||
|
||||
self.load_extensions()
|
||||
|
||||
#Init the asyncron worker for this process
|
||||
from .workers import AsyncronWorker
|
||||
#The worker should not start working until they know we're responding to requests.
|
||||
|
|
@ -34,3 +36,39 @@ class AsyncronConfig(AppConfig):
|
|||
#print( f"Loading {app.name}.{name}:", import_file )
|
||||
loader = importlib.machinery.SourceFileLoader( f"{app.name}.{name}", str(import_file) )
|
||||
loader.exec_module( types.ModuleType(loader.name) )
|
||||
|
||||
|
||||
def load_extensions( self ):
|
||||
from .base.models import BaseModel
|
||||
from .extender import Extender
|
||||
for app in apps.get_app_configs():
|
||||
app_dir = pathlib.Path(app.path)
|
||||
for model in app.get_models():
|
||||
if not issubclass( model, BaseModel ): continue
|
||||
|
||||
ext_dir = app_dir / "extensions" / model.__name__
|
||||
if not ext_dir.exists(): continue
|
||||
|
||||
extender = None
|
||||
for import_file in ext_dir.iterdir():
|
||||
if not import_file.is_file(): continue
|
||||
if import_file.suffixes[-1] != ".py": continue
|
||||
|
||||
#So the imported module can attach it's method to the extender created last
|
||||
if extender is None: extender = Extender( model )
|
||||
|
||||
loader = importlib.machinery.SourceFileLoader( f"{app.name}.extensions.{model.__name__}.{import_file.stem}", str(import_file) )
|
||||
loader.exec_module( types.ModuleType(loader.name) )
|
||||
|
||||
if extender:
|
||||
extender.attach( model )
|
||||
|
||||
Extender.stop_capturing()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#
|
||||
|
|
|
|||
48
asyncron/extender.py
Normal file
48
asyncron/extender.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import collections
|
||||
import functools
|
||||
|
||||
class Extender:
|
||||
capturing_instance = None
|
||||
|
||||
@classmethod
|
||||
def stop_capturing( cls ):
|
||||
cls.capturing_instance = None
|
||||
|
||||
def __init__( self, model ):
|
||||
self.__class__.capturing_instance = self
|
||||
self.model = model
|
||||
self.extensions = collections.defaultdict( list ) # method_name -> list of tuple(args, kwargs)
|
||||
|
||||
def __call__( self, *checks, **filters ):
|
||||
def decorator( f ):
|
||||
self.extensions[ f.__name__ ].append( (f, checks, filters) )
|
||||
return f
|
||||
return decorator
|
||||
|
||||
def attach( extender, cls ):
|
||||
#Attach the miss catch attr method
|
||||
def __getattr__( self, extended_name ):
|
||||
if extended_name not in extender.extensions:
|
||||
try: super_getattr = super(self.__class__).__getattr__
|
||||
except AttributeError: raise AttributeError(f"{cls} Model has no extension '{extended_name}'.")
|
||||
else: return super_getattr( self, method )
|
||||
|
||||
method_candidates = extender.extensions[extended_name]
|
||||
def run_matching_candidate( *args, **kwargs ):
|
||||
for f, checks, filters in method_candidates:
|
||||
|
||||
check_failed = False
|
||||
for check in checks:
|
||||
if not check( self ):
|
||||
check_failed = True
|
||||
break
|
||||
if check_failed: continue
|
||||
|
||||
if any( getattr(self, k) != v for k, v in filters.items() ):
|
||||
continue
|
||||
|
||||
return f( self, *args, **kwargs )
|
||||
raise AttributeError(f"{self} Did not match any extensions for '{extended_name}'.")
|
||||
return run_matching_candidate
|
||||
|
||||
cls.__getattr__ = __getattr__
|
||||
137
asyncron/extender_draft.py
Normal file
137
asyncron/extender_draft.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
"""
|
||||
# This is a failed draft of extender,
|
||||
# I was trying to make the exact function attached as an extension, not evaluate until the first
|
||||
# time the function is about to enter an async context, so we can potentially hit the db to figure out which functions
|
||||
# from the 'method_candidates' list should be given the arguments provided,
|
||||
# But this proved challenging to get working with all types of async initiation (async for, async with, await)
|
||||
# I even have a hunch this is something that might later be made easier/possible from python's side
|
||||
#
|
||||
# Main Issue I faced:
|
||||
# asyncio.run explicitly checks if an object is a coroutine:
|
||||
# python3.12/asyncio/runners.py line 89 & 90:
|
||||
# if not coroutines.iscoroutine(coro):
|
||||
# raise ValueError("a coroutine was expected, got {!r}".format(coro))
|
||||
#
|
||||
# So I can't have a superposition of "coroutine / async context / async iterable" until it's collapsed the moment it's used!
|
||||
# I have to choose one. which would limit (arguably very little in practice) the functionality of
|
||||
# extensions in a way that would force the same names for extensions to be different async types.
|
||||
#
|
||||
# I know that's almost never an issue, but the code smell from doing this will drive me crazy, so this is gonna stay like this for now.
|
||||
"""
|
||||
|
||||
import collections
|
||||
import functools
|
||||
|
||||
class Extender:
|
||||
capturing_instance = None
|
||||
|
||||
@classmethod
|
||||
def stop_capturing( cls ):
|
||||
cls.capturing_instance = None
|
||||
|
||||
def __init__( self, model ):
|
||||
self.__class__.capturing_instance = self
|
||||
self.model = model
|
||||
self.extensions = collections.defaultdict( list ) # method_name -> list of tuple(args, kwargs)
|
||||
|
||||
def __call__( self, *checks, **filters ):
|
||||
def decorator( f ):
|
||||
self.extensions[ f.__name__ ].append( (f, checks, filters) )
|
||||
return f
|
||||
return decorator
|
||||
|
||||
def attach( extender, cls ):
|
||||
#Attach the miss catch attr method
|
||||
def __getattr__( self, extended_name ):
|
||||
if extended_name not in extender.extensions:
|
||||
try: super_getattr = super(self.__class__).__getattr__
|
||||
except AttributeError: raise AttributeError(f"{cls} Model has no extension '{extended_name}'.")
|
||||
else: return super_getattr( self, method )
|
||||
|
||||
method_candidates = extender.extensions[extended_name]
|
||||
|
||||
wrapper = AsyncWrapper( self, method_candidates )
|
||||
return wrapper.await_arguments
|
||||
|
||||
cls.__getattr__ = __getattr__
|
||||
|
||||
|
||||
|
||||
import asyncio
|
||||
import collections.abc# -> collections.abc.Coroutine
|
||||
|
||||
class AsyncWrapper( collections.abc.Coroutine ):
|
||||
def __init__(self, model_instance, wrapper_candidates):
|
||||
self._wrapper_model_instance = model_instance
|
||||
self._wrapper_candidates = wrapper_candidates
|
||||
self._wrapper_obj = None
|
||||
self._wrapper_args = None
|
||||
self._wrapper_kwargs = None
|
||||
self._wrapper_init_lock = asyncio.Lock() # Ensure init happens only once in case of concurrent access
|
||||
|
||||
# Asynchronous initializer
|
||||
async def _wrapper_init( self ):
|
||||
if not self._wrapper_obj:
|
||||
async with self._wrapper_init_lock:
|
||||
if not self._wrapper_obj: # Double-check after acquiring the lock
|
||||
await self._wrapper_find_matching_candidate()
|
||||
|
||||
|
||||
# Example async initialization logic
|
||||
async def _wrapper_find_matching_candidate( self ):
|
||||
for f, checks, filters in self._wrapper_candidates:
|
||||
|
||||
check_failed = False
|
||||
for check in checks:
|
||||
if not ( await check( self._wrapper_model_instance ) ):
|
||||
check_failed = True
|
||||
break
|
||||
if check_failed: continue
|
||||
|
||||
|
||||
if any( getattr(self._wrapper_model_instance, k) != v for k, v in filters.items() ):
|
||||
continue
|
||||
|
||||
self._wrapper_obj = f
|
||||
return
|
||||
|
||||
raise AttributeError(f"{self._wrapper_model_instance} Did not match any extensions for '{extended_name}'.")
|
||||
|
||||
def await_arguments( self, *args, **kwargs ):
|
||||
self._wrapper_args = args
|
||||
self._wrapper_kwargs = kwargs
|
||||
return self
|
||||
|
||||
def send( self, thing): pass
|
||||
def throw( self, thing ): pass
|
||||
|
||||
# Await method
|
||||
def __await__( self ):
|
||||
print("R: __await__")
|
||||
async def wrapper():
|
||||
print("R: __await__ wrapper")
|
||||
await self._wrapper_init()
|
||||
return await self._wrapper_obj( self._wrapper_model_instance, *self._wrapper_args, **self._wrapper_kwargs )
|
||||
return wrapper().__await__()
|
||||
|
||||
# Async iterator
|
||||
async def __aiter__( self ):
|
||||
print("R: __aiter__")
|
||||
await self._wrapper_init()
|
||||
return self._wrapper_obj.__aiter__()
|
||||
|
||||
async def __anext__( self ):
|
||||
print("R: __anext__")
|
||||
await self._wrapper_init()
|
||||
return await self._wrapper_obj.__anext__()
|
||||
|
||||
# Async context manager
|
||||
async def __aenter__( self ):
|
||||
print("R: __aenter__")
|
||||
await self._wrapper_init()
|
||||
return await self._wrapper_obj.__aenter__()
|
||||
|
||||
async def __aexit__( self, exc_type, exc, tb ):
|
||||
print("R: __aexit__")
|
||||
await self._wrapper_init()
|
||||
return await self._wrapper_obj.__aexit__(exc_type, exc, tb)
|
||||
|
|
@ -57,7 +57,10 @@ def run_on_model_change( *models ):
|
|||
return f
|
||||
return decorator
|
||||
|
||||
|
||||
from .extender import Extender
|
||||
def extension( *args, **kwargs ):
|
||||
assert Extender.capturing_instance, "Cannot only extend a model during inital import and after apps are ready!"
|
||||
return Extender.capturing_instance( *args, **kwargs )
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user