Skip to content
/ django Public

Fixed #30427, Fixed #16176 -- Corrected setting descriptor in Field.contribute_to_class().#11337

Closed
jarekwg wants to merge 4 commits intodjango:mainfrom
jarekwg:ticket_30427
Closed

Fixed #30427, Fixed #16176 -- Corrected setting descriptor in Field.contribute_to_class().#11337
jarekwg wants to merge 4 commits intodjango:mainfrom
jarekwg:ticket_30427

Conversation

@jarekwg
Copy link
Copy Markdown
Contributor

@jarekwg jarekwg commented May 7, 2019

Ticket #30427

Previously: DeferredAttributes would not get stapled onto models where the model (or an ancestor) already had an existing truthy attribute (despite comment only mentioning protecting against overriding classmethods).
With this change: DeferredAttributes will not get stapled onto models where the model (or an ancestor) already has a callable attribute always get stapled on, regardless of other attributes on the models or their ancestors.

This means we have the following changes:

  • If a model field is declared twice (once on the class and once on the ancestor), mro order is respected when deciding which should take precedence.
  • If a model field is declared over a class attribute on the ancestor, the model field will override.

Aside from the change, this PR includes:

  • Failing tests. A failing test that this change fixes.
  • An update to an existing test, covering the new overriding order of DeferredAttributes. Arguably, what was previously being tested for didn't make sense.

Problems:

  • Albeit obscure, this introduces a slight change in behaviour, so we might have to somehow communicate that..
  • @property decorators throw a fly in the ointment. They look like methods, but the decorator makes callable return False for them. It's why the testsuite is failing presently. We can add an additional check for the existence of __isabstractmethod__ on the attribute, but it starts to feel rabbitholey. Perhaps we should think about a more aggressive change here, such as always overriding everything. Why do we protect classmethods anyway?

Happy to discuss ideas on how to solve this here or on the ticket.

[2020-03-04] edit: updated to reflect most recent decisions/discussion.

Copy link
Copy Markdown
Member

@carltongibson carltongibson left a comment

Choose a reason for hiding this comment

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

Hi @jarekwg. Thanks for this.

Mailing list discussion: https://groups.google.com/d/topic/django-developers/zXB0oJ8tD3E/discussion

The general approach seems... OK. 🙂

I've pinged your thread to ask for input. Why is it as it is? (What is this "class method with the same name as a field" use-case?)

@mxschmitt
Copy link
Copy Markdown

@jarekwg @carltongibson

Any updates related to this issue? As far as I see has Jarek updated his PR.

jazzband/django-model-utils#382 is e.g. waiting for a fix.

Thanks!

@carltongibson carltongibson self-assigned this Oct 31, 2019
Copy link
Copy Markdown
Member

@carltongibson carltongibson left a comment

Choose a reason for hiding this comment

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

Hi @jarekwg.

I'm still trying to get to the very bottom of this. I think that's quite a long way down. 🙂

I'm toying with the idea that we need to always set the descriptor for fields, so:

        if self.column:
            setattr(cls, self.attname, self.descriptor_class(self))

We already advertise that Field inheritance does not work (cannot work) exactly
like Python inheritance
. This would be more of the same.

It was a possibility @carljm discussed on ticket-16176:

[The] suggestion to set all fields as class attributes so they override descriptors from a parent class is certainly worth considering, as it would make models behave more like regular Python classes. This could result in some backwards-incompatibility; would need to look into it more to get a clearer sense of what cases might cause trouble, but I think it's probably acceptable if documented in the release notes.

Making that change, only three existing tests break. The one we're discussing here, plus a couple related to checks that can be adjusted. (In particular there's nothing I can find for the Why? of not overriding existing attributes.)

If you have capacity to work on this, can I ask you to add test cases for:

