From c8b0275265f416b9c89307486bfe8bcea5fcce50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Gr=C3=B6nholm?= Date: Mon, 30 Dec 2024 17:29:33 +0200 Subject: [PATCH] Updated configs, docs and some code for Asphalt 5 --- .github/workflows/test.yml | 4 +-- docs/conf.py | 2 ++ docs/configuration.rst | 34 ++++++----------------- pyproject.toml | 32 +++++++++++----------- src/asphalt/sqlalchemy/_component.py | 29 ++++---------------- tests/test_component.py | 40 ++++++++-------------------- 6 files changed, 43 insertions(+), 98 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7f95541..85e99af 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,9 +24,7 @@ jobs: - name: Start external services run: docker compose up -d - name: Install dependencies - run: | - pip install git+https://github.com/asphalt-framework/asphalt.git@5.0 - pip install -e .[test] coverage + run: pip install -e .[test] - name: Test with pytest run: coverage run -m pytest -v - name: Generate coverage report diff --git a/docs/conf.py b/docs/conf.py index 838e2e3..22b23f5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,6 +7,7 @@ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx_autodoc_typehints", + "sphinx_rtd_theme", "sphinx_tabs.tabs", ] @@ -26,6 +27,7 @@ exclude_patterns = ["_build"] pygments_style = "sphinx" autodoc_default_options = {"members": True, "show-inheritance": True} +autodoc_inherit_docstrings = False highlight_language = "python3" todo_include_todos = False diff --git a/docs/configuration.rst b/docs/configuration.rst index 181079a..a0da32f 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -5,16 +5,16 @@ Configuration A typical SQLAlchemy configuration consists of a single database. At minimum, you only need a connection URL (see the -`SQLAlchemy documentation`_ for how to construct one). Such a configuration would look -something like this:: +:doc:`SQLAlchemy documentation ` on how to construct one). +Such a configuration would look something like this:: components: sqlalchemy: - url: postgresql+asyncpg://user:password@10.0.0.8/mydatabase + url: postgresql+psycopg://user:password@10.0.0.8/mydatabase This will add two static resources and one resource factory, as follows: -* engine (type: :class:`sqlalchemy.future.engine.Engine` or +* engine (type: :class:`sqlalchemy.engine.Engine` or :class:`sqlalchemy.ext.asyncio.AsyncEngine`) * sessionmaker (type: :class:`sqlalchemy.orm.sessionmaker`) * session factory (generates :class:`~sqlalchemy.orm.Session` or @@ -26,7 +26,7 @@ sharing features):: components: sqlalchemy: url: - drivername: postgresql+asyncpg + drivername: postgresql+psycopg username: user password: password host: 10.0.0.8 @@ -34,15 +34,14 @@ sharing features):: .. seealso:: * :class:`sqlalchemy.engine.URL` - * :class:`asphalt.sqlalchemy.component.SQLAlchemyComponent` - -.. _SQLAlchemy documentation: https://docs.sqlalchemy.org/en/14/core/engines.html + * :class:`asphalt.sqlalchemy.SQLAlchemyComponent` Setting engine or session options --------------------------------- If you need to adjust the options used for creating new sessions, or pass extra -arguments to the engine, you can do so by specifying them in the ``session`` option:: +arguments to the engine, you can do so by specifying them in the ``session_args`` and +``engine_args``, respectively:: components: sqlalchemy: @@ -52,20 +51,3 @@ arguments to the engine, you can do so by specifying them in the ``session`` opt session_args: info: hello: world - -Multiple databases ------------------- - -If you need to work with multiple databases, you will need to use multiple instances -of the ``sqlalchemy`` component:: - - components: - sqlalchemy: - resource_name: db1 - url: postgresql+asyncpg:///mydatabase - sqlalchemy/db2: - resource_name: db2 - url: sqlite+aiosqlite:///mydb.sqlite - -This will make the appropriate resources available using their respective namespaces -(``db1`` or ``db2`` instead of ``default``). diff --git a/pyproject.toml b/pyproject.toml index deaf6c7..c5a4d6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ classifiers = [ ] requires-python = ">=3.9" dependencies = [ - "asphalt ~= 4.11", + "asphalt @ git+https://github.com/asphalt-framework/asphalt", "SQLAlchemy[asyncio] >= 2.0", ] dynamic = ["version"] @@ -40,6 +40,7 @@ test = [ "aiosqlite", "anyio[trio] >= 4.2", "asyncmy; platform_python_implementation == 'CPython'", + "coverage >= 7", "pymysql", "psycopg >= 3.1; platform_python_implementation == 'CPython'", "pytest >= 7.4", @@ -73,9 +74,10 @@ select = [ [tool.ruff.lint.isort] known-first-party = ["asphalt.sqlalchemy"] +known-third-party = ["asphalt.core"] [tool.pytest.ini_options] -addopts = "-rsx --tb=short" +addopts = ["-rsfE", "--tb=short"] testpaths = ["tests"] [tool.mypy] @@ -92,20 +94,18 @@ branch = true show_missing = true [tool.tox] -legacy_tox_ini = """ -[tox] -envlist = py39, py310, py311, py312, py313, pypy3 +env_list = ["py39", "py310", "py311", "py312", "py313"] skip_missing_interpreters = true -minversion = 4.0 -[testenv] -extras = test -commands = python -m pytest {posargs} -setenv = - MYSQL_URL = mysql+pymysql://root@localhost:33060/asphalttest - POSTGRESQL_URL = postgresql+psycopg://postgres:secret@localhost:54320/asphalttest - -[testenv:docs] -extras = doc -commands = sphinx-build -W docs build/sphinx +[tool.tox.env_run_base] +commands = [["python", "-m", "pytest", { replace = "posargs", extend = true }]] +package = "editable" +extras = ["test"] +setenv = """ + MYSQL_URL = mysql+pymysql://root@localhost:33060/asphalttest + POSTGRESQL_URL = postgresql+psycopg://postgres:secret@localhost:54320/asphalttest """ + +[tool.tox.env.docs] +commands = [["sphinx-build", "-W", "-n", "docs", "build/sphinx", { replace = "posargs", extend = true }]] +extras = ["doc"] diff --git a/src/asphalt/sqlalchemy/_component.py b/src/asphalt/sqlalchemy/_component.py index 6bc6c35..4813efa 100644 --- a/src/asphalt/sqlalchemy/_component.py +++ b/src/asphalt/sqlalchemy/_component.py @@ -30,8 +30,6 @@ from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.pool import Pool -logger = logging.getLogger(__name__) - class SQLAlchemyComponent(Component): """ @@ -59,15 +57,15 @@ class SQLAlchemyComponent(Component): * ``expire_on_commit``: ``False`` :param url: the connection url passed to - :func:`~sqlalchemy.engine.create_engine` + :func:`~sqlalchemy.create_engine` (can also be a dictionary of :class:`~sqlalchemy.engine.url.URL` keyword arguments) :param bind: a connection or engine to use instead of creating a new engine :param prefer_async: if ``True``, try to create an async engine rather than a synchronous one, in cases like ``psycopg`` where the driver supports both :param engine_args: extra keyword arguments passed to - :func:`sqlalchemy.engine.create_engine` or - :func:`sqlalchemy.ext.asyncio.create_engine` + :func:`sqlalchemy.create_engine` or + :func:`sqlalchemy.ext.asyncio.create_async_engine` :param session_args: extra keyword arguments passed to :class:`~sqlalchemy.orm.session.Session` or :class:`~sqlalchemy.ext.asyncio.AsyncSession` @@ -76,9 +74,8 @@ class SQLAlchemyComponent(Component): :param ready_callback: a callable that is called right before the resources are added to the context (can be a coroutine function too) :param poolclass: the SQLAlchemy pool class (or a textual reference to one) to use; - passed to :func:`sqlalchemy.engine.create_engine` or - :func:`sqlalchemy.ext.asyncio.create_engine` - :param resource_name: name space for the database resources + passed to :func:`sqlalchemy.create_engine` or + :func:`sqlalchemy.ext.asyncio.create_async_engine` """ _engine: Engine | AsyncEngine @@ -96,9 +93,7 @@ def __init__( commit_executor_workers: int = 50, ready_callback: Callable[[Engine, sessionmaker[Any]], Any] | str | None = None, poolclass: str | type[Pool] | None = None, - resource_name: str = "default", ): - self.resource_name = resource_name self.commit_thread_limiter = CapacityLimiter(commit_executor_workers) self.ready_callback = resolve_reference(ready_callback) engine_args = engine_args or {} @@ -220,23 +215,19 @@ async def start(self) -> None: add_resource( self._engine, - self.resource_name, description="SQLAlchemy engine (asynchronous)", teardown_callback=teardown_callback, ) add_resource( self._sessionmaker, - self.resource_name, description="SQLAlchemy session factory (synchronous)", ) add_resource( self._async_sessionmaker, - self.resource_name, description="SQLAlchemy session factory (asynchronous)", ) add_resource_factory( self.create_async_session, - self.resource_name, description="SQLAlchemy session (asynchronous)", ) else: @@ -253,24 +244,14 @@ async def start(self) -> None: add_resource( self._engine, - self.resource_name, description="SQLAlchemy engine (synchronous)", teardown_callback=teardown_callback, ) add_resource( self._sessionmaker, - self.resource_name, description="SQLAlchemy session factory (synchronous)", ) add_resource_factory( self.create_session, - self.resource_name, description="SQLAlchemy session (synchronous)", ) - - logger.info( - "Configured SQLAlchemy resources (%s; dialect=%s, driver=%s)", - self.resource_name, - bind.dialect.name, - bind.dialect.driver, - ) diff --git a/tests/test_component.py b/tests/test_component.py index 33c4eac..bc714ea 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -34,47 +34,29 @@ pytestmark = pytest.mark.anyio -@pytest.mark.parametrize( - "component_opts, args", - [ - pytest.param({}, ()), - pytest.param({"resource_name": "alternate"}, ("alternate",)), - ], -) -async def test_component_start_sync( - component_opts: dict[str, Any], args: tuple[Any] -) -> None: +async def test_component_start_sync() -> None: """Test that the component creates all the expected (synchronous) resources.""" url = URL.create("sqlite", database=":memory:") - component = SQLAlchemyComponent(url=url, **component_opts) + component = SQLAlchemyComponent(url=url) async with Context(): await component.start() - get_resource_nowait(Engine, *args) - get_resource_nowait(sessionmaker, *args) - get_resource_nowait(Session, *args) + get_resource_nowait(Engine) + get_resource_nowait(sessionmaker) + get_resource_nowait(Session) -@pytest.mark.parametrize( - "component_opts, args", - [ - pytest.param({}, ()), - pytest.param({"resource_name": "alternate"}, ("alternate",)), - ], -) -async def test_component_start_async( - component_opts: dict[str, Any], args: tuple[Any] -) -> None: +async def test_component_start_async() -> None: """Test that the component creates all the expected (asynchronous) resources.""" url = URL.create("sqlite+aiosqlite", database=":memory:") - component = SQLAlchemyComponent(url=url, **component_opts) + component = SQLAlchemyComponent(url=url) async with Context(): await component.start() - get_resource_nowait(AsyncEngine, *args) - async_session_class = get_resource_nowait(async_sessionmaker, *args) - get_resource_nowait(AsyncSession, *args) - sync_session_class = get_resource_nowait(sessionmaker, *args) + get_resource_nowait(AsyncEngine) + async_session_class = get_resource_nowait(async_sessionmaker) + get_resource_nowait(AsyncSession) + sync_session_class = get_resource_nowait(sessionmaker) assert async_session_class.kw["sync_session_class"] is sync_session_class