def test_di_methods(): """ Test how DI works with classes """ # A dependency to provide @di.signature() def current_user(): return User(email='*****@*****.**') # A class that uses it @di.kwargs(current_user=current_user) class Actions: def __init__(self, current_user: User): self.user = current_user @di.kwargs() def method(self): return self.user.email # Test with di.Injector() as root: root.provide(current_user, current_user) # Try to use a class actions = root.invoke(Actions) ret = root.invoke(actions.method) assert ret == '*****@*****.**'
def test_di_cleanup_when_everything_fails(): """ Test how cleanup behaves when there are multiple failures """ cleaned_up = [] root = di.Injector() # Init 5 providers # Get 5 instances for n in range(5): @di.depends() @contextmanager def failing_provider(n=n): try: yield n finally: cleaned_up.append(n) raise AssertionError root.provide(n, failing_provider) assert root.get(n) == n # Now we have 5 clean-ups waiting to be executed # Quit. They will all fail, but report their numbers into `cleaned_up` with pytest.raises(AssertionError): root.close() # See that all 5 clean-ups have had a chance to run. assert cleaned_up == [4, 3, 2, 1, 0]
def test_complex_dependencies(): """ Test low-level: nested dependencies """ root = di.Injector() # "Z" depends on "A" and "B' root.register_provider( di.Provider( 'Z', lambda a, b: f'a={a},b={b}', deps_kw={ 'a': di.Dependency('A'), 'b': di.Dependency('B') }, )) # "A" depends on "C" root.register_provider( di.Provider('A', lambda c: f'one before {c}', deps_kw={'c': di.Dependency('C')})) # "B" depends on "D" root.register_provider( di.Provider('B', lambda d: f'one before {d}', deps_kw={'d': di.Dependency('D')})) # "C", "D" are regular root.register_provider(di.Provider('C', lambda: 'cee')) root.register_provider(di.Provider('D', lambda: 'dee')) # The whole tree is resolved assert root.get('Z') == 'a=one before cee,b=one before dee' # Done root.close()
def test_di_cleanup(): """ Test how cleanup works """ # Only one connection is available. The Injector should be able to reuse it connection_pool = ['connection-1'] @di.signature() @contextmanager def get_connection(): try: # Get a connection connection = connection_pool.pop() yield connection finally: # Clean-up connection_pool.append(connection) @di.kwargs(ssn='connection') def save_to_db(ssn): pass with di.Injector() as root: # I'm lazy. Let's use a custom token root.provide('connection', get_connection) root.get('connection') root.get('connection') root.get('connection') root.get('connection') # The only connection is in use assert len(connection_pool) == 0 # Clean-up returned it to the pool assert len(connection_pool) == 1
def main(): # Try out @operation-decorated business logic with an Injector DI and class-based CRUD with di.Injector() as root: root.provide(authenticated_user, authenticated_user) # Call some operations assert call_operation(root, 'whoami') == 'kolypto' assert call_operation(root, 'user/list') == [] assert call_operation(root, 'user/create', login='******') == None assert call_operation(root, 'user/list') == [User(login='******', created_by=User(login='******'))] assert call_operation(root, 'user/delete', index=0) == None assert call_operation(root, 'user/list') == []
def test_di_overrides(): """ Test how overrides work with DI """ # NOTE: it does not! @di.kwargs(user='******') def authenticated_user_email(user: Optional[User]): if user is None: return None else: return user.email with di.Injector() as request: request.provide_value('authenticated_user', User(email='*****@*****.**')) request.provide('email', authenticated_user_email) # User authenticated ok assert request.get('email') == '*****@*****.**' # Now relogin as another user with di.Injector(parent=request) as anonymized: anonymized.provide_value('authenticated_user', User(email='anonymous@localhost')) # The higher-level injector still sees the old value assert request.get('email') == '*****@*****.**' # NOTE: at this level, we don't see any changes. # Currently, this is the expected behavior: you can't override higher-level things from down below. # assert anonymized.get('email') == 'anonymous@localhost' # not implemented assert anonymized.get( 'email') == '*****@*****.**' # ❗ not overridden! # And it's immediately lost when the `anonymized` context quits assert request.get('email') == '*****@*****.**'
def __init__(self, *, debug: bool = False, fully_documented: bool = True, **kwargs): """ Args: debug: Debug mode; will make extra consistency and quality checks fully_documented: Ensure that every function, parameter, and result, has a description and annotation. This parameter is dearly loved by documentation freaks, but is probably a curse to everyone else ... :) **kwargs: Keyword arguments for the APIRouter, if you for some reason want to tune it to your taste """ super().__init__(**kwargs) # Debug mode self.debug: bool = debug # Do we want all operations fully documented? self.fully_documented: bool = fully_documented # Init self.injector = di.Injector() self.func_operations: List[Callable] = [] self.class_operations: List[type] = []
def test_injector_low_level_api(): """ Test low level: Injector.add_provider() """ # Use the following scenario: # "root" will be an application-wide injector with global stuff. # When a user connects via WebSockets, they are authenticated and get a connection-level injector. # When a user calls a function, a request-level injector is created. # Root injector. # Application-wide with di.Injector() as root: root.register_provider( di.Provider( token=Application, func=lambda: Application(title='App'), )) # A connected session injector. # For every connected user. Imagine WebSockets with di.Injector(parent=root) as connection: connection.register_provider( di.Provider( token=User, func=lambda: User(email='*****@*****.**'), )) # A request injector. # For every command the user sends, an injector is created to keep the context with di.Injector(parent=connection) as request: request.register_provider( di.Provider( token=DatabaseSession, func=session_maker, deps_kw={ # Function argument 'connection': di.Dependency(token=Connection) }, )) request.register_provider( di.Provider(token=Connection, func=lambda: Connection(url='localhost'))) # Get Application: root injector app = request.get(Application) assert isinstance(app, Application) assert app.title == 'App' # Get Application again; get the very same instance assert request.get(Application) is app # Get User: request injector (parent) user = request.get(User) assert isinstance(user, User) and user.email == '*****@*****.**' # Get DB session # Local injector + dependencies ssn = request.get(DatabaseSession) assert isinstance(ssn, DatabaseSession) assert isinstance( ssn.connection, Connection ) and ssn.connection.url == 'localhost' # got its dependency assert ssn.closed == False # Request quit. Make sure context managers cleaned up assert ssn.closed == True # magic # Cannot reuse the same injector because it's been closed with pytest.raises(di.exc.ClosedInjectorError): with request: pass # Copy its providers def new_request(): return copy(request) # ### Test: InjectFlags.SELF with new_request() as request: # Can get local things assert request.has(DatabaseSession, di.SELF) == True assert request.get(DatabaseSession, di.SELF) assert request.get(DatabaseSession, di.SELF) is not ssn # a different object! # Cannot go upwards assert request.has(Application, di.SELF) == False with pytest.raises(di.NoProviderError): request.get(Application, di.SELF) # Optional works though assert request.has(Application, di.SELF | di.OPTIONAL) == False assert request.get(Application, di.SELF | di.OPTIONAL, default='Z') == 'Z' # ### Test: InjectFlags.SKIP_SELF with new_request() as request: # Can find things on parents assert request.has(Application, di.SKIP_SELF) == True assert request.get(Application, di.SKIP_SELF) # Can't find things on self assert request.has(DatabaseSession, di.SKIP_SELF) == False with pytest.raises(di.NoProviderError): request.get(DatabaseSession, di.SKIP_SELF) # ### Test: InjectFlags.OPTIONAL with new_request() as request: assert request.get('NONEXISTENT', default=123) == 123 # no error
def test_di_functions(): """ Test DI with functions: automatic dependency reading from a function's signature """ # Prepare some resolvables @di.signature(exclude=['debug'] ) # exclude an argument from DI consideration def init_application(debug: bool = False) -> Application: """ Provider: Application """ return Application(title='App') @di.signature() def authenticate_user(app: Application) -> User: """ Provider: User """ assert app.title == 'App' return User(email='*****@*****.**') @di.signature() def db_connect(app: Application) -> Connection: """ Provider: DB connection """ assert app.title == 'App' return Connection(url='localhost') @di.signature('connection') # pick one argument @contextmanager def db_session(connection: Connection, connect: bool = True) -> DatabaseSession: """ Provider: DB session """ connection = DatabaseSession(connection=connection, closed=False) try: yield connection finally: connection.closed = True @di.signature() def authenticated(user: User): """ Provider: anonymous requirement to be signed in (guard) """ if user.email != '*****@*****.**': raise Unauthenticated('Unauthenticated') # Prepare an injector with di.Injector() as root: root.provide(Application, init_application) root.provide(Connection, db_connect) root.provide(DatabaseSession, db_session) # Authenticate with di.Injector(parent=root) as client: client.provide(User, authenticate_user) client.provide(authenticated, authenticated) # Run a function @di.kwargs(app=Application, ssn=DatabaseSession) # explicit kwargs @di.depends(authenticated) # double-depends def hello_app( greeting: str, app, ssn ): # `greeting` is not provided; it's a required argument """ The function to invoke """ if not ssn.closed: return f'{greeting} {app.title}' ret = client.invoke(hello_app, greeting='hello') assert ret == 'hello App' # Authenticate as another user with di.Injector(parent=root) as client: root.provide_value(User, User(email='anonymous')) client.provide(authenticated, authenticated) # See that authenticated() reports an error with pytest.raises(Unauthenticated): client.invoke(authenticated) # See that user_func() fails with it with pytest.raises(Unauthenticated): client.invoke(hello_app, greeting='hello')