Question: are these all resolved by changing the behaviour here, without breaking other tests? (If not, which one's not?) If we could show that then we could feasibly think about whether the MRO change (which only affects the diamond-like case, I think) would be acceptable.

I'll mark this as Patch needs improvement on the ticket, but if you can keep working on this I'd be interested to help you, since it would be nice to resolve if we can. Thanks.

@jarekwg
Copy link
Copy Markdown
Contributor Author

jarekwg commented Jan 23, 2020

Awesome. Thanks for the detailed review!

  • I agree that always setting the descriptor is arguably a better way to go. I believe additional tests may fail as a result, and obviously the change may affect more users too, but I'm really not a fan of the is_attname_settable method I've currently got in this PR when we don't have a solid case for why we desire such specific behaviour.
  • I will do a bit of my own investigation into with why mro is affected by this; the metaclasses probably have a hand in it. If the rabbithole gets too deep, it's nice you're saying we might be happy to concede that we won't support pure python MRO. The "diamond" case is kind of unusual anyway so usage of that would be pretty low.
  • I'll read through the other tickets you've linked and see if I this change helps them; if yes, will add tests.
  • Yes, I'm definitely interested in seeing this to completion, and your guidance will certainly help. Give me a few weeks to come back with the next iteration, and we'll discuss more then.

Cheers,

@jarekwg
Copy link
Copy Markdown
Contributor Author

jarekwg commented Feb 20, 2020

> from django.contrib.auth.models import AbstractUser

# Prior to change in this PR:
> AbstractUser.is_active
True  # True is defined on AbstractBaseUser and was preventing AbstractUser from overriding it.

# After the change in this PR:
> AbstractUser.is_active
<django.db.models.query_utils.DeferredAttribute object at 0x7fa1d01a1b20>
  • ticket-16176 -- covered in test_shadow_parent_property_with_field
  • ticket-28198 -- arguably already covered by test_shadow_parent_attribute_with_field ensuring we've got an instance of DeferredAttribute, but I've added ShadowDeferTests to defer/test.py to cover more explicitly.
  • ticket-27807 -- not relevant I think. username_validator does not come through the contribute_to_class method. I'd assume it inherits just fine, but the point at which the field is created happens prior to any inheritance behaviour. I've added a test for this as well (for your reference), though I'd be inclined to back it out and have it addressed separate to this work.

I've stuck to the wording 'shadow', since that's what was in there already, though i think shadowing refers more to cross-scope variable overrides, and might not entirely be correct in this case.. lemme know if you think it should be changed.

As for the diamond case, I can see what's going on.
This is the line that adds fields for abstract base models: https://github.com/jarekwg/django/blame/ticket_30427/django/db/models/base.py#L274
We iterate over them in mro order, however when we ask them what fields they have to offer, we don't ask them if the fields were defined explicitly on them, or on one of their ancestors.
So in this case, DescendantOne comes in as the first abstract base, and even though it doesn't define name directly, it inherits it from AbstractBaseTwo, basically pulling it up and causing it to get looked at before the name defined directly on DescendantTwo. Once name is looked at once, all future abstract bases that try to bring it in are ignored.
The fix would be to have abstract bases only add_to_class fields that they define explicitly (not inherit from a parent), however this is actually nontrivial to check for (I believe). If we want to fix this, we'll have to get a little crafty, and I'm not sure it's worth the effort. Lemme know what you think!
Alternatively, we don't iterate over the base classes at all and only staple on fields at the top level (where python would've already ensured correct mro behaviour), however that's a much more drastic change/simplification, and there must be reasons it's been done the way it is now.

@carltongibson
Copy link
Copy Markdown
Member

Hi @jarekwg. Good stuff. I had a quick look and rebased and squashed.

...I'd be inclined to back it out ...

OK, do that. I agree. I was just being optimistic I think 🙂

I need to go through your comments and the failures one by one to determine if they're acceptable to change.

@carltongibson carltongibson self-requested a review February 20, 2020 10:10
@jarekwg
Copy link
Copy Markdown
Contributor Author

jarekwg commented Feb 20, 2020

Removed username_validator tests. Left a diff for them on https://code.djangoproject.com/ticket/27807#comment:15

