Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PostGeneration extracted = None vs not given #476

Open
petriggles opened this issue May 17, 2018 · 3 comments
Open

PostGeneration extracted = None vs not given #476

petriggles opened this issue May 17, 2018 · 3 comments
Labels

Comments

@petriggles
Copy link

I've come across a case where, with post_generation, I can't tell if no value was given upon creation in order to be extracted, or if the value given was None. It's set up sort of like the following.

class FooFactory(factory.Factory)
    @factory.post_generation
    def bar(self, created, extracted, **kwargs):
        self.bar = extracted if *extracted value was given* else fake_bar()

I'd like to be able to tell the difference between FooFactory.create() and FooFactory.create(bar=None), such that
FooFactory.create().bar == <faked_value> and FooFactory.create(bar=None).bar == None

Unless I'm missing something, it doesn't seem like there's a way to do this.

@johnraz
Copy link

johnraz commented Feb 17, 2020

I do have the very same need and this use case seems quite "usual" to me.

So the class responsible for this is:

class PostGeneration(PostGenerationDeclaration):
"""Calls a given function once the object has been generated."""
def __init__(self, function):
super(PostGeneration, self).__init__()
self.function = function
def call(self, instance, step, context):
logger.debug(
"PostGeneration: Calling %s.%s(%s)",
self.function.__module__,
self.function.__name__,
utils.log_pprint(
(instance, step),
context._asdict(),
),
)
create = step.builder.strategy == enums.CREATE_STRATEGY
return self.function(
instance, create, context.value, **context.extra)

There is a value on the context, context.value_provided, that I hoped would have provided insight on whether or not the argument was passed to the instantiation of the factory but it doesn't seem to be working like that.

For now I'm relying on using an extra context arg... which gives (pseudo code):

class MyFactory:
    @factory.post_generation
    def my_field(self, create, extracted, **kwargs):
        if not create:
            return
        set_to_none = kwargs.get("None")

        if set_to_none:
            self.my_field = None
        elif extracted:
            self.my_field = extracted
        else:
            self.my_field = "generated value"

This will give the following behaviors:

The most confusing one:

my_factory = MyFactory(my_field=None)
my_factory.my_field == "generated value"

The not so pretty and full of indirection:

my_factory = MyFactory(my_field__None=True)
my_factory.my_field == None

The 2 most obvious:

my_factory = MyFactory(my_field="something")
my_factory.my_field == "something"

my_factory = MyFactory()
my_factory.my_field == "generated value"

@rbarrois:

I looked in the code quite a bit but I'm not sure from a design point of view if and how we could implement this?

I was thinking of passing a new field to the post generation function, something like is_value_provided=True in case something is passed to the field.

We could also make this behavior optional by passing an argument to the decorator:

@factory.post_generation(include_value_provided=True)
def my_field(self, create, extracted, is_value_provided, **kwargs):
    ...

If you don't mind providing some guidance I'm open to submit a PR and improve this very case 😉

Thanks.

@rbarrois
Copy link
Member

Thanks for reviving this issue!

Indeed, the API for passing complex arguments to post_generation declarations isn't very satisfying.

I'm wondering whether we should split it up:

  • One API for "do something after generating the instance", but only accept kwargs-based parameters;
  • Another one closer to the current setup, where a default value can be provided or overridden.

For those questions, I like to start from the kind of API one would expect when using the factory; I'm thinking of those:

# Provide parameters to the "publish" action
ProfileFactory(publish__targets=[1, 2, 3])

# Decide whether to publish
ProfileFactory(publish=False)

# Override the default values
ProfileFactory(groups=['admin', 'testers'])
# If empty, equivalent to:
ProfileFactory(groups=['users'])

Do you see other usage patterns that should be covered beyond these call-side APIs?

@johnraz
Copy link

johnraz commented Feb 18, 2020

Just making sure that I read you correctly.

Case 1

You would have one case where the post_generation is more of an "action":

And your example allows to provide kwargs to that action:

# Provide parameters to the "publish" action
ProfileFactory(publish__targets=[1, 2, 3])

# Decide whether to publish
ProfileFactory(publish=False)

Case 2

The other case would be changing the value of a model field after the model is created, in this case it is:

# Override the default values
ProfileFactory(groups=['admin', 'testers'])
# If empty, equivalent to:
ProfileFactory(groups=['users'])

So I believe the specific case for this issue would belong to case 2:

# Override the default with None
ProfileFactory(groups=None)

# Which is different from:
ProfileFactory() --> Which is equivalent to ProfileFactory(groups=['users'])

Do you have any ideas on how that could look in the declaration?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants