Check for name clashes in startproject without importing modules

Hey all,

I’d like to propose a small cleanup (ticket-34778): when the startproject command checks for name clashes with other modules, it could just use importlib’s find_spec() (docs), which checks for the presence of a module without importing it. Right now the command tries to import modules and depends on an exception being raised in the absence of one.

Why?
Importing modules can execute code, e.g. dumping to the terminal or importing other further modules. (Of course, the user has trusted any hypothetical same-named modules by having installed them already, but it just seems polite to stop at checking for existence when that’s all we need for the name clash check.)

Who might this help?
My team is wrapping the startproject command to help people create instances of our software, let’s call it presto. A user might do something like use our command to create a project called presto-education. But what if they’ve installed someone else’s module called presto-education before there was a cool startpresto-way to get everything they need to use presto for education? The kinds of users we’re making this command for are not full-time Django developers. We write explicit instructions helping them accomplish their upgrades. I noticed this in code review and saw that we were just extending Django’s behavior. (I opened a ticket and Natalia helpfully suggested raising a forum post.)

The diff would look like:

diff --git a/django/core/management/templates.py b/django/core/management/templates.py
index 8c2232ee7a..ec4a4b4214 100644
--- a/django/core/management/templates.py
+++ b/django/core/management/templates.py
@@ -5,7 +5,7 @@ import posixpath
 import shutil
 import stat
 import tempfile
-from importlib import import_module
+from importlib.util import find_spec
 from urllib.request import build_opener
 
 import django
@@ -277,11 +277,7 @@ class TemplateCommand(BaseCommand):
                 )
             )
         # Check it cannot be imported.
-        try:
-            import_module(name)
-        except ImportError:
-            pass
-        else:
+        if "." in name or find_spec(name) is not None:
             raise CommandError(
                 "'{name}' conflicts with the name of an existing Python "
                 "module and cannot be used as {an} {app} {type}. Please try "

Of course, this is a judgment call. Maybe not worth bothering. We haven’t even shipped our wrapped command yet. But this came up in code review and I wanted to “offer it back upstream”.

Your thoughts welcome!

Hi @jacobtylerwalls, this seems reasonable as described. (Are there any gotchas? If not :+1:) Thanks!

Seems fair to me too.

Thanks for the quick replies!

Couldn’t come up with a gotcha. If anything, this might find more name clashes, as some existing module might raise ImportError itself and pass the existing check.

(The check for dotted paths in my diff was because the benefit of find_spec() is partially lost in that case, since it will import the parent, but the command already checks for dotted paths in the earlier part of this method, so this diff wouldn’t even need to check again.)