Leaving the other failing tests as they are until you get a look at them first.
I'd say:

  • remove check_framework.tests.CheckFrameworkReservedNamesTests.test_model_check_method_not_shadowed
  • remove invalid_models_tests.test_models.OtherModelTests.test_property_and_related_field_accessor_clash
  • mark as expected failure model_inheritance.test_abstract_inheritance.AbstractInheritanceTests.test_multiple_parents_mro

@carltongibson
Copy link
Copy Markdown
Member

carltongibson commented Feb 21, 2020

OK. Great stuff @jarekwg. I’ll look at this again next week now.

But we’ll definitely need a release note for the breaking change.

Then we’ll need to approach the list and ask for extra feedback. I think the bug we’re fixing (the User case being the clearest in my mind) is probably worth it but it’s not something I’d take in without consultation first. (So, release notes, my review, edit tests, mailing list...)

Copy link
Copy Markdown
Member

@carltongibson carltongibson left a comment

Choose a reason for hiding this comment

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

Hi @jarekwg.

I added release notes, adjusted the tests and rebased on latest master.

Mailing list post asking for comments on the proposed change here:
https://groups.google.com/d/topic/django-developers/BNXorfBeNSk/discussion
It's quite long. :)

Let's see what folks have to say.
Thanks for you work here. It feels like we've been scratching at this for a long time now.

@carltongibson carltongibson changed the title Fixed #30427 - Allow descriptors to override non-method attrs Fixed #30427 -- Corrected setting descriptor in Field.contribute_to_class(). Mar 4, 2020
@carltongibson carltongibson requested a review from felixxm March 4, 2020 11:35
Copy link
Copy Markdown
Member

@felixxm felixxm left a comment

Choose a reason for hiding this comment

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

Thanks both 🚀 Great detective job 🕵️‍♂️ 🕵️‍♀️

We should mention ticket-16176 in the commit message, e.g. "Fixed #30427, Fixed #16176 -- Corrected setting descriptor in Field.contribute_to_class().". Also we can mark ticket-28198 as a duplicate.

@felixxm felixxm changed the title Fixed #30427 -- Corrected setting descriptor in Field.contribute_to_class(). Fixed #30427, Fixed #16176 -- Corrected setting descriptor in Field.contribute_to_class(). Mar 12, 2020
Copy link
Copy Markdown
Contributor Author

@jarekwg jarekwg left a comment

Choose a reason for hiding this comment

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

Thanks for review, will update shortly.

@jarekwg
Copy link
Copy Markdown
Contributor Author

jarekwg commented Mar 12, 2020

Let me know if you'd rather have the commit squashed into previous. I'm leaving it separate to make it more obvious where I've made changes.

e: spellcheck doesn't approve of "truthy"! :o

@carltongibson
Copy link
Copy Markdown
Member

Hey @jarekwg. Thanks for picking this up. Please do leave separate commits for now. (Exactly makes it easier to review.)

@carltongibson
Copy link
Copy Markdown
Member

Right, "truthy" is a word, so I added it to the spelling wordlist.

I added an example to the release notes. It's a bit long but I don't see that it can be much shorter and still get the point across? (@felixxm @adamchainz?)

If we're happy, I think we're there... 🤞

Thanks for your work on this one @jarekwg. It's a monster. 👹

Copy link
Copy Markdown
Member

@felixxm felixxm left a comment

Choose a reason for hiding this comment

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

Thanks both 🚀

@adamchainz
Copy link
Copy Markdown
Member

It's a bit long but I don't see that it can be much shorter and still get the point across? (@felixxm @adamchainz?)

I can't see anything to cut out. I'd even consider adding a sentence that makemigrations will detect changes if they exist (they will right?).

Copy link
Copy Markdown
Member

@carltongibson carltongibson left a comment

Choose a reason for hiding this comment

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

So after 4bbe826 this looks much less controversial: there's no effective change in behaviour (bar the two checks tests adjustments). It's just a fix.

