Skip to content

Support per-model FakeQuerySet class customisation - #210

Open
ababic wants to merge 3 commits into
wagtail:mainfrom
ababic:feature/support-per-model-fakequeryset-class-customisation
Open

ababic wants to merge 3 commits into
wagtail:mainfrom
ababic:feature/support-per-model-fakequeryset-class-customisation

Conversation

@ababic

@ababic ababic commented Jun 14, 2026 •

Copy link
Copy Markdown
Contributor

Implements #209

Description

Allows registering of methods on a models manager's default QuerySet as compatible with FakeQuerySet classes via a new @fakequeryset_compatible decorator - so that they become available in draft/preview/in-memory-only contexts.

When used, assembled in-memory classes take on the name FakeModelNameQuerySet, and have the decorated methods copied over to them. The methods will then be available to use on in-memory versions of those querysets, AND fake querysets returned by parent -> child relationship access.

The generated classes are cached to avoid repeat effort, and are true FakeQuerySet subclasses, so any in-project code that does isinstance(queryset, FakeQuerySet) will continue to work with these changes in place.

The decorator supports an as_name option, which allows developers to write alternative implementations of methods on querysets specifically for in-memory usage (where they live alongside the original implementation). This is useful in cases where developers want the 'regular' querysets to continue to use the ORM implementation, but are happy to add a pure-Python version for the in-memory representation case, where the options would be:

  1. Return self to serve as a stub/no-op
  2. Recreate what the ORM version does exactly
  3. Implement a simplified / dumbed-down version of the ORM behaviour

Attempts to override methods that FakeQuerySet already implements will raise a clear warning, and the registered version of the method will be ignored.

Additional changes

It's clear from issue reports that there is a lot of confusion when developers see a bare AttributeError when trying to use custom queryset methods in a draft/preview context without an explanation of why it's occurring. So, this PR introduces QuerySetMethodOrAttributeUnavailableError to better explain why it is happening, and also proposes a solution. It's an AttributeError subclass, so any existing projects attempting to catch/handle the original exception will still work.

Design justifications

Why not just force devs to define a custom FakeQuerySet class, and register that somehow?

While this might be beneficial from a clarity / typing perspective, it comes with complication and development overhead, e.g.

  • Where should these classes live in the codebase?
  • How do I ensure the queryset and fake implementations stay in-sync over time?
  • I already have a nice class-inheritance tree for my querysets... do I need to replicate that with FakeQuerySet subclasses too? What naming patterns would make sense for that?

The simple decorator approach solves all this by:

  • Keeping everything together on existing QuerySet definitions.
  • The decorator name is clear in it's intention, and devs can use simple comment / docstring additions to reference any dual implementation pairs
  • Inheritance works as-expected alongside queryset inheritance.. no separate trees to manage.

Plus, django-modelcluster is really a 'tool choice' of Wagtail. I think forcing a full understanding of FakeQuerySet on developers and pushing them toward class replication / duplication is a bad idea generally - This isn't a common enough problem in projects to make devs completely rethink their approach to code definition. Being able to decorate existing code in-place is a low-footprint 'Get out of jail free card' - just for when you need it.

Why log a warning when method name clashes are detected instead of raising a hard exception?

This could add unnecessary friction to the upgrade process should the FakeQuerySet API be updated to support more of Django's QuerySet API in future. Release management is complicated enough without having to worry about breaking live-running projects that are currently using their own, native implementation.

Exciting thoughts

  • The decorator could be used to experiment in-project with implementations of QuerySet methods that aren't yet supported by FakeQuerySet - which could lower the barrier to sharing and eventual contribution.
  • Third-party apps that use django-modelcluster (e.g. Wagtail) can opt to register methods on their own custom queryset classes, helping to reduce ocurrances of QuerySetMethodOrAttributeUnavailableError in the first-place - all without polluting FakeQuerySets's generic API.

AI usage

Like many devs, I use AI as part of my everyday development process - but, never as a shortcut to thinking things through. In this case, OpenAI's Codex 5.3 model was used (via Cursor) for scaffolding the changes based on the issue description I put together. I then iterated on that implementation to add the method-name conflict handling and other tweaks. The idea itself, decisions on naming/messaging, inheritance-scenario tests etc were all my own - as is this PR description.

@ababic
ababic marked this pull request as draft June 14, 2026 06:44
@ababic
ababic force-pushed the feature/support-per-model-fakequeryset-class-customisation branch 2 times, most recently from 84a7454 to 3b915a5 Compare June 14, 2026 07:06
@ababic
ababic marked this pull request as ready for review June 14, 2026 07:07
@ababic
ababic force-pushed the feature/support-per-model-fakequeryset-class-customisation branch 3 times, most recently from d3bcd49 to 2968b3a Compare June 16, 2026 09:59
@ababic

ababic commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

I didn't mention above, because I assumed it was common knowledge. But, my intention here is try and make Wagtail projects a little less 'special' in terms of dev approach to other Django projects.

I consider myself to be a pretty hardcore 'Django principles' guy, and even I've avoided queryset customisation for child models because of this class of issue. Child object filtering logic usually ends up as cautiously-written methods on the parent model instead, with conditional logic to account for preview behaviour. This then sets an unhealthy precedent for the project - Less django-familiar devs then come to the project and think that's the preferred pattern for everything, and the project slowly merges away from queryset customisation, even where draft preview isn't a concern.

Ultimately, Wagtail being based on Django is one of its greatest strengths - A lot of folks love Django because of it's clear and consistant patterns, and AI generally understands these same patterns pretty well. I want to make queryset customisation the standard for all Django projects - including ones using Wagtail.

cursoragent and others added 3 commits July 20, 2026 14:46
Co-authored-by: Andy Babic <ababic@users.noreply.github.com>
Co-authored-by: Andy Babic <ababic@users.noreply.github.com>
Co-authored-by: Andy Babic <ababic@users.noreply.github.com>
@cursor
cursor Bot force-pushed the feature/support-per-model-fakequeryset-class-customisation branch from 2968b3a to 80cd62a Compare July 20, 2026 14:46

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants