On 19.01.2014, at 08:56, Russell Keith-Magee <russ...@keith-magee.com> wrote:

> Hi all,
> 
> First off - this isn't critical for 1.7 alpha - I'm raising it now because 
> I've been in this space for the last couple of days as a result of the 
> working on the check framework. I suspect any changes stemming from this 
> discussion can be landed in the beta period without major problems.
> 
> Also - Aymeric - you've done a great job with this app loading work. I didn't 
> get much of a chance to follow along as it was unfolding, but the resultant 
> work has been a pleasure to work with.
> 
> With one exception :-)
> 
> We've now got AppConfig objects for each app (e.g., contrib.admin). Those 
> AppConfig objects can have ready() methods that invoke startup logic. This is 
> all great stuff that has been a long time coming.
> 
> *But*
> 
> From a backwards compatibility perspective, there seems to be a gap in the 
> way AppConfig objects are instantiated.
> 
> The gap is this - historical apps (and historical documentation) calls for 
> admin to be put into INSTALLED_APPS using 'django.contrib.admin'. However, 
> when you do this, you get the system default AppConfig, not something that is 
> admin specific. In order to get the new AdminConfig, you need to update your 
> INSTALLED_APPS to point at "django.contrib.admin.apps.AdminConfig". 
> 
> This is fine, but it limits the logic that we (as a project) can put into 
> AdminConfig.ready(), because existing projects won't reference the new 
> AdminConfig. So, cleanups like putting admin.autoregister and check framework 
> registrations into the ready function can't be done, because existing 
> projects won't automatically gain that functionality. 
> 
> So, we end up with a situation where we have a nifty new on-startup method, 
> but we can't use it for any of our own on-startup app requirements, because 
> we have no guarantees that it's going to be invoked.
> 

Hi Russ, all,

Thanks for raising this issue as it’s indeed an important one to discuss. I 
hoped we’d have time to let the community figure out some of the techniques to 
handle version compatibility to Django but alas I didn’t expect the checks 
framework to so thoroughly crash the party. Well done! :) I wish we would have 
had more time to take these points into account when we were discussing the 
various details back in December, but I guess as long as 1.7 isn’t out it’s not 
too late. I probably would have made the argument that the check system may be 
best placed in AppConfig classes (e.g. in AppConfig.check_* methods). But on 
the other hand that may have made the work on the app config classes even 
harder.

For the record, the app config concept has a long history with different 
implementations and goals but always (AFAIR) tried to fix one thing: the side 
effects of the implicit Django app concept. This issue is just another example 
of that problem. It bubbles up in our compatibility strategy which is to still 
allow dotted app module paths in INSTALLED_APPS. We assumed users to upgrade to 
the dotted app config paths in INSTALLED_APPS on their own eventually if the 
app in question required it. In other words, we really left it to the app 
authors to decide when to ask their users to make the jump. Instead of a hard 
coded check we wanted to gently push the new APIs into the community — also to 
find concept holes.

I now believe that we may have been too conservative since that strategy 
introduces a new level of uncertainty in the app API. I can see the user 
questions already: “Does the 3rd party app XYZ work on 1.7?” “Can I also 
install it on my legacy 1.6 project?” “How am I going to override some options 
without a AppConfig?” “What’s the best way to create cross version compatible 
apps?”

> I've dug through the mailing lists discussions about app-loading, and I can't 
> see anywhere that this was explicitly discussed, so I can't tell if this was 
> done deliberately, or as a side effect of the circular import problems that 
> Aymeric described.
> 
> Assuming that this wasn't done for import-related reasons, I can see a couple 
> of possible solutions:
> 
> 1) Require updating INSTALLED_APPS as a migration step. This is something we 
> can detect with a check migration command - we inspect all the strings in 
> INSTALLED_APPS; if it isn't an AppConfig subclass, look to see if there is an 
> apps module, and if so, suggest that an update may be required. 
> 
> Benefits - explicit upgrade path.
> Downside - if you ignore the warning, you potentially lose functionality. And 
> nobody ever reads the documentation :-)

+1 It’s the strictest and but also clearest migration path and I believe we 
can’t afford to fix this over time and leave our users in limbo about how 
Django apps work or what to expect from the app API. It means it’ll be harder 
for reusable app developers to explain backward compatibility to their users 
but we could offer some pointers, e.g. in blog/models.py::

    try:
        from django import apps
    except ImportError:
        from blog.utils import register_something
        register_something()

That would work for Django versions < 1.7 and implies 
blog.utils.register_something is called in the ready() method of 
blog.apps.BlogConfig, too. It’s not an exciting pattern but at least common 
enough for Python developers to require little explanation.

> 2) Use a naming convention - require that the default app configuration must 
> be called AppConfig, so we can always try to load myapp.apps.AppConfig. If 
> this object doesn't exist, install the system default AppConfig (as it does 
> currently). 
> 
> Benefits - Easy to implement and document the requirement.
> Downside - It's coding by convention; also, every app config is called 
> AppConfig, which is potentially painful for debugging.

Yeah, this is a bad idea. I love conventions, but not if they prevent me to 
paint my shed blue. -1

> 3) Do 2, but with a level of indirection. Let the user call the AppConfig 
> module whatever they want, but allow the apps module to define a 
> 'default_app_config' attribute that provides the string to use as a default 
> AppConfig. 
> 
> This is essentially halfway between 1 and 2 - it's implicitly doing the 
> INSTALLED_APPS update, using information provided by the app developer.

While I understand that it would allow us to keep using dotted module paths in 
INSTALLED_APPS, it’s also a great way to stall the adoption of app config 
classes. To be clear: it *is* the goal to eventually have all apps (contrib and 
3rd party) use app configs as the only location for startup code and app 
configuration. App config classes are intended to be user and not only app 
developer facing. We strive to encourage users to be app developers as soon as 
possible, e.g. we tell them how to write an app in the tutorial. So introducing 
a hidden configuration variable like ‘default_app_config’ makes me really 
uncomfortable with regard to the mixed message we’d be sending. We should be 
clear about what app is used. The “default app config” is the one that is 
configured in the settings.py, just like the “default database backend” is the 
one defined there. In other words it’s the users choice what is considered 
default, not ours or the app developer’s.

> 3a) As for 3, but use a flag on the AppConfig subclass that marks it as "use 
> me as the default". If there are more than one AppConfig objects in an apps 
> module that has the 'use as default" flag, raise an error.

This smells like dependency tracking and hard to explain errors. I also don’t 
see how a user could prevent 3rd party apps to not mark itself as default. -1

> 3b) As for 3, but use the first class discovered that is a subclass of 
> AppConfig. Actually identifying the "first" is a bit of an implementation 
> issue - I suppose we'd need to have a MetaClass that registered AppConfig 
> objects as they are imported. 

More implicit behavior that reminds me of how the model implementation made it 
so hard to have pluggable user models. -1 

Jannis

Attachment: signature.asc
Description: Message signed with OpenPGP using GPGMail

Reply via email to