Still a bit 😬, because it's contribute_to_class() but, I think it should be good to go.

@felixxm @adamchainz I'd appreciate that extra look-over. Thanks :)

@carltongibson
Copy link
Copy Markdown
Member

carltongibson commented Apr 9, 2020

Ah, didn't actually run against the new test from 4bbe826 ... Let me work out what it did change here.

(Sigh 🙂)

@carltongibson
Copy link
Copy Markdown
Member

carltongibson commented Apr 9, 2020

Right, this is the pull request that never dies.

Putting the old test back in, multiply inheriting fields is enabled by this change.

The test added in 4bbe826 fails, and the old test needs adjusting, as we had before, since normal Python MRO is not respected.

@jarekwg Do you have capacity to play with this and work out exactly what the ordering is etc?

  • This is the relevant bit of ModelBase.__new__() It's looking at new_class._meta.local_fields at each point there and making sure it's sane. (I'll try and fire up the debugger for that again, but it keeps eating the time I allocate for it...)
  • Adjusting the failing test here.
  • (Re-)Adding a diamond-like example, like the old test, and adjusting that as we had it, with the comment
  • Then documenting exactly what we can say about the behaviour. (Docs & release notes.)

On master you can't multiply inherit a field (you've never been able to) so we're not breaking anything (AFAICS). We should be able to get a fix in (the new test cases are compelling). If not we should have a definite idea of what the blocker is.

@jarekwg
Copy link
Copy Markdown
Contributor Author

jarekwg commented Apr 9, 2020

@carltongibson sure!
Just to clarify, what further insights (beyond what I summarised here) are you after when you ask:

... work out exactly what the ordering is etc?

Resummarising my previous comment in short: abstract base classes are traversed in mro() order, but the way we then add fields from them onto the new class is reckless -- we don't check whether the fields are defined directly on the classes we're traversing, or on one of their ancestors.

I'll:

  • address the new failing test from master.
  • re-add the diamond test.
  • have another read of the ModelBase.__new__() code to see if we can get diamond inheritance working without making any scary changes to this part of the code.
  • update docs accordingly, depending on the changes.

Will try to have a crack this long easter weekend.

@carltongibson
Copy link
Copy Markdown
Member

Hi Jared.

I want to be 100% clear on the state of the model at each point.

The old test looked like it was working. Except whilst it had a single Deferred descriptor, there were two identically named fields added (to local_fields) which is why, when we added the model check() call the error was raised.

It wasn’t really the case that the fields were overridden in the sense of normal inheritance. Rather when you fetch the field you’d get the one from the base class, not the DescendantTwo.

With the change, I want to make sure that the resultant model is sane. Maybe an added assert or two on the state of the _meta object makes sense.

(As I say, I want to fire it up in the debugger myself as well.)

This is such a central part of the framework that we must tread very carefully.

Unless we have no option, I don’t think further changes to ModelBase.__new__() is a good idea here.

@jarekwg
Copy link
Copy Markdown
Contributor Author

jarekwg commented Apr 13, 2020

With my latest read-through of ModelBase.__new__(), I just noticed that it doesn't even do a full mro traversal of ancestors as i'd initially thought; it only looks at direct ancestors due to the base not in parents condition here:

if base not in parents or not hasattr(base, '_meta'):

So there really is absolutely no way of supporting diamond inheritance in the current state, because even if we did tell DescendantOne to only provide fields that were directly defined on it, we'd never end up looping through to its ancestors (ie AbstractBase) to find the other fields..

It desperately needs a rewrite; dancing around it is just going to continue causing pain.
But for that to happen safely, we need beefier tests.

Soooo, I went a little more aggressive on these abstract inheritance tests, adding some utils for quick class creation, then applying a more rigorous covering of test cases. The PRO of this approach is we can write a tonne of complex inheritance cases in a readable way. The CON is a pattern like this could itself potentially introduce issues, or be a little harder to maintain. Please let me know if I've gone too far and I can revert the changes back to an expanded form. Please also let me know what you think of this suite of test cases; whether they're all sensible, or somewhy superfluous.

