Skip to content

Commit

Permalink
Merge pull request #12 from DanCardin/dc/update-docs
Browse files Browse the repository at this point in the history
  • Loading branch information
DanCardin authored Dec 6, 2021
2 parents 86a53cb + e4f8f32 commit ec14a08
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 10 deletions.
5 changes: 1 addition & 4 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ Declarative
-----------

.. automodule:: sqlalchemy_model_factory.declarative
:members: declarative

.. automodule:: sqlalchemy_model_factory.declarative
:members: DeclarativeMF
:members: declarative, DeclarativeMF


Factory Utilities
Expand Down
63 changes: 63 additions & 0 deletions docs/source/declarative.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,66 @@ ou have one pre-constructed.
instantiated. This is notable, primarily in the event that an `__init__`
is defined on the class for whatever reason. Each class will be instantiated
once without arguments.


Conversion from ``@register_at``
--------------------------------
If you have an existing body of model-factory functions registered using the
``@register_at`` pattern, you can incrementally adopt (and therefore incrementally
get viable type hinting support) the declarative api.


If you are importing ``from sqlalchemy_model_factory import register_at``, today,
you can import ``from sqlalchemy_model_factory import registry``, and send that
into the ``@declarative`` decorator:

.. code-block:: python
from sqlalchemy_model_factory import registry
@declarative(registry=registry)
class ModelFactory:
def example():
...
Alternatively, you can switch to manually constructing your own ``Registry``,
though you will need to change your ``@register_at`` calls to use it!

.. code-block:: python
from sqlalchemy_model_factory import Registry, declarative
registry = Registry()
@register_at("path", name="new")
def new_path():
...
@declarative(registry=registry)
class Base:
def example():
...
Then once you make use of the annotation, say in some test:

.. code-block:: python
def test_path(mf: Base):
mf.example()
you should get go-to-definition and hinting support for the declaratively specified
methods **only**.

.. note::

You might see mypy type errors like ``Type[...] has no attribute "..."``
for ``@register_at``. You can either ignore these, or else apply the
``compat`` as a superclass to your declarative:

.. code-block:: python
from sqlalchemy_model_factory import declarative
@declarative.declarative
class Factory(declarative.compat):
...
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "sqlalchemy-model-factory"
version = "0.4.0"
version = "0.4.1"
description = "A library to assist in generating models from a central location."
authors = ["Dan Cardin <[email protected]>"]
license = "Apache-2.0"
Expand Down
8 changes: 5 additions & 3 deletions src/sqlalchemy_model_factory/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from sqlalchemy_model_factory.declarative import declarative # noqa
from sqlalchemy_model_factory.registry import register_at, Registry, registry # noqa
from sqlalchemy_model_factory.utils import autoincrement, fluent, for_model # noqa
# flake8: noqa
from sqlalchemy_model_factory.base import ModelFactory
from sqlalchemy_model_factory.declarative import declarative
from sqlalchemy_model_factory.registry import register_at, Registry, registry
from sqlalchemy_model_factory.utils import autoincrement, fluent, for_model
20 changes: 20 additions & 0 deletions src/sqlalchemy_model_factory/declarative.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,26 @@ def _declarative(cls, *, context=None):
return _root_declarative


class compat_meta(type):
def __getattr__(self, attr):
return super().__getattr__(attr)


class compat(metaclass=compat_meta):
"""Compatibility base class for factory classes.
Essentially what we're doing here is providing a base-class which
will disable mypy checks for attributes which might not exist like
'Type[factory] has no attribute "foo"'. By defining a getattr, we opt
that class out of such checks (because it cannot be statically defined),
without modifying the class's behavior.
This requires a metaclass because nested class definitions mean the type
of your sub-class attribute is actually the type itself rather than an
instance.
"""


class DeclarativeMF:
"""Provide an alternative to the class decorator for declarative base factories.
Expand Down
50 changes: 48 additions & 2 deletions tests/test_declarative.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from sqlalchemy import Column, types
import pytest
from sqlalchemy import Column, create_engine, types
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.session import sessionmaker
from sqlalchemy_model_factory import base
from sqlalchemy_model_factory.declarative import declarative, DeclarativeMF
from sqlalchemy_model_factory.declarative import compat, declarative, DeclarativeMF
from sqlalchemy_model_factory.pytest import create_registry_fixture
from sqlalchemy_model_factory.registry import Registry
from tests import get_session
Expand Down Expand Up @@ -130,3 +132,47 @@ def default(self, id: int):

foos = session.query(Foo.id).all()
assert foos == [(-5,), (5,)]


# Mixed-dynamic and declarative setup.
mixed_registry = Registry()


@mixed_registry.register_at("ex", name="new")
def new():
return Foo(id=6)


@declarative(registry=mixed_registry)
class MixedModelFactory(compat):
class ex(compat):
@staticmethod
def default(id: int):
return Foo(id=id)


@pytest.fixture
def mixed_mf_session(mf_engine):
mf_engine = create_engine("sqlite:///")
Base.metadata.create_all(mf_engine)
Session = sessionmaker(mf_engine)
return Session()


@pytest.fixture
def mixed_mf(mixed_mf_session):
with base.ModelFactory(mixed_registry, mixed_mf_session) as model_manager:
yield model_manager


def test_mixed_dynamic_and_declarative(mixed_mf: MixedModelFactory, mixed_mf_session):
session = mixed_mf_session

mixed_mf.ex.new()
foos = session.query(Foo.id).all()
assert foos == [(6,)]

mixed_mf.ex.default(5)

foos = session.query(Foo.id).all()
assert foos == [(5,), (6,)]

0 comments on commit ec14a08

Please sign in to comment.