Opened 3 years ago
Closed 3 years ago
#33139 closed Bug (wontfix)
Using IPython for the manage.py REPL interface clobbers __main__, where the normal python REPL doesn't.
Reported by: | Keryn Knight | Owned by: | Keryn Knight |
---|---|---|---|
Component: | Core (Management commands) | Version: | dev |
Severity: | Normal | Keywords: | ipython shell |
Cc: | Triage Stage: | Unreviewed | |
Has patch: | no | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
The only thing I can find of related note is #30588 which observes a similar thing but around auto-reloading*
Given the following file, which is ... I believe it has been called esoteric but works:
import django from django.conf import settings from django.http import HttpResponse from django.urls import path if not settings.configured: settings.configure( SECRET_KEY="?", DEBUG=True, INSTALLED_APPS=(), ROOT_URLCONF=__name__, ) django.setup() def test(request): return HttpResponse('test') urlpatterns = [ path("test/", test, name="test"), path("best/", test, name="test"), ] if __name__ == "__main__": from django.core import management management.execute_from_command_line() else: from django.core.wsgi import get_wsgi_application application = get_wsgi_application()
it is possible to reverse those url patterns, like so:
$ python whypython.py shell -ipython Python 3.9.5 ... >>> __name__ 'builtins' >>> import sys >>> sys.modules['__main__'] <module '__main__' from '/path/to/whypython.py'> >>> dir(sys.modules['__main__']) ['HttpResponse', ..., 'urlpatterns'] >>> sys.modules['builtins'] <module 'builtins' (built-in)> >>> from django.urls import reverse >>> reverse('test') '/best/' >>>
But doing the same via the IPython support:
$ python whypython.py shell -iipython Python 3.9.5 ... IPython 7.24.1 ... In [1]: __name__ Out[1]: '__main__' In [2]: import sys In [3]: sys.modules['__main__'] Out[3]: <module '__main__'> In [4]: dir(sys.modules['__main__']) Out[4]: ['In', 'Out', ... 'quit', 'sys'] In [5]: sys.modules['builtins'] Out[5]: <module 'builtins' (built-in)> In [6]: from django.urls import reverse In [7]: reverse('test') ... ImproperlyConfigured: The included URLconf '__main__' does not appear to have any patterns in it [...]
This is because django.core.management.commands.shell.Command.ipython
invokes start_python
without supplying a user_ns
, which means that down in IPython.core.interactiveshell.InteractiveShell.prepare_user_module
this runs:
if user_module is None: user_module = types.ModuleType("__main__", doc="Automatically created module for IPython interactive environment")
and then IPython.core.interactiveshell.InteractiveShell.init_sys_modules
patches that oddness in. I don't know why it does so, particularly.
The "fix" is to change the Django invocation to:
start_ipython(argv=[], user_ns={'__name__': '__ipython_main__'})
which leaves __main__
in peace, and inserts a DummyMod
with that name into the sys.modules table. As far as I can tell, both are treated by IPython the same ... just a big __dict__
to patch things into.
I suppose an argument could be made for checking if __main__
is in sys.modules
and only set the user_ns
if it is.
You'd think that supplying user_module=...
would also be an effective fix, but alas, IPython.terminal.ipapp.TerminalIPythonApp.init_shell
doesn't pass that along, only user_ns
. I don't know if it's possible to pass it along somehow because of the amount of magic in IPython (traits, observes, etc)
Thus, using user_ns
, it all works:
In [1]: __name__ Out[1]: '__ipython_main__' In [2]: import sys In [3]: sys.modules['__main__'] Out[3]: <module '__main__' from '/path/to/whypython.py'> In [4]: sys.modules[__name__] Out[4]: <IPython.core.interactiveshell.DummyMod at 0x10659da90> In [5]: dir(sys.modules['__main__']) Out[5]: ['HttpResponse', ... 'urlpatterns'] In [6]: dir(sys.modules[__name__]) ['In', 'Out', ... 'quit', 'sys'] In [7]: from django.urls import reverse In [8]: reverse('test') Out[8]: '/best/'
I've got little concrete idea about the knock-on effects on changing it -- I don't feel like there should be any, because as I mentioned, I think they're both just used as glorified dicts. But it does change the global __name__
and I don't know how to avoid that off the top of my head, I only know that I can't think of many reasons to be checking __name__
in the REPL, beyond pulling it from sys.modules
for reasons (and there I think replacing/shadowing the real one is a very weird move for IPython to have made).
For historical reference, here's the pdb where
output to get to the bit which triggers a change in behaviour:
/path/to/whypython.py(29)<module>() -> management.execute_from_command_line() /path/to/django/core/management/__init__.py(419)execute_from_command_line() -> utility.execute() /path/to/django/core/management/__init__.py(413)execute() -> self.fetch_command(subcommand).run_from_argv(self.argv) /path/to/django/core/management/base.py(363)run_from_argv() -> self.execute(*args, **cmd_options) /path/to/django/core/management/base.py(407)execute() -> output = self.handle(*args, **options) /path/to/django/core/management/commands/shell.py(112)handle() -> return getattr(self, shell)(options) /path/to/django/core/management/commands/shell.py(36)ipython() -> start_ipython(argv=[], user_ns={'__name__': '__ipython_main__'}) /path/to/site-packages/IPython/__init__.py(126)start_ipython() -> return launch_new_instance(argv=argv, **kwargs) /path/to/site-packages/traitlets/config/application.py(844)launch_instance() -> app.initialize(argv) /path/to/site-packages/traitlets/config/application.py(87)inner() -> return method(app, *args, **kwargs) /path/to/site-packages/IPython/terminal/ipapp.py(317)initialize() -> self.init_shell() /path/to/site-packages/IPython/terminal/ipapp.py(331)init_shell() -> self.shell = self.interactive_shell_class.instance(parent=self, /path/to/site-packages/traitlets/config/configurable.py(537)instance() -> inst = cls(*args, **kwargs) /path/to/site-packages/IPython/terminal/interactiveshell.py(525)__init__() -> super(TerminalInteractiveShell, self).__init__(*args, **kwargs) /path/to/site-packages/IPython/core/interactiveshell.py(647)__init__() -> self.init_create_namespaces(user_module, user_ns) /path/to/site-packages/IPython/core/interactiveshell.py(1239)init_create_namespaces() -> self.user_module, self.user_ns = self.prepare_user_module(user_module, user_ns) > /path/to/site-packages/IPython/core/interactiveshell.py(1307)prepare_user_module()
Assigning it to myself on the off-chance it is accepted.
(* and I think in the process links to the wrong start_ipython
, to no detriment. The actual one used is IPython.start_ipython
rather than IPython.testing.globalipapp.start_ipython
, whose implementation is different)
Thanks for the ticket. Closing as "wontfix" based on discussion in #19737. Feel free to write to the DevelopersMailingList if you disagree with the conclusions of that ticket.