Also, what do you think about playing things out this way:

  • We hold off with this PR (again), and introduce a new check, specifically for any instance of diamond inheritance (regardless of what kind of field definitions exist). If identified, slap user on wrist and tell them Django doesn't support this. If we give them a firm NO now, then we'll have grounds to fix it in future without worrying about causing further regressions.
  • We merge this PR. Failing test for diamond inheritance will be a sleeper.
  • One day, ModelBase.__new__() will get rewritten, at which point we can remove the check and the @expectedfailure off the test.

I made a commit changing tests only, cause would like to know what you think; but happy to update docs too.

re the sanity you were referring to, I had a quick look with:

                # Add fields from abstract base class if it wasn't overridden.
                print(base._meta)
                print(parent_fields)
                for field in parent_fields:
                    print(new_class.__dict__)
                    if (field.name not in field_names and
                            field.name not in new_class.__dict__ and
                            field.name not in inherited_attributes):
                        new_field = copy.deepcopy(field)
                        print(f'BEFORE: {new_class._meta.local_fields}')
                        new_class.add_to_class(field.name, new_field)
                        print(f'AFTER: {new_class._meta.local_fields}')

new_class._meta.local_fields appears to have the new field cleanly added, once only, thanks to the field.name not in new_class.__dict__ check. Here's some sample output from running the above against one of my new tests:

--------------------------------------------------
DerivedFromBases12NO
model_inheritance.abstractbase1
[<django.db.models.fields.CharField: foo>]
{'__module__': 'model_inheritance.test_abstract_inheritance', '__doc__': None, '_meta': <Options for DerivedFromBases12NO>, 'DoesNotExist': <class 'model_inheritance.test_abstract_inheritance.DerivedFromBases12NO.DoesNotExist'>, 'MultipleObjectsReturned': <class 'model_inheritance.test_abstract_inheritance.DerivedFromBases12NO.MultipleObjectsReturned'>}
BEFORE: []
AFTER: [<django.db.models.fields.CharField: foo>]
model_inheritance.abstractbase2
[<django.db.models.fields.CharField: foo>]
{'__module__': 'model_inheritance.test_abstract_inheritance', '__doc__': None, '_meta': <Options for DerivedFromBases12NO>, 'DoesNotExist': <class 'model_inheritance.test_abstract_inheritance.DerivedFromBases12NO.DoesNotExist'>, 'MultipleObjectsReturned': <class 'model_inheritance.test_abstract_inheritance.DerivedFromBases12NO.MultipleObjectsReturned'>, 'foo': <django.db.models.query_utils.DeferredAttribute object at 0x7f85f00f8e50>}
--------------------------------------------------

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Appreciate what you've done with the tests but I think they're harder to comprehend now. As a Django dev I can jump in and read model classes, but the helpers are obscuring things a bit to me.

Also you now have multiple tests per test method now - ideally there should just be a handful of classes per test and some assertions. I guess you can split based on overriding, non-overriding, etc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks for taking a look!

Yeah it is more obscure, I was just hoping to find a way to avoid the verbosity of


Meta:
    abstract = True

over and over again.

Happy to revert, I guess the exercise helped list out several additional cases that weren't being covered before, like making sure each level pulls through from the previous in the absence of overrides.
I agree that test methods should hold minimal assertions that tie more directly to one specific use case, but I found it hard to break these down into multiple methods without having even more repetition/verbosity. Splitting on overriding/non-overriding still fans out a fair amount (if we care for each branch of the fan).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Also, question: Is there a pattern for any kind of "short-form" class creation in the django test suite currently? I had a quick look but couldn't find anything, probs due to the "readability" argument.
Could be a worthwhile exercise to implement a standard for "quickbuild" setup functions for model classes -- would be more understandable if such things were used across the board, and much easier to scan than tonnes of vertical scrolling.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm pretty sure you can make the abstract bases once in the test case class level:

