On Tue, Oct 11, 2011 at 9:05 AM, Carl Meyer <c...@oddbird.net> wrote:
> -----BEGIN PGP SIGNED MESSAGE-----
> Hash: SHA1
>
> Hi all,
>
> In the spirit of making Django behave better as a Python library (c.f.
> Glyph's keynote at djangocon.us), I'd like to finally tackle removing
> the sys.path hacks in django.core.management.setup_environ. I'll give
> the full detailed rundown here on the current behavior and how I propose
> to change it. Fortunately, the fix isn't that complicated, and I think
> it's a no-brainer. For the impatient, you can skip straight to my
> proposed patch [2].
>
> The tl;dr summary is that I think with some small changes to manage.py
> and the default project layout, we can fix some very common bugs and
> deployment problems, dramatically reduce the extent to which manage.py
> is "unknown magic" for newcomers to Django, and take a significant step
> towards being "just a Python library."
>
> This issue is tracked in ticket #15372 [1], though there's a lot more
> detailed info in this message than on that ticket.
>
> The problem
> ===========
>
> Right now "django-admin.py startproject mysite" generates this:
>
>    mysite/
>        __init__.py
>        manage.py
>        settings.py
>        urls.py
>
> This layout is the root of the problem, because it commingles manage.py
> (a command-line script entry point that should not be imported) with
> importable modules inside a package.
>
> When Python runs a command-line script it puts the script's directory
> onto sys.path (even if you run "python /full/path/to/mysite/manage.py"
> from somewhere else entirely). So with this layout, via manage.py,
> "import settings" and "import urls" will both work, and if we add any
> other modules or packages to this directory, they are also importable as
> top-level modules/packages. This is a standard feature of Python.
>
> But "mysite" is a package, and Django wants us to be able to import
> "mysite.settings" and "mysite.urls" (in fact, the latter is the default
> ROOT_URLCONF setting). In order to make that possible, setup_environ
> does some magic: it finds the parent directory of "mysite", temporarily
> adds it to sys.path, imports mysite, and then reverts sys.path. Now we
> can either "import settings" or "import mysite.settings", and both will
> work. Same is true for any other modules or packages (e.g. apps) we add
> under mysite/. How nice!
>
> Not really. This bit of clever causes a boat-load of problems:
>
> 1. Python's importer doesn't know that the same module imported under
> two different names is the same module, so it imports it twice. Which
> means module-level code can run twice, which causes really confusing bugs.
>
> 2. People write code that imports things inconsistently (sometimes with
> the project prefix, sometimes without it), and then when they go to
> deploy (without manage.py or setup_environ), their imports break and
> they don't understand why. This comes up frequently in #django. In order
> to fix it they either have to make all their imports consistent, or add
> both the inner and outer directories to sys.path/PYTHONPATH in their
> deployment setup. Inconsistent imports should fail fast, not when you
> move to production.
>
> 2a. Even worse, our tutorial now encourages people to make their imports
> inconsistent! ROOT_URLCONF is "mysite.urls" but we tell people to use
> just "polls" rather than "mysite.polls" - so any tutorial-based project
> now requires the double import paths in order to even run.
>
> 3. Since it's not obvious what Django is doing, people don't always
> realize that the name of the outer directory matters, they think it's
> just a container (which is what it ought to be). If they commit the
> startproject layout at the top of a VCS repo, then check it out under a
> different name, imports break. (If they check it out under a name that
> isn't a valid Python module name, setup_environ breaks entirely).
>
> 4. The temporary addition to sys.path can cause unrelated packages
> adjacent to the project to be imported, if the import of the project
> module triggers an import of a name that happens to exist adjacent. This
> is extremely difficult to track down; since Django restores sys.path, it
> appears the adjacent package is somehow imported without even being on
> sys.path. This can catch very experienced Python devs; I once spent an
> hour on IRC working with Ned Batchelder to track this down in his project.
>
> 5. Understanding Python import paths is hard enough for beginners. The
> Django tutorial could help them on the road to understanding, with no
> loss in usability; pulling a trick like this behind the scenes helps
> them stay confused instead.
>
> The solution
> ============
>
> The key is to fix the "startproject" layout:
>
>    mysite/
>        manage.py
>        mysite/
>            __init__.py
>            settings.py
>            urls.py
>
> The outer "mysite/" is now just a container, not a Python package. It
> can be renamed without consequence. The inner "mysite/" is the Python
> package, and contains settings and urls. This means they are no longer
> importable as top-level modules; they must always be "mysite.settings"
> and "mysite.urls", which is better than having them pollute the
> top-level namespace. Python's built-in sys.path handling for scripts
> means that sys.path will be correct using manage.py as the entry-point
> (or the wsgi.py entry point that may be added shortly for #16360), and
> people not using either of those entry points only need to add one
> sys.path entry rather than two.
>
> With that simple change, setup_environ and execute_manager (in
> django.core.management) both become superfluous, and manage.py can be
> made much simpler. Since it no longer needs to muck with sys.path, its
> only remaining function is to set a default value for
> DJANGO_SETTINGS_MODULE, if it isn't already set. This also gets rid of
> the double import of the settings module that manage.py currently
> causes, which Graham Dumpleton discusses here [3]. And it's now obvious
> from inspection what exactly manage.py does, which certainly wasn't true
> before.
>
> All win so far. But what about...
>
> Backwards compatibility
> - ----------------------

Backwards compatibility is easily solved, include an upgrade.py file
which is called if project rootfolder has settings.py. upgrade.py
would fix the directory structure

> For new projects, there's no backward-compat issue. For existing
> projects that upgrade to Django 1.4, we leave setup_environ and
> execute_manager in place (so the old-style manage.py will continue to
> work as it always did), but we start them on a deprecation path.
>
> When the project decides to make the update (possibly when they read the
> 1.4 release notes, probably when they start getting loud
> DeprecationWarning in 1.5), here's what they need to do:
>
> 1. Replace the contents of manage.py with the new version (which is five
> lines long and can be included in its entirety in the release notes).
>
> 2. Make their imports consistent. This means that anything imported with
> the project-module prefix needs to live inside the project module, and
> anything imported as a top-level module (that isn't a
> separately-installed dependency) needs to live outside the project
> module next to manage.py. And, of course, a given module needs to
> consistently be imported one way or the other.
>
> I think this is a reasonable amount of work for a deprecation upgrade,
> and a good trade-off for the benefits. Really, step 2 is cleanup of
> broken code where manage.py is currently hiding its brokenness.
>
> Other issues
> ============
>
> What to do about startapp?
> - --------------------------
>
> Currently, startapp always creates the new app inside the "project
> directory", currently defined as "the directory where the settings
> module lives" (and found via a combination of baling wire and ugly
> hackery). If we want to maintain this behavior of startapp, we need to
> change the tutorial back to using "mysite.polls" in imports rather than
> "polls". I have a version of my proposed patch that does this [4].
>
> My preference, however, and what I've implemented in my preferred patch
> [2], is to change startapp so that it always creates the app in the
> current directory, rather than jumping through hoops to find the
> "project directory". This makes startapp useful for starting reusable
> apps as well as project-tied apps, and allows us to keep "polls" in the
> tutorial rather than "mysite.polls" (because if the tutorial tells the
> user to run "python manage.py startapp polls", the polls app will be
> created next to manage.py, which means it's imported as a top-level
> package).
>
> Removing the restrictions on startproject
> - -----------------------------------------
>
> My patch also removes the current restriction that "startproject" cannot
> be run if there is a DJANGO_SETTINGS_MODULE environment variable. I
> don't think there's any good reason for this restriction, and it can be
> really irritating and confusing. (This also means you can now run
> startproject via manage.py, which doesn't make a ton of sense, but also
> doesn't actually hurt anything).
>
> What's the impact on adding a standard WSGI entrypoint?
> - -------------------------------------------------------
>
> #16360 [5] has a patch to add a "wsgi.py" file to the default project,
> which will serve as a standard WSGI entrypoint that can be used both by
> runserver and by production WSGI servers. I love the idea and the patch
> - - except that its approach to easing the deployment path is to extend
> the current sys.path-hacking of setup_environ beyond just manage.py and
> into every WSGI deployment.
>
> But if we first fix the sys.path-hacking as I propose, the wsgi.py file
> will go next to manage.py in the default project layout. Since WSGI
> servers chdir() to the directory of the wsgi file you point them at, no
> sys.path additions (whether manually or magically in Django) will be
> needed in order to deploy a Django project under a production WSGI
> server. Again, win.
>
> Next steps
> ==========
>
> Unless there are significant objections, I'd like to commit this in the
> next few days and get it into 1.4. I'll also modify the patch on #16360
> to take advantage of the new layout; hopefully that can get into 1.4 as
> well.
>
> Let me know if you see something I've overlooked!
>
> Thanks,
>
> Carl
>
> [1] https://code.djangoproject.com/ticket/15372
> [2] https://github.com/django/django/pull/62
> [3] http://blog.dscpl.com.au/2010/03/improved-wsgi-script-for-use-with.html
> [4] https://github.com/carljm/django/compare/master...t15372-app-in-project
> [5] https://code.djangoproject.com/ticket/16360
> -----BEGIN PGP SIGNATURE-----
> Version: GnuPG v1.4.10 (GNU/Linux)
> Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/
>
> iEYEARECAAYFAk6TbDIACgkQ8W4rlRKtE2dhCQCgzoXxt245wB7UVkaDeeK0U7NO
> 2hsAn2Wx90fdbJte2lPfW4onyoIqbyf0
> =MUge
> -----END PGP SIGNATURE-----
>
> --
> You received this message because you are subscribed to the Google Groups 
> "Django developers" group.
> To post to this group, send email to django-developers@googlegroups.com.
> To unsubscribe from this group, send email to 
> django-developers+unsubscr...@googlegroups.com.
> For more options, visit this group at 
> http://groups.google.com/group/django-developers?hl=en.
>
>

-- 
You received this message because you are subscribed to the Google Groups 
"Django developers" group.
To post to this group, send email to django-developers@googlegroups.com.
To unsubscribe from this group, send email to 
django-developers+unsubscr...@googlegroups.com.
For more options, visit this group at 
http://groups.google.com/group/django-developers?hl=en.

Reply via email to