Added extension support

This commit is contained in:
Oracle 2024-10-25 20:25:13 +02:00
parent 68a5cc7d30
commit 0ebca05144
6 changed files with 234 additions and 4 deletions

View File

@ -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'
]

View File

@ -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
View 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
View 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)

View File

@ -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 )

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name='asyncron',
version='0.1.3',
version='0.1.4',
packages=find_packages(),
#include_package_data=True, # Include static files from MANIFEST.in
install_requires=[