class ...Tests(...):
    class SomeBase(models.Model):
         ...

Then reuse them appropriately in the individual test methods, which should only need to create the "bottom" classes?

I grepped for \btype\( and found a few uses, but none are for "quick" building.

I personally think the "tonnes of vertical scrolling" is a small concern compared to having to grok how type() works.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm pretty sure you can make the abstract bases once in the test case class level:

class ...Tests(...):
    class SomeBase(models.Model):
         ...

Then reuse them appropriately in the individual test methods, which should only need to create the "bottom" classes?

Ok, I like that. Wasn't aware you could declare there without having isolation or referencing issues, but had a quick play and looks pretty solid.

I grepped for \btype\( and found a few uses, but none are for "quick" building.

I personally think the "tonnes of vertical scrolling" is a small concern compared to having to grok how type() works.

Well the tradeoff is you learn the extra bit of lingo (don't necessarily have to read up on type; docstring on util function gives arguably sufficient blackbox-level insight) to then be able to sweep through a whole bunch of class definitions quickly. Having them defined so close together allows for easy comparison between them, as well as between their usage across tests.
I think if someone were to visit this file in the future to work on a single test, then having everything splayed out as normal django class definitions is much more accessible. However, when looking at the tests holistically (to identify all the cases they're covering and how), a compact perspective can be useful.
I've seen a lot of django tests where multiple contributors have added test cases to the same suite, with approaches that are all quite different, with high potential for covering the same functionality multiple times. Seeing more neighbouring tests on the screen when writing one's own can lead to more consistency, both in test layout, and in ensuring the tests all work together to provide maximal non-overlapping coverage.
So I think there's at least some merit to it. But I understand trying to forge a new pattern on a random PR probs serves only to detract from what the PR's discussion should actually be about.

Thanks for having a look anyway, I'll expand it out again after Carlton drops his thoughts on some of the other stuff I mentioned in my main comment (blocking diamond inheritance altogether).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we should block users from building diamond cases at all, rather than testing for the converse.

But we don't block, right? It's not normally worth leaving comments about things Django "should" do.

Also I think this test can be split into a few different diamond cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

we should block users from building diamond cases at all, rather than testing for the converse.

But we don't block, right? It's not normally worth leaving comments about things Django "should" do.

Yeah fair. I'll update this comment once we decide whether we go ahead and address the should bit prior to merging this PR.

Also I think this test can be split into a few different diamond cases.

There are 4 cases (A=Abstract, *=defines field). I think all are worth checking, based on :

  A*     A*     A*     A*
 / \    / \    / \    / \ 
A*  A  A*  A  A   A* A   A*
 \ /    \ /    \ /    \ /  
  D*     D      D*     D

So four separate test cases for diamond inheritance? Feels very verbose. Possibly no need to test in cases where Derived overrides with its own value since the earlier multi inheritance test already covers this, but given the complexity of the underlying metaclass logic, might be worth keeping still?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it's fine. Separate test cases are normally a good thing since they can fail individually.

Copy link
Copy Markdown
Member

@carltongibson carltongibson left a comment

Choose a reason for hiding this comment

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

Hi @jarekwg — You still with me on this one? 🙂

Sorry for the slow follow-up: in the first-place, 2020, but it's taken me a while to have a window to sit down with this again, and to get to stare at ModelBase.__new__() for sufficiently long. 😳

With the proposed change if you do not override the field yourself the a copy of the field from the first base that declares it will be added, via the adjustment in contribute_to_class().

The flow is ModelBase.__new__() -> .add_to_class() -> Field.contribute_to_class().

The first two steps are unchanged. They were always called. It's only that contribute_to_class() would previously filter the field. (This was the Don't override classmethods with the descriptor that we never found a cause for.)

The change in behaviour is just two-fold.

  1. Checks which would previously have ruled out a model now pass.
  2. The diamond-like MRO order is changed but checks (if run) would have previously failed on such a model.

On master, calling check() for the models defined in the test_multi_parent_diamond case:

[<Error: level=40, msg="The field 'foo' clashes with the field 'foo' from model 'model_inheritance.derivedfromdescendantsnofirstno'.", hint=None, obj=<django.db.models.fields.CharField: foo>, id='models.E006'>]
[<Error: level=40, msg="The field 'foo' clashes with the field 'foo' from model 'model_inheritance.derivedfromdescendantsofirstno'.", hint=None, obj=<django.db.models.fields.CharField: foo>, id='models.E006'>]

With the change, those checks don't fail, so this kind of inheritance would be allowed now.

There's the example that doesn't respect Python MRO:

self.assertCorrectInheritance('DerivedFromDescendantsNOFirstNO', 40)  # just this one that fails.

The reason is it's not really MRO: a copy of the field is put onto the child class, so bases aren't consulted.

I continue to think that what's going on here is compatible with what we say elsewhere about model inheritance. (i.e. that it's not really like Python inheritance) and that the cases this fixes justify making the change. People are really hitting those cases, whereas the MRO example wouldn't currently make it past the check framework, so you'd have to be going some to run into it.

Code it written once and read many times: @jarekwg could you rewrite the tests to explicitly define the classes, as per discussion with @adamchainz? — It's a bit more verbose but much easier to follow in the end. (I imagine someone opening these tests in 6 years time: it would be good if they spelled out the situation at the first pass.)

Make sense?

Thanks for your effort here.

Comment based on 724d1cb9bada7adea5248bce14bdda29588ebe55 (in case that gets lost).

@jarekwg
Copy link
Copy Markdown
Contributor Author

jarekwg commented Jul 23, 2020

Sorry for the slow follow-up: in the first-place, 2020, but it's taken me a while to have a window to sit down with this again, and to get to stare at ModelBase.__new__() for sufficiently long. 😳

Heh, no worries. Thanks for circling back round to this.

I'll need to refresh my thoughts a bit again, but if you're saying that diamond-inheritance-wrist-slapping already occurs currently (not sure how i failed to notice this..), then we're good to go with pushing out this change. I'll try to find some time to expand out the tests this weekend.

Cheers,

@carltongibson
Copy link
Copy Markdown
Member

The key point is that such a model fails the check() call. (The old test didn't actually call check() so it was never caught...)

@jarekwg
Copy link
Copy Markdown
Contributor Author

jarekwg commented Jul 23, 2020

The key point is that such a model fails the check() call. (The old test didn't actually call check() so it was never caught...)

Ahh, ok that makes more sense. Thanks!

Copy link
Copy Markdown
Contributor Author

@jarekwg jarekwg left a comment

Choose a reason for hiding this comment

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

Ok, I've rebased the branch, and expanded out the tests.
Let me know if there's anything else!

Comment on lines +134 to +136
NOTE: We need a rewrite of `ModelBase.__new__()` to support this properly.
Until that happens, one of the tests here is marked as expected failure. Startup checks will
attempt to prevent users from building these cases anyway.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@carltongibson I've updated this note in response to @adamchainz' feedback, as well given what you said about the diamond check above. I think it's correct now and useful (?) to explain why we've marked one of the tests with @expectedfailure, but can remove it altogether if it still feels more like a ticket that's been shoved into the code.

@carltongibson carltongibson self-requested a review July 28, 2020 10:04
@aidanlister
Copy link
Copy Markdown

Great work on this @jarekwg ... @carltongibson how's it looking now?

@carltongibson
Copy link
Copy Markdown
Member

@aidanlister it's just waiting for another chance to review again. It's on my long list for 3.2.

@carltongibson
Copy link
Copy Markdown
Member

Closing in favour of #14508. Thanks @jarekwg!

@jarekwg
Copy link
Copy Markdown
Contributor Author

jarekwg commented Jun 9, 2021

No worries! I'm just happy to see it get addresed.

@jarekwg jarekwg closed this Jun 9, 2021
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.

6 participants