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

Add unique_constraints, a more versatile replacement for django_get_or_create and sqlalchemy_get_or_create #794

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

rbarrois
Copy link
Member

This change deprecates django_get_or_create and sqlalchemy_get_or_create; upgrade helpers are provided through a newly-introduced option renaming helper.

Closes #241: The new unique_constraints feature is more powerful, but supports the exact same cases; a specific case has been added to opt out of lookups for Factory.build() and Factory.stub().
Examples are provided for Django and SQLAlchemy unique constraint declarations.

Closes #69: With the implementation of unique_constraints - and its lazy declaration resolution algorithm, non-constrained declarations will only be resolved once lookups have failed; this avoids creating orphan objects.

Closes #781: The call-time parameters are now explicitly included in the lookup algorithm; they no longer need to be kept attached to the factory class, which leaked instances.

Note: the change includes updated code, additional tests, deprecation warning and migration helpers.

The code responsible for `get_or_create` places a copy of the passed-in
params on the factory class itself; which is kept after the instance has
been created.

Those fields are part of the BuildStep context, and should be fetched
from there instead.

The issue also occurs in DjangoModelFactory.

See #781 for details.
This could be used to ensure that an entry is actually a list.
Through `def _get_renamed_options()`, a specific `FactoryOptions`
subclass may now indicate that one of its fields should now use another
name, and optionally another syntax.

The warning message displays the new format to use when adjusting the
option.
@rbarrois
Copy link
Member Author

@francoisfreitag if you've got some time, I'd be happy to get another set of eyes on this ;)

Declare unique constraints on field values through this new `class Meta`
option.

When building a new instance, the parameters will first be resolved for
each unique constraint, and a dedicated lookup performed; an instance
will only be created if each lookup failed.

Declarations are resolved lazily: a non-constraint SubFactory will only
be called if each previous constraint lookup failed.
This change deprecates django_get_or_create and
sqlalchemy_get_or_create; upgrade helpers are provided throught the
option renaming helpers.

Closes #241

The new `unique_constraints` feature is more powerful, but supports the
exact same cases; a specific case has been added to opt out of lookups
for `Factory.build()` and `Factory.stub()`.
Examples are provided for Django and SQLAlchemy unique constraint
declarations.

Closes #69

With the implementation of `unique_constraints` - and its lazy
declaration resolution algorithm, non-constrained declarations will only
be resolved once lookups have failed; this avoids creating orphan
objects.

Closes #781

The call-time parameters are now explicitly included in the lookup
algorithm; they no longer need to be kept attached to the factory class,
which leaked instances.
The variable is a StepBuilder, not a BuildStep...
Copy link
Member

@francoisfreitag francoisfreitag left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome ⭐.

It unifies <orm>_get_or_create() under a single option. Having the unique_constraints matching Django unique_together facilitates writing unique_constraints for Django. unique_constraints are more powerful than get_or_create, they grant more controls over the lookup strategy. Removing the junk data generated when resolving the factory declarations is just great.

I’m sure users will be thankful for the very clear migration path. It would be nice to provide a deadline for removing the compatibility schemes for get_or_create. Perhaps for version 4.0?

IMO, the following commits should be squashed. They all have a single purpose, adding the unique_constraints feature. Splitting them was helpful for the review 👍.

  • Expose memleak in SQLAlchemyModelFactory.
  • Add an optional normalization to class Meta fields
  • Provide support for migrating class Meta fields
  • Add unique_constraints and associated _lookup()
  • Replace *_get_or_create with unique_constraints.

As far as I’m concerned, get_or_create (or unique_constraints) should not be used in tests. When writing a test, the exact state of the database should be known and the dev should know whether the data is to be created or if it’s already present in the DB and should be retrieved. But users rely on that feature, and getting it an upgrade is likely beneficial to them.


Commit “Expose memleak in SQLAlchemyModelFactory.”

-Expose memleak in SQLAlchemyModelFactory.
-Expose memleak in SQLAlchemyModelFactory

Commit “Replace *_get_or_create with unique_constraints.”:

-Replace *_get_or_create with unique_constraints.
+Replace *_get_or_create with unique_constraints

and

-upgrade helpers are provided throught the
+ upgrade helpers are provided through the

Commits “Rename a StepBuilder instance to step_builder” and “docs: Use .. rubric:: for minor sections. ” could be separate PRs, this one is big enough.


# Ensure the instance pointed to by the weak reference is no longer available.
self.assertIsNone(target_weak())

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

class FactoryOptions:

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

self.transform = transform

def __str__(self):
return '%s(%r, %r, tranform=%r)' % (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return '%s(%r, %r, tranform=%r)' % (
return '%s(%r, %r, transform=%r)' % (

Comment on lines +248 to +249
self.assertEqual(1, len(w))
message = str(w[-1].message)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.assertEqual(1, len(w))
message = str(w[-1].message)
[warn] = w
message = str(warn.message)

2. That method computes groups of lookups to try:

- Any call-time parameter that appear in any unique constraint will *always* be included;
- The first lookups are performed on the unique constraintss sharing the most parameters
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- The first lookups are performed on the unique constraintss sharing the most parameters
- The first lookups are performed on the unique constraints sharing the most parameters


class MultipleGetOrCreateFieldsTest(django_test.TestCase):
def test_one_defined(self):
def test_any_field(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this is a better name, the improvement could be made in a separate commit.

@@ -1048,4 +1082,3 @@ class Meta:

# Ensure the instance pointed to by the weak reference is no longer available.
self.assertIsNone(target_weak())

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The improvement could be made in a separate commit.

Comment on lines +2118 to +2121
if kwargs == self.keys:
return self.instance
else:
return None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if kwargs == self.keys:
return self.instance
else:
return None
return self.instance if kwargs == self.keys else None


a = factory.Sequence(lambda n: 'foo_%s' % n)
b = 2
c = 3
d = 4

o = TestModelFactory()
self.assertEqual({}, o._defaults)
self.assertEqual(dict(a='foo_0', b=2, c=3, d=4), TestModel.objects.last_lookup)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.assertEqual(dict(a='foo_0', b=2, c=3, d=4), TestModel.objects.last_lookup)
self.assertEqual({"a": 'foo_0', "b": 2, "c": 3, "d": 4}), TestModel.objects.last_lookup)

@@ -2293,15 +2299,17 @@ def test_full_get_or_create(self):
class TestModelFactory(factory.django.DjangoModelFactory):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring above this line could be updated to remove the get_or_create mention.

self.assertEqual(o1, o4)
self.assertEqual(o2, o6)
self.assertEqual([o1, o2, o3, o5, o7], LookupGroupTests.store)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current django_get_or_create fails when one of the unique field is a Trait or Maybe. Maybe you could add a test to show that (and in the worst case xfail it)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what you're talking about; could you point to a code sample where that fails?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is a failing test case on the actual implementation. I could adapt it to yours or you might take a look directly #810

@aalvrz
Copy link

aalvrz commented Feb 8, 2021

Any updates on this?

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