-
Notifications
You must be signed in to change notification settings - Fork 399
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
base: master
Are you sure you want to change the base?
Conversation
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.
@francoisfreitag if you've got some time, I'd be happy to get another set of eyes on this ;) |
8ea93ea
to
2a87857
Compare
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...
There was a problem hiding this 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.
tests/test_django.py
Outdated
|
||
# Ensure the instance pointed to by the weak reference is no longer available. | ||
self.assertIsNone(target_weak()) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
class FactoryOptions: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.transform = transform | ||
|
||
def __str__(self): | ||
return '%s(%r, %r, tranform=%r)' % ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return '%s(%r, %r, tranform=%r)' % ( | |
return '%s(%r, %r, transform=%r)' % ( |
self.assertEqual(1, len(w)) | ||
message = str(w[-1].message) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- 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): |
There was a problem hiding this comment.
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.
tests/test_django.py
Outdated
@@ -1048,4 +1082,3 @@ class Meta: | |||
|
|||
# Ensure the instance pointed to by the weak reference is no longer available. | |||
self.assertIsNone(target_weak()) | |||
|
There was a problem hiding this comment.
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.
if kwargs == self.keys: | ||
return self.instance | ||
else: | ||
return None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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): |
There was a problem hiding this comment.
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) | ||
|
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
Any updates on this? |
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 forFactory.build()
andFactory.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.