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
|
except (KeyError, AttributeError): pass
|
||||||
else: self.import_per_app( names )
|
else: self.import_per_app( names )
|
||||||
|
|
||||||
|
self.load_extensions()
|
||||||
|
|
||||||
#Init the asyncron worker for this process
|
#Init the asyncron worker for this process
|
||||||
from .workers import AsyncronWorker
|
from .workers import AsyncronWorker
|
||||||
#The worker should not start working until they know we're responding to requests.
|
#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 )
|
#print( f"Loading {app.name}.{name}:", import_file )
|
||||||
loader = importlib.machinery.SourceFileLoader( f"{app.name}.{name}", str(import_file) )
|
loader = importlib.machinery.SourceFileLoader( f"{app.name}.{name}", str(import_file) )
|
||||||
loader.exec_module( types.ModuleType(loader.name) )
|
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 f
|
||||||
return decorator
|
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 )
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
2
setup.py
2
setup.py
|
|
@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='asyncron',
|
name='asyncron',
|
||||||
version='0.1.3',
|
version='0.1.4',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
#include_package_data=True, # Include static files from MANIFEST.in
|
#include_package_data=True, # Include static files from MANIFEST.in
|
||||||
install_requires=[
|
install_requires=[
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user