-
-
Notifications
You must be signed in to change notification settings - Fork 32.9k
Fixed #20625 -- Chainable Manager/QuerySet methods. #1328
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
Conversation
sex = models.CharField(max_length=1, choices=(('M', 'Male'), ('F', 'Female'))) | ||
people = PersonManager() | ||
|
||
This example allows you to call both ``men()`` and ``women()`` directly from the manager ``Person.people``. |
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.
Text wrapping
Overall I love this change! Just to check cos I'm not that clear on how inspect is working - are |
@mjtamlyn for I've added a new commit with a test that ensures that the stock Btw thanks for the review. I think I've already made most of the changes you've suggested. |
manager_cls._queryset_class = cls | ||
return manager_cls | ||
|
||
def as_manager(cls, base_class=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.
Should we keep the base_class
argument for this method?
As documented it's already possible to set the attribute QuerySet.base_manager_class
, so it effectively introduces more than one way to do it.
@akaariai: I leave it up to you since it was part of your original proposal.
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 removed it, I think it makes for a cleaner API. It wasn't documented by an example anyway because I didn't want to show 2 different ways of doing the same thing.
In any case, it would be a lot easier to add it back if people feel a need for it than it would be to deprecate it down the road.
@timgraham: I made most of the suggested changes. The comment regarding the gender issue has been collapsed because of the updated diff but I replied to it. There is also a pending remark regarding the use of "automatically". |
@loic FYI this is no longer merging cleanly. We can probably fix it when we merge it though |
@mjtamlyn thanks for the heads up; I've rebased to the latest master. |
This example allows you to call both ``men()`` and ``women()`` directly from | ||
the manager ``Person.people``. | ||
|
||
.. _create-manager-with-queryset-methods: |
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'd add a section heading here "Creating Manager
instances with QuerySet
methods" (you can then remove the custom text from the :ref: links as well) and make it more clear that this approach is intended to replace the above: "In lieu of the above approach which requires duplicating methods on the both the QuerySet and the Manager, ...."
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.
Done even though GH doesn't collapse this outdated diff.
return manager_method | ||
|
||
new_methods = {} | ||
predicate = inspect.isfunction |
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 would be more readable as a one-liner:
predicate = inspect.isfunction if six.PY3 else inspect.ismethod
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.
No worries, will update promptly.
continue | ||
# Only copy public methods or methods with the attribute `manager=True`. | ||
should_copy = getattr(method, 'manager', None) | ||
if should_copy is False or should_copy is None and name.startswith('_'): |
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.
Please add parentheses around the and
block, precedence isn't obvious.
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.
Done
I tested the The only way to support inheritance is by generating real classes ahead of time, a class factory can do that, the Between Also I'd like to point out that both |
Added two commits which hopefully address the concerns of making I still like better the end user API of I've wanted this general feature pretty much since I've started using Django, so if there is consensus on Edit: One more reason I'm not so keen on the |
Both as_manager() and from_queryset() work and have some advantages each. as_manager() is slightly shorter and requires one less import in the common case, while from_queryset() is better if you need both custom queryset and manager classes, and doesn't have the circular dependency problem. My preference is as_manager() because it makes the common case shortest. But, I can go with from_queryset(), too. Most of all I don't want to stall the patch on this issue. Seems like overall there is a slight preference for from_queryset(), so if no further ideas or opinions are presented lets just go with from_queryset(). |
Maybe there is a middle ground:
So effectivelly you could do: class BaseManager(Manager):
def __init__(self, *args, **kwargs):
pass
CustomManager = BaseManager.from_queryset(CustomQuerySet)
# For the common case where you only need a custom `QuerySet`:
manager = CustomQuerySet.as_manager()
# When you need a custom `Manager`:
manager = CustomManager(*args, **kwargs)
# or
manager = BaseManager.from_queryset(CustomQuerySet)(*args, **kwargs) Edit: One thing I like about this approach is that we make it elegant to create a Edit 2: d8d3a60 implements this. I'm now nearly convinced this is the way to go. Edit 3: Touched up the docs which should be mostly accurate again, we may need to update the release notes to mention |
What is the argument for Personally I don't see sufficient justification there for adding manager-specific API to queryset and making the dependency between those classes bidirectional. |
My argument is that this feature will pretty much deprecate the concept of manager as we know it. And when most of the time all we need is This new feature opens up a lot of new possibilities, With this new feature we can do: class CompositeQuerySet(RatedQuerySet, PublishedQuerySet, SiteRelatedQuerySet):
pass
objects = CompositeQuerySet.as_manager() Which replaces: class RatingManager(Manager):
...
class PublishingManager(Manager):
...
class SiteManager(Manager):
...
objects = ? # Pick one only. Combining multiple Yet this patch ensures that anyone who really want to create a custom Edit: Reworked the example and changed wording to some extent. Edit 2: For the record, I do not suggest we deprecate the |
'_queryset_class': queryset_class, | ||
} | ||
class_dict.update(cls._get_queryset_methods(queryset_class)) | ||
return type(cls.__name__, (cls,), class_dict) |
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'd try to build a more informative name, using also the queryset_class's name; ("%sFrom%s" % (cls.__name__,queryset_class.__name__))
seems reasonable. True, normally you won't see the class name in code, but it will be helpful in tracebacks and Django debug screens where locals are printed.
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 agree with this suggestion; it's a small change but it could really help debugging. Without it, all manager classes created from querysets will just be called "Manager", which could be quite confusing if you're working with more than one of them in a PDB session or a traceback.
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.
Yep, I'm on it already, running the test suite.
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.
Done. Please review the implementation.
Additionally this patch solves the orthogonal problem that specialized `QuerySet` like `ValuesQuerySet` didn't inherit from the current `QuerySet` type. This wasn't an issue until now because we didn't officially support custom `QuerySet` but it became necessary with the introduction of this new feature. Thanks aaugustin, akaariai, carljm, charettes, mjtamlyn, shaib and timgraham for the reviews.
merged in 31fadc1 |
No description provided.