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)

Change History (1)

comment:1 by Mariusz Felisiak, 3 years ago

Resolution: wontfix
Status: assignedclosed

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.

Note: See TracTickets for help on using tickets.
Back to Top