kuzxnia / async_factory_boy

factory_boy extension with asynchronous ORM support
https://pypi.org/project/async-factory-boy/
MIT License
14 stars 7 forks source link

Issue with using with sub-factories #5

Open ADR-007 opened 9 months ago

ADR-007 commented 9 months ago

Hi! Thank you for this project; it is really helpful!

I found that it doesn't currently work with sub-factories, and here is my hacky fix:

from inspect import isawaitable
from typing import Any

import factory
from async_factory_boy.factory.sqlalchemy import AsyncSQLAlchemyFactory
from factory import errors
from factory.builder import BuildStep, DeclarationSet, Resolver, StepBuilder, parse_declarations

class AsyncResolver(Resolver):
    async def async_get(self, name: str) -> Any:
        """Get a value from the stub, resolving async values."""
        value = getattr(self, name)
        if isawaitable(value):
            value = await value
            self._Resolver__values[name] = value

        return value

class AsyncBuildStep(BuildStep):
    async def resolve(self, declarations: DeclarationSet) -> None:
        self.stub = AsyncResolver(
            declarations=declarations,
            step=self,
            sequence=self.sequence,
        )

        for field_name in declarations:
            self.attributes[field_name] = await self.stub.async_get(field_name)

class AsyncStepBuilder(StepBuilder):
    async def build(self, parent_step: AsyncBuildStep = None, force_sequence: Any = None) -> Any:
        """Build a factory instance."""
        # TODO: Handle "batch build" natively
        pre, post = parse_declarations(
            self.extras,
            base_pre=self.factory_meta.pre_declarations,
            base_post=self.factory_meta.post_declarations,
        )

        if force_sequence is not None:
            sequence = force_sequence
        elif self.force_init_sequence is not None:
            sequence = self.force_init_sequence
        else:
            sequence = self.factory_meta.next_sequence()

        step = AsyncBuildStep(
            builder=self,
            sequence=sequence,
            parent_step=parent_step,
        )
        await step.resolve(pre)

        args, kwargs = self.factory_meta.prepare_arguments(step.attributes)

        instance = self.factory_meta.instantiate(
            step=step,
            args=args,
            kwargs=kwargs,
        )
        if isawaitable(instance):
            instance = await instance

        postgen_results = {}
        for declaration_name in post.sorted():
            declaration = post[declaration_name]
            postgen_results[declaration_name] = declaration.declaration.evaluate_post(
                instance=instance,
                step=step,
                overrides=declaration.context,
            )
        self.factory_meta.use_postgeneration_results(
            instance=instance,
            step=step,
            results=postgen_results,
        )
        return instance

class FixedAsyncSQLAlchemyFactory(AsyncSQLAlchemyFactory):
    @classmethod
    async def _generate(cls, strategy: Any, params: Any) -> None:
        """Generate the object.

        Args:
            params (dict): attributes to use for generating the object
            strategy: the strategy to use
        """
        # Original params are used in _get_or_create if it cannot build an
        # object initially due to an IntegrityError being raised
        cls._original_params = params

        if cls._meta.abstract:
            raise errors.FactoryError(
                "Cannot generate instances of abstract factory {f}; "
                "Ensure {f}.Meta.model is set and {f}.Meta.abstract "
                "is either not set or False.".format(**dict(f=cls.__name__)))

        step = AsyncStepBuilder(cls._meta, params, strategy)
        return await step.build()

I would be happy if it would be fixed on the library side. What do you think?

Thanks!

kuzxnia commented 5 months ago

Hi, thanks for the suggestions and code snipped 🚀 Would you like to prepare PR with tests?

ADR-007 commented 5 months ago

Added PR :)

wombat-artem commented 4 months ago

Hey, it would be nice if you merged this PR. It will help me a lot.

ADR-007 commented 4 months ago

Hi @wombat-artem, I fixed Python 3.8 support (at least, I hope so). For now you can use my branch as the package source with pip or poetry

wombat-artem commented 3 months ago

Thanks @ADR-007

matthewbal commented 1 month ago

I'm also having an issue with subfactories, the library works perfectly for 1 or 2 subfactories, but the moment I go to 3 on the same object it starts throwing this if I let it use them. For now I've just been limiting my use of subfactories but it'd be really great to have this workaround merged in :)

sqlalchemy.exc.SAWarning: Usage of the 'Session.add()' operation is not currently supported within the execution stage of the flush process. Results may not be consistent. Consider using alternative event listeners or connection-level operations instead.