async def test_resources(): mailer = SMTPMailer(ssl='contextresource') context = Context() sslcontext = ssl.create_default_context() context.publish_resource(sslcontext, 'contextresource') await mailer.start(context) assert mailer.ssl is sslcontext
async def test_resources(): mailer = SMTPMailer(tls_context='contextresource') context = Context() sslcontext = ssl.create_default_context() context.add_resource(sslcontext, 'contextresource') await mailer.start(context) assert mailer.tls_context is sslcontext
async def test_get_resources_include_parents(self, context): subcontext = Context(context) resource1 = context.publish_resource(6, 'int1') resource2 = subcontext.publish_resource(8, 'int2') resource3 = context.publish_resource('foo', 'str') assert subcontext.get_resources() == [resource1, resource2, resource3] assert subcontext.get_resources(include_parents=False) == [resource2]
async def test_multiple_renderers(): ctx = Context() ctx.testvar = 'åäö' component = TemplatingComponent({ 'jinja2': {'package_name': 'tests'}, 'mako': {'package_paths': ['tests/templates']} }) await component.start(ctx) assert isinstance(ctx.jinja2, TemplateRendererProxy) assert isinstance(ctx.mako, TemplateRendererProxy)
async def test_request_resource_parent_add(self, context, event_loop): """ Test that publishing a resource to the parent context will satisfy a resource request in a child context. """ child_context = Context(context) task = event_loop.create_task(child_context.request_resource(int)) context.publish_resource(6) resource = await task assert resource == 6
async def test_single_renderer(): ctx = Context() ctx.testvar = 'åäö' component = TemplatingComponent(backend='jinja2', package_name='tests') await component.start(ctx) assert isinstance(ctx.jinja2, TemplateRendererProxy) assert type(ctx.jinja2.environment).__name__ == 'Environment' assert ctx.jinja2.render('jinja2_context.html') == """\ <div> This is a sample Test variable: åäö </div>""" assert ctx.jinja2.render_string('This is testvar: {{ ctx.testvar }}') == 'This is testvar: åäö'
async def start(self, ctx: Context): ctx.add_teardown_callback(self.teardown_callback, pass_exception=True) if self.method == 'stop': ctx.loop.call_later(0.1, ctx.loop.stop) elif self.method == 'exit': ctx.loop.call_later(0.1, sys.exit) elif self.method == 'keyboard': ctx.loop.call_later(0.1, self.press_ctrl_c) elif self.method == 'sigterm': ctx.loop.call_later(0.1, sigterm_handler, logging.getLogger(__name__), ctx.loop) elif self.method == 'exception': raise RuntimeError('this should crash the application') elif self.method == 'timeout': await asyncio.sleep(1)
async def test_finish_commit(raise_exception, tmp_path): """ Tests that the session is automatically committed if and only if the context was not exited with an exception. """ db_path = tmp_path / "test.db" engine = create_engine(f"sqlite:///{db_path}", poolclass=NullPool) with engine.begin() as connection: connection.execute(text("CREATE TABLE foo (id INTEGER PRIMARY KEY)")) component = SQLAlchemyComponent(url={ "drivername": "sqlite", "database": str(db_path) }, ) with ExitStack() as stack: async with Context() as ctx: await component.start(ctx) session = ctx.require_resource(Session) session.execute(text("INSERT INTO foo (id) VALUES(3)")) if raise_exception: stack.enter_context(pytest.raises(Exception, match="dummy")) raise Exception("dummy") rows = connection.execute(text("SELECT * FROM foo")).fetchall() assert len(rows) == (0 if raise_exception else 1)
async def test_component_multiple(caplog): caplog.set_level(logging.INFO, logger='asphalt.mailer.component') component = MailerComponent( mailers={ 'smtp': { 'backend': 'smtp', 'context_attr': 'mailer1' }, 'sendmail': { 'backend': 'sendmail', 'context_attr': 'mailer2' } }) async with Context() as ctx: await component.start(ctx) assert isinstance(ctx.mailer1, Mailer) assert isinstance(ctx.mailer2, Mailer) records = [ record for record in caplog.records if record.name == 'asphalt.mailer.component' ] records.sort(key=lambda r: r.message) assert len(records) == 4 assert records[0].message.startswith( "Configured mailer (sendmail / ctx.mailer2; class=SendmailMailer)") assert records[1].message.startswith( "Configured mailer (smtp / ctx.mailer1; class=SMTPMailer)") assert records[2].message.startswith('Mailer (sendmail) stopped') assert records[3].message.startswith('Mailer (smtp) stopped')
def test_get_parent_attribute(self, context): """ Test that accessing a nonexistent attribute on a context retrieves the value from parent. """ child_context = Context(context) context.a = 2 assert child_context.a == 2
def mailer(event_loop, unused_tcp_port, client_tls_context, smtp_server, tls): mailer = SMTPMailer(port=unused_tcp_port, timeout=1, tls=tls, tls_context=client_tls_context) with Context() as ctx: event_loop.run_until_complete(mailer.start(ctx)) yield mailer
async def test_default_config(): component = SerializationComponent(backend='json') async with Context() as ctx: await component.start(ctx) resource = ctx.require_resource(Serializer) assert isinstance(resource, JSONSerializer) assert ctx.json is resource
def test_run_return_5(self, event_loop): class DummyCLIComponent(CLIApplicationComponent): async def run(self, ctx: Context): return 5 component = DummyCLIComponent() event_loop.run_until_complete(component.start(Context())) exc = pytest.raises(SystemExit, event_loop.run_forever) assert exc.value.code == 5
async def test_multiple_engines(): component = SQLAlchemyComponent(engines={'db1': {}, 'db2': {}}, url='sqlite:///:memory:') async with Context() as ctx: await component.start(ctx) engine1 = ctx.require_resource(Engine, 'db1') engine2 = ctx.require_resource(Engine, 'db2') assert ctx.db1.bind is engine1 assert ctx.db2.bind is engine2
async def test_bind(): """Test that a Connection can be passed as "bind" in place of "url".""" engine = create_engine('sqlite:///:memory:') connection = engine.connect() component = SQLAlchemyComponent(bind=connection) async with Context() as ctx: await component.start(ctx) assert ctx.require_resource(Engine) is engine assert ctx.sql.bind is connection
async def test_component_start_async(): """Test that the component creates all the expected (asynchronous) resources.""" url = URL.create("sqlite+aiosqlite", database=":memory:") component = SQLAlchemyComponent(url=url) async with Context() as ctx: await component.start(ctx) ctx.require_resource(AsyncEngine) ctx.require_resource(sessionmaker) ctx.require_resource(AsyncSession)
async def test_component_start(poolclass): """Test that the component creates all the expected resources.""" component = SQLAlchemyComponent(url='sqlite:///:memory:', poolclass=poolclass) async with Context() as ctx: await component.start(ctx) engine = ctx.require_resource(Engine) ctx.require_resource(sessionmaker) assert ctx.sql is ctx.require_resource(Session) assert ctx.sql.bind is engine
async def test_memory_leak(): """Test that creating a session in a context does not leak memory.""" component = SQLAlchemyComponent(url='sqlite:///:memory:') async with Context() as ctx: await component.start(ctx) assert isinstance(ctx.sql, Session) del ctx gc.collect() # needed on PyPy assert next((x for x in gc.get_objects() if isinstance(x, Context)), None) is None
async def test_finish_commit(raise_exception, executor, commit_executor, tmpdir): """ Tests that the session is automatically committed if and only if the context was not exited with an exception. """ db_path = tmpdir.join('test.db') session = dict(commit_executor=executor if commit_executor == 'instance' else commit_executor) component = SQLAlchemyComponent(url={'drivername': 'sqlite', 'database': str(db_path)}, session=session) ctx = Context() ctx.publish_resource(executor, types=[Executor]) await component.start(ctx) ctx.dbsession.execute('CREATE TABLE foo (id INTEGER PRIMARY KEY)') ctx.dbsession.execute('INSERT INTO foo (id) VALUES(3)') await ctx.finished.dispatch(Exception('dummy') if raise_exception else None, return_future=True) rows = ctx.sql.execute('SELECT * FROM foo').fetchall() assert len(rows) == (0 if raise_exception else 1)
async def test_bind_async(): """Test that a Connection can be passed as "bind" in place of "url".""" engine = create_async_engine("sqlite+aiosqlite:///:memory:") connection = await engine.connect() component = SQLAlchemyComponent(bind=connection) async with Context() as ctx: await component.start(ctx) assert ctx.require_resource(AsyncEngine) is engine assert ctx.require_resource(AsyncSession).bind is connection await connection.close()
async def test_finish_commit(raise_exception, executor, commit_executor, tmpdir): """ Tests that the session is automatically committed if and only if the context was not exited with an exception. """ db_path = tmpdir.join('test.db') engine = create_engine('sqlite:///%s' % db_path, poolclass=NullPool) engine.execute('CREATE TABLE foo (id INTEGER PRIMARY KEY)') component = SQLAlchemyComponent( url={'drivername': 'sqlite', 'database': str(db_path)}, commit_executor=executor if commit_executor == 'instance' else commit_executor) ctx = Context() ctx.add_resource(executor, types=[Executor]) await component.start(ctx) ctx.sql.execute('INSERT INTO foo (id) VALUES(3)') await ctx.close(Exception('dummy') if raise_exception else None) rows = engine.execute('SELECT * FROM foo').fetchall() assert len(rows) == (0 if raise_exception else 1)
def test_run_return_invalid_type(self, event_loop): class DummyCLIComponent(CLIApplicationComponent): async def run(self, ctx: Context): return 'foo' component = DummyCLIComponent() event_loop.run_until_complete(component.start(Context())) with pytest.warns(UserWarning) as record: exc = pytest.raises(SystemExit, event_loop.run_forever) assert exc.value.code == 1 assert len(record) == 1 assert str(record[0].message) == 'run() must return an integer or None, not str'
def test_run_return_invalid_value(self, event_loop): class DummyCLIComponent(CLIApplicationComponent): async def run(self, ctx: Context): return 128 component = DummyCLIComponent() event_loop.run_until_complete(component.start(Context())) with pytest.warns(UserWarning) as record: exc = pytest.raises(SystemExit, event_loop.run_forever) assert exc.value.code == 1 assert len(record) == 1 assert str(record[0].message) == 'exit code out of range: 128'
def test_event_context(): parent = Context() session_details = SessionDetails('default', 5) event_details = EventDetails(publication=15, publisher=8, publisher_authid='user', publisher_authrole='role', topic='topic') context = EventContext(parent, session_details, event_details) assert context.session_id == 5 assert context.publisher_session_id == 8 assert context.publisher_auth_id == 'user' assert context.publisher_auth_role == 'role' assert context.publication_id == 15 assert context.topic == 'topic' assert context.enc_algo is None
async def test_null_configs(): component = SerializationComponent(serializers={ 'json': None, 'msgpack': None, 'pickle': None, 'yaml': None }) async with Context() as ctx: await component.start(ctx) assert ctx.json assert ctx.msgpack assert ctx.pickle assert ctx.yaml
async def test_component_start(): component = SerializationComponent(serializers={ 'json': {'encoder_options': {'allow_nan': False}}, 'msgpack': {'unpacker_options': {'encoding': 'iso-8859-1'}}, 'pickle': {'protocol': 3}, 'yaml': {'safe': False} }) async with Context() as ctx: await component.start(ctx) assert ctx.json assert ctx.msgpack assert ctx.pickle assert ctx.yaml
async def test_close_twice_async(asyncpg_url): """Test that closing a session releases connection resources, but remains usable.""" component = SQLAlchemyComponent(url=asyncpg_url) async with Context() as ctx: await component.start(ctx) session = ctx.require_resource(AsyncSession) pool = session.bind.pool await session.execute(text("SELECT 1")) assert pool.checkedout() == 1 await session.close() assert pool.checkedout() == 0 await session.execute(text("SELECT 1")) assert pool.checkedout() == 1 assert pool.checkedout() == 0
async def test_component_single(caplog, backend): caplog.set_level(logging.INFO, logger='asphalt.mailer.component') component = MailerComponent(backend=backend) async with Context() as ctx: await component.start(ctx) assert isinstance(ctx.mailer, Mailer) records = [ record for record in caplog.records if record.name == 'asphalt.mailer.component' ] records.sort(key=lambda r: r.message) assert len(records) == 2 assert records[0].message.startswith( "Configured mailer (default / ctx.mailer; class=%s)" % ctx.mailer.__class__.__name__) assert records[1].message.startswith('Mailer (default) stopped')
async def test_context_teardown(self, expected_exc): @context_teardown async def start(ctx: Context): nonlocal phase, received_exception phase = 'started' exc = yield phase = 'finished' received_exception = exc phase = received_exception = None context = Context() await start(context) assert phase == 'started' await context.close(expected_exc) assert phase == 'finished' assert received_exception == expected_exc
def test_call_context(): def progress(arg): pass parent = Context() session_details = SessionDetails('default', 5) call_details = CallDetails(progress=progress, caller=8, caller_authid='user', caller_authrole='role', procedure='procedurename') context = CallContext(parent, session_details, call_details) assert context.session_id == 5 assert context.caller_session_id == 8 assert context.caller_auth_id == 'user' assert context.caller_auth_role == 'role' assert context.procedure == 'procedurename' assert context.enc_algo is None assert context.progress is progress
async def test_single_client(event_loop): ctx = Context() component = WAMPComponent(ssl='default', serializer='default') ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) serializer = JSONSerializer() ctx.publish_resource(serializer, types=[Serializer]) ctx.publish_resource(ssl_context) await component.start(ctx) assert isinstance(ctx.wamp, WAMPClient) assert ctx.wamp.ssl is ssl_context assert ctx.wamp.serializer is serializer
async def test_single_renderer(): async with Context() as ctx: ctx.add_resource("åäö") component = TemplatingComponent(backend="jinja2", options={"package_name": "tests"}) await component.start(ctx) renderer = ctx.require_resource(TemplateRenderer) assert isinstance(renderer, TemplateRendererProxy) assert type(renderer.environment).__name__ == "Environment" assert (renderer.render("jinja2_context.html", str=str) == """\ <div> This is a sample Test variable: åäö </div>""") assert (renderer.render_string( "This is testvar: {{ ctx.require_resource(str) }}", str=str) == "This is testvar: åäö")
async def test_ready_callback(asynchronous): def ready_callback(engine, factory): nonlocal engine2, factory2 engine2 = engine factory2 = factory async def ready_callback_async(engine, factory): nonlocal engine2, factory2 engine2 = engine factory2 = factory engine2 = factory2 = None callback = ready_callback_async if asynchronous else ready_callback component = SQLAlchemyComponent(url='sqlite:///:memory:', ready_callback=callback) async with Context() as ctx: await component.start(ctx) engine = ctx.require_resource(Engine) factory = ctx.require_resource(sessionmaker) assert engine is engine2 assert factory is factory2
async def test_session_event_async(request, asyncpg_url, psycopg2_url): """Test that creating a session in a context does not leak memory.""" listener_session: Session listener_thread: Thread def listener(session: Session) -> None: nonlocal listener_session, listener_thread try: async_session = get_resource(AsyncSession) except NoCurrentContext: return if async_session and session is async_session.sync_session: listener_session = session listener_thread = current_thread() listen(Session, "before_commit", listener) request.addfinalizer(lambda: remove(Session, "before_commit", listener)) component = SQLAlchemyComponent(url=asyncpg_url) async with Context() as ctx: await component.start(ctx) dbsession = ctx.require_resource(AsyncSession) await dbsession.run_sync( lambda session: Person.metadata.create_all(session.bind)) dbsession.add(Person(name="Test person")) assert listener_session is dbsession.sync_session assert listener_thread is current_thread() engine = create_engine(psycopg2_url) with Session(engine) as sess: sess.add(Person(name="Test person 2")) sess.commit() engine.dispose() assert listener_session is dbsession.sync_session assert listener_thread is current_thread()
async def start(self, ctx: Context): for resource_name, context_attr, mailer in self.mailers: await mailer.start(ctx) ctx.publish_resource(mailer, resource_name, context_attr, types=[Mailer]) logger.info('Configured mailer (%s / ctx.%s)', resource_name, context_attr)
def run_application(component: Union[Component, Dict[str, Any]], *, event_loop_policy: str = None, max_threads: int = None, logging: Union[Dict[str, Any], int, None] = INFO, start_timeout: Union[int, float, None] = 10): """ Configure logging and start the given root component in the default asyncio event loop. Assuming the root component was started successfully, the event loop will continue running until the process is terminated. Initializes the logging system first based on the value of ``logging``: * If the value is a dictionary, it is passed to :func:`logging.config.dictConfig` as argument. * If the value is an integer, it is passed to :func:`logging.basicConfig` as the logging level. * If the value is ``None``, logging setup is skipped entirely. By default, the logging system is initialized using :func:`~logging.basicConfig` using the ``INFO`` logging level. The default executor in the event loop is replaced with a new :class:`~concurrent.futures.ThreadPoolExecutor` where the maximum number of threads is set to the value of ``max_threads`` or, if omitted, the default value of :class:`~concurrent.futures.ThreadPoolExecutor`. :param component: the root component (either a component instance or a configuration dictionary where the special ``type`` key is either a component class or a ``module:varname`` reference to one) :param event_loop_policy: entry point name (from the ``asphalt.core.event_loop_policies`` namespace) of an alternate event loop policy (or a module:varname reference to one) :param max_threads: the maximum number of worker threads in the default thread pool executor (the default value depends on the event loop implementation) :param logging: a logging configuration dictionary, :ref:`logging level <python:levels>` or ``None`` :param start_timeout: seconds to wait for the root component (and its subcomponents) to start up before giving up (``None`` = wait forever) """ assert check_argument_types() # Configure the logging system if isinstance(logging, dict): dictConfig(logging) elif isinstance(logging, int): basicConfig(level=logging) # Inform the user whether -O or PYTHONOPTIMIZE was set when Python was launched logger = getLogger(__name__) logger.info('Running in %s mode', 'development' if __debug__ else 'production') # Switch to an alternate event loop policy if one was provided if event_loop_policy: create_policy = policies.resolve(event_loop_policy) policy = create_policy() asyncio.set_event_loop_policy(policy) logger.info('Switched event loop policy to %s', qualified_name(policy)) # Assign a new default executor with the given max worker thread limit if one was provided event_loop = asyncio.get_event_loop() if max_threads is not None: event_loop.set_default_executor(ThreadPoolExecutor(max_threads)) logger.info('Installed a new thread pool executor with max_workers=%d', max_threads) # Instantiate the root component if a dict was given if isinstance(component, dict): component = cast(Component, component_types.create_object(**component)) logger.info('Starting application') context = Context() exception = None # type: Optional[BaseException] exit_code = 0 # Start the root component try: coro = asyncio.wait_for(component.start(context), start_timeout, loop=event_loop) event_loop.run_until_complete(coro) except asyncio.TimeoutError as e: exception = e logger.error('Timeout waiting for the root component to start') exit_code = 1 except Exception as e: exception = e logger.exception('Error during application startup') exit_code = 1 else: logger.info('Application started') # Add a signal handler to gracefully deal with SIGTERM try: event_loop.add_signal_handler(signal.SIGTERM, sigterm_handler, logger, event_loop) except NotImplementedError: pass # Windows does not support signals very well # Finally, run the event loop until the process is terminated or Ctrl+C is pressed try: event_loop.run_forever() except KeyboardInterrupt: pass except SystemExit as e: exit_code = e.code # Close the root context logger.info('Stopping application') event_loop.run_until_complete(context.close(exception)) # Shut down leftover async generators (requires Python 3.6+) try: event_loop.run_until_complete(event_loop.shutdown_asyncgens()) except (AttributeError, NotImplementedError): pass # Finally, close the event loop itself event_loop.close() logger.info('Application stopped') # Shut down the logging system shutdown() if exit_code: sys.exit(exit_code)