shell_plus.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563
  1. # -*- coding: utf-8 -*-
  2. import inspect
  3. import os
  4. import sys
  5. import traceback
  6. from django.conf import settings
  7. from django.core.management.base import BaseCommand, CommandError
  8. from django.utils.datastructures import OrderedSet
  9. from django_extensions.management.shells import import_objects
  10. from django_extensions.management.utils import signalcommand
  11. from django_extensions.management.debug_cursor import monkey_patch_cursordebugwrapper
  12. def use_vi_mode():
  13. editor = os.environ.get('EDITOR')
  14. if not editor:
  15. return False
  16. editor = os.path.basename(editor)
  17. return editor.startswith('vi') or editor.endswith('vim')
  18. def shell_runner(flags, name, help=None):
  19. """
  20. Decorates methods with information about the application they are starting
  21. :param flags: The flags used to start this runner via the ArgumentParser.
  22. :param name: The name of this runner for the help text for the ArgumentParser.
  23. :param help: The optional help for the ArgumentParser if the dynamically generated help is not sufficient.
  24. """
  25. def decorator(fn):
  26. fn.runner_flags = flags
  27. fn.runner_name = name
  28. fn.runner_help = help
  29. return fn
  30. return decorator
  31. class Command(BaseCommand):
  32. help = "Like the 'shell' command but autoloads the models of all installed Django apps."
  33. extra_args = None
  34. tests_mode = False
  35. def __init__(self):
  36. super().__init__()
  37. self.runners = [member for name, member in inspect.getmembers(self)
  38. if hasattr(member, 'runner_flags')]
  39. def add_arguments(self, parser):
  40. super().add_arguments(parser)
  41. group = parser.add_mutually_exclusive_group()
  42. for runner in self.runners:
  43. if runner.runner_help:
  44. help = runner.runner_help
  45. else:
  46. help = 'Tells Django to use %s.' % runner.runner_name
  47. group.add_argument(
  48. *runner.runner_flags, action='store_const', dest='runner', const=runner, help=help)
  49. parser.add_argument(
  50. '--connection-file', action='store', dest='connection_file',
  51. help='Specifies the connection file to use if using the --kernel option'
  52. )
  53. parser.add_argument(
  54. '--no-startup', action='store_true', dest='no_startup',
  55. default=False,
  56. help='When using plain Python, ignore the PYTHONSTARTUP environment variable and ~/.pythonrc.py script.'
  57. )
  58. parser.add_argument(
  59. '--use-pythonrc', action='store_true', dest='use_pythonrc',
  60. default=False,
  61. help='When using plain Python, load the PYTHONSTARTUP environment variable and ~/.pythonrc.py script.'
  62. )
  63. parser.add_argument(
  64. '--print-sql', action='store_true',
  65. default=False,
  66. help="Print SQL queries as they're executed"
  67. )
  68. parser.add_argument(
  69. '--print-sql-location', action='store_true',
  70. default=False,
  71. help="Show location in code where SQL query generated from"
  72. )
  73. parser.add_argument(
  74. '--dont-load', action='append', dest='dont_load', default=[],
  75. help='Ignore autoloading of some apps/models. Can be used several times.'
  76. )
  77. parser.add_argument(
  78. '--quiet-load', action='store_true',
  79. default=False,
  80. dest='quiet_load', help='Do not display loaded models messages'
  81. )
  82. parser.add_argument(
  83. '--vi', action='store_true', default=use_vi_mode(), dest='vi_mode',
  84. help='Load Vi key bindings (for --ptpython and --ptipython)'
  85. )
  86. parser.add_argument(
  87. '--no-browser', action='store_true',
  88. default=False,
  89. dest='no_browser',
  90. help='Don\'t open the notebook in a browser after startup.'
  91. )
  92. parser.add_argument(
  93. '-c', '--command',
  94. help='Instead of opening an interactive shell, run a command as Django and exit.',
  95. )
  96. def run_from_argv(self, argv):
  97. if '--' in argv[2:]:
  98. idx = argv.index('--')
  99. self.extra_args = argv[idx + 1:]
  100. argv = argv[:idx]
  101. return super().run_from_argv(argv)
  102. def get_ipython_arguments(self, options):
  103. ipython_args = 'IPYTHON_ARGUMENTS'
  104. arguments = getattr(settings, ipython_args, [])
  105. if not arguments:
  106. arguments = os.environ.get(ipython_args, '').split()
  107. return arguments
  108. def get_notebook_arguments(self, options):
  109. notebook_args = 'NOTEBOOK_ARGUMENTS'
  110. arguments = getattr(settings, notebook_args, [])
  111. if not arguments:
  112. arguments = os.environ.get(notebook_args, '').split()
  113. return arguments
  114. def get_imported_objects(self, options):
  115. imported_objects = import_objects(options, self.style)
  116. if self.tests_mode:
  117. # save imported objects so we can run tests against it later
  118. self.tests_imported_objects = imported_objects
  119. return imported_objects
  120. @shell_runner(flags=['--kernel'], name='IPython Kernel')
  121. def get_kernel(self, options):
  122. try:
  123. from IPython import release
  124. if release.version_info[0] < 2:
  125. print(self.style.ERROR("--kernel requires at least IPython version 2.0"))
  126. return
  127. from IPython import start_kernel
  128. except ImportError:
  129. return traceback.format_exc()
  130. def run_kernel():
  131. imported_objects = self.get_imported_objects(options)
  132. kwargs = dict(
  133. argv=[],
  134. user_ns=imported_objects,
  135. )
  136. connection_file = options['connection_file']
  137. if connection_file:
  138. kwargs['connection_file'] = connection_file
  139. start_kernel(**kwargs)
  140. return run_kernel
  141. def load_base_kernel_spec(self, app):
  142. """Finds and returns the base Python kernelspec to extend from."""
  143. ksm = app.kernel_spec_manager
  144. try_spec_names = getattr(settings, 'NOTEBOOK_KERNEL_SPEC_NAMES', [
  145. 'python3',
  146. 'python',
  147. ])
  148. if isinstance(try_spec_names, str):
  149. try_spec_names = [try_spec_names]
  150. ks = None
  151. for spec_name in try_spec_names:
  152. try:
  153. ks = ksm.get_kernel_spec(spec_name)
  154. break
  155. except Exception:
  156. continue
  157. if not ks:
  158. raise CommandError("No notebook (Python) kernel specs found. Tried %r" % try_spec_names)
  159. return ks
  160. def generate_kernel_specs(self, app, ipython_arguments):
  161. """Generate an IPython >= 3.0 kernelspec that loads django extensions"""
  162. ks = self.load_base_kernel_spec(app)
  163. ks.argv.extend(ipython_arguments)
  164. ks.display_name = getattr(settings, 'IPYTHON_KERNEL_DISPLAY_NAME', "Django Shell-Plus")
  165. manage_py_dir, manage_py = os.path.split(os.path.realpath(sys.argv[0]))
  166. if manage_py == 'manage.py' and os.path.isdir(manage_py_dir):
  167. pythonpath = ks.env.get('PYTHONPATH', os.environ.get('PYTHONPATH', ''))
  168. pythonpath = pythonpath.split(os.pathsep)
  169. if manage_py_dir not in pythonpath:
  170. pythonpath.append(manage_py_dir)
  171. ks.env['PYTHONPATH'] = os.pathsep.join(filter(None, pythonpath))
  172. return {'django_extensions': ks}
  173. def run_notebookapp(self, app, options, use_kernel_specs=True):
  174. no_browser = options['no_browser']
  175. if self.extra_args:
  176. # if another '--' is found split the arguments notebook, ipython
  177. if '--' in self.extra_args:
  178. idx = self.extra_args.index('--')
  179. notebook_arguments = self.extra_args[:idx]
  180. ipython_arguments = self.extra_args[idx + 1:]
  181. # otherwise pass the arguments to the notebook
  182. else:
  183. notebook_arguments = self.extra_args
  184. ipython_arguments = []
  185. else:
  186. notebook_arguments = self.get_notebook_arguments(options)
  187. ipython_arguments = self.get_ipython_arguments(options)
  188. # Treat IPYTHON_ARGUMENTS from settings
  189. if 'django_extensions.management.notebook_extension' not in ipython_arguments:
  190. ipython_arguments.extend(['--ext', 'django_extensions.management.notebook_extension'])
  191. # Treat NOTEBOOK_ARGUMENTS from settings
  192. if no_browser and '--no-browser' not in notebook_arguments:
  193. notebook_arguments.append('--no-browser')
  194. if '--notebook-dir' not in notebook_arguments and not any(e.startswith('--notebook-dir=') for e in notebook_arguments):
  195. notebook_arguments.extend(['--notebook-dir', '.'])
  196. # IPython < 3 passes through kernel args from notebook CLI
  197. if not use_kernel_specs:
  198. notebook_arguments.extend(ipython_arguments)
  199. app.initialize(notebook_arguments)
  200. # IPython >= 3 uses kernelspecs to specify kernel CLI args
  201. if use_kernel_specs:
  202. ksm = app.kernel_spec_manager
  203. for kid, ks in self.generate_kernel_specs(app, ipython_arguments).items():
  204. roots = [os.path.dirname(ks.resource_dir), ksm.user_kernel_dir]
  205. success = False
  206. for root in roots:
  207. kernel_dir = os.path.join(root, kid)
  208. try:
  209. if not os.path.exists(kernel_dir):
  210. os.makedirs(kernel_dir)
  211. with open(os.path.join(kernel_dir, 'kernel.json'), 'w') as f:
  212. f.write(ks.to_json())
  213. success = True
  214. break
  215. except OSError:
  216. continue
  217. if not success:
  218. raise CommandError("Could not write kernel %r in directories %r" % (kid, roots))
  219. app.start()
  220. @shell_runner(flags=['--notebook'], name='IPython Notebook')
  221. def get_notebook(self, options):
  222. try:
  223. from IPython import release
  224. except ImportError:
  225. return traceback.format_exc()
  226. try:
  227. from notebook.notebookapp import NotebookApp
  228. except ImportError:
  229. if release.version_info[0] >= 7:
  230. return traceback.format_exc()
  231. try:
  232. from IPython.html.notebookapp import NotebookApp
  233. except ImportError:
  234. if release.version_info[0] >= 3:
  235. return traceback.format_exc()
  236. try:
  237. from IPython.frontend.html.notebook import notebookapp
  238. NotebookApp = notebookapp.NotebookApp
  239. except ImportError:
  240. return traceback.format_exc()
  241. use_kernel_specs = release.version_info[0] >= 3
  242. def run_notebook():
  243. app = NotebookApp.instance()
  244. self.run_notebookapp(app, options, use_kernel_specs)
  245. return run_notebook
  246. @shell_runner(flags=['--lab'], name='JupyterLab Notebook')
  247. def get_jupyterlab(self, options):
  248. try:
  249. from jupyterlab.labapp import LabApp
  250. except ImportError:
  251. return traceback.format_exc()
  252. def run_jupyterlab():
  253. app = LabApp.instance()
  254. self.run_notebookapp(app, options)
  255. return run_jupyterlab
  256. @shell_runner(flags=['--plain'], name='plain Python')
  257. def get_plain(self, options):
  258. # Using normal Python shell
  259. import code
  260. imported_objects = self.get_imported_objects(options)
  261. try:
  262. # Try activating rlcompleter, because it's handy.
  263. import readline
  264. except ImportError:
  265. pass
  266. else:
  267. # We don't have to wrap the following import in a 'try', because
  268. # we already know 'readline' was imported successfully.
  269. import rlcompleter
  270. readline.set_completer(rlcompleter.Completer(imported_objects).complete)
  271. # Enable tab completion on systems using libedit (e.g. macOS).
  272. # These lines are copied from Lib/site.py on Python 3.4.
  273. readline_doc = getattr(readline, '__doc__', '')
  274. if readline_doc is not None and 'libedit' in readline_doc:
  275. readline.parse_and_bind("bind ^I rl_complete")
  276. else:
  277. readline.parse_and_bind("tab:complete")
  278. use_pythonrc = options['use_pythonrc']
  279. no_startup = options['no_startup']
  280. # We want to honor both $PYTHONSTARTUP and .pythonrc.py, so follow system
  281. # conventions and get $PYTHONSTARTUP first then .pythonrc.py.
  282. if use_pythonrc or not no_startup:
  283. for pythonrc in OrderedSet([os.environ.get("PYTHONSTARTUP"), os.path.expanduser('~/.pythonrc.py')]):
  284. if not pythonrc:
  285. continue
  286. if not os.path.isfile(pythonrc):
  287. continue
  288. with open(pythonrc) as handle:
  289. pythonrc_code = handle.read()
  290. # Match the behavior of the cpython shell where an error in
  291. # PYTHONSTARTUP prints an exception and continues.
  292. try:
  293. exec(compile(pythonrc_code, pythonrc, 'exec'), imported_objects)
  294. except Exception:
  295. traceback.print_exc()
  296. if self.tests_mode:
  297. raise
  298. def run_plain():
  299. code.interact(local=imported_objects)
  300. return run_plain
  301. @shell_runner(flags=['--bpython'], name='BPython')
  302. def get_bpython(self, options):
  303. try:
  304. from bpython import embed
  305. except ImportError:
  306. return traceback.format_exc()
  307. def run_bpython():
  308. imported_objects = self.get_imported_objects(options)
  309. kwargs = {}
  310. if self.extra_args:
  311. kwargs['args'] = self.extra_args
  312. embed(imported_objects, **kwargs)
  313. return run_bpython
  314. @shell_runner(flags=['--ipython'], name='IPython')
  315. def get_ipython(self, options):
  316. try:
  317. from IPython import start_ipython
  318. def run_ipython():
  319. imported_objects = self.get_imported_objects(options)
  320. ipython_arguments = self.extra_args or self.get_ipython_arguments(options)
  321. start_ipython(argv=ipython_arguments, user_ns=imported_objects)
  322. return run_ipython
  323. except ImportError:
  324. str_exc = traceback.format_exc()
  325. # IPython < 0.11
  326. # Explicitly pass an empty list as arguments, because otherwise
  327. # IPython would use sys.argv from this script.
  328. # Notebook not supported for IPython < 0.11.
  329. try:
  330. from IPython.Shell import IPShell
  331. except ImportError:
  332. return str_exc + "\n" + traceback.format_exc()
  333. def run_ipython():
  334. imported_objects = self.get_imported_objects(options)
  335. shell = IPShell(argv=[], user_ns=imported_objects)
  336. shell.mainloop()
  337. return run_ipython
  338. @shell_runner(flags=['--ptpython'], name='PTPython')
  339. def get_ptpython(self, options):
  340. try:
  341. from ptpython.repl import embed, run_config
  342. except ImportError:
  343. tb = traceback.format_exc()
  344. try: # prompt_toolkit < v0.27
  345. from prompt_toolkit.contrib.repl import embed, run_config
  346. except ImportError:
  347. return tb
  348. def run_ptpython():
  349. imported_objects = self.get_imported_objects(options)
  350. history_filename = os.path.expanduser('~/.ptpython_history')
  351. embed(globals=imported_objects, history_filename=history_filename,
  352. vi_mode=options['vi_mode'], configure=run_config)
  353. return run_ptpython
  354. @shell_runner(flags=['--ptipython'], name='PT-IPython')
  355. def get_ptipython(self, options):
  356. try:
  357. from ptpython.repl import run_config
  358. from ptpython.ipython import embed
  359. except ImportError:
  360. tb = traceback.format_exc()
  361. try: # prompt_toolkit < v0.27
  362. from prompt_toolkit.contrib.repl import run_config
  363. from prompt_toolkit.contrib.ipython import embed
  364. except ImportError:
  365. return tb
  366. def run_ptipython():
  367. imported_objects = self.get_imported_objects(options)
  368. history_filename = os.path.expanduser('~/.ptpython_history')
  369. embed(user_ns=imported_objects, history_filename=history_filename,
  370. vi_mode=options['vi_mode'], configure=run_config)
  371. return run_ptipython
  372. @shell_runner(flags=['--idle'], name='Idle')
  373. def get_idle(self, options):
  374. from idlelib.pyshell import main
  375. def run_idle():
  376. sys.argv = [
  377. sys.argv[0],
  378. '-c',
  379. """
  380. from django_extensions.management import shells
  381. from django.core.management.color import no_style
  382. for k, m in shells.import_objects({}, no_style()).items():
  383. globals()[k] = m
  384. """,
  385. ]
  386. main()
  387. return run_idle
  388. def set_application_name(self, options):
  389. """
  390. Set the application_name on PostgreSQL connection
  391. Use the fallback_application_name to let the user override
  392. it with PGAPPNAME env variable
  393. http://www.postgresql.org/docs/9.4/static/libpq-connect.html#LIBPQ-PARAMKEYWORDS # noqa
  394. """
  395. supported_backends = ['django.db.backends.postgresql',
  396. 'django.db.backends.postgresql_psycopg2']
  397. opt_name = 'fallback_application_name'
  398. default_app_name = 'django_shell'
  399. app_name = default_app_name
  400. dbs = getattr(settings, 'DATABASES', [])
  401. # lookup over all the databases entry
  402. for db in dbs.keys():
  403. if dbs[db]['ENGINE'] in supported_backends:
  404. try:
  405. options = dbs[db]['OPTIONS']
  406. except KeyError:
  407. options = {}
  408. # dot not override a defined value
  409. if opt_name in options.keys():
  410. app_name = dbs[db]['OPTIONS'][opt_name]
  411. else:
  412. dbs[db].setdefault('OPTIONS', {}).update({opt_name: default_app_name})
  413. app_name = default_app_name
  414. return app_name
  415. @signalcommand
  416. def handle(self, *args, **options):
  417. verbosity = options["verbosity"]
  418. get_runner = options['runner']
  419. print_sql = getattr(settings, 'SHELL_PLUS_PRINT_SQL', False)
  420. runner = None
  421. runner_name = None
  422. with monkey_patch_cursordebugwrapper(print_sql=options["print_sql"] or print_sql, print_sql_location=options["print_sql_location"], confprefix="SHELL_PLUS"):
  423. SETTINGS_SHELL_PLUS = getattr(settings, 'SHELL_PLUS', None)
  424. def get_runner_by_flag(flag):
  425. for runner in self.runners:
  426. if flag in runner.runner_flags:
  427. return runner
  428. return None
  429. self.set_application_name(options)
  430. if get_runner:
  431. runner = get_runner(options)
  432. runner_name = get_runner.runner_name
  433. elif SETTINGS_SHELL_PLUS:
  434. get_runner = get_runner_by_flag('--%s' % SETTINGS_SHELL_PLUS)
  435. if not get_runner:
  436. runner = None
  437. runner_name = SETTINGS_SHELL_PLUS
  438. else:
  439. def try_runner(get_runner):
  440. runner_name = get_runner.runner_name
  441. if verbosity > 2:
  442. print(self.style.NOTICE("Trying: %s" % runner_name))
  443. runner = get_runner(options)
  444. if callable(runner):
  445. if verbosity > 1:
  446. print(self.style.NOTICE("Using: %s" % runner_name))
  447. return runner
  448. return None
  449. tried_runners = set()
  450. # try the runners that are least unexpected (normal shell runners)
  451. preferred_runners = ['ptipython', 'ptpython', 'bpython', 'ipython', 'plain']
  452. for flag_suffix in preferred_runners:
  453. get_runner = get_runner_by_flag('--%s' % flag_suffix)
  454. tried_runners.add(get_runner)
  455. runner = try_runner(get_runner)
  456. if runner:
  457. runner_name = get_runner.runner_name
  458. break
  459. # try any remaining runners if needed
  460. if not runner:
  461. for get_runner in self.runners:
  462. if get_runner not in tried_runners:
  463. runner = try_runner(get_runner)
  464. if runner:
  465. runner_name = get_runner.runner_name
  466. break
  467. if not callable(runner):
  468. if runner:
  469. print(runner)
  470. if not runner_name:
  471. raise CommandError("No shell runner could be found.")
  472. raise CommandError("Could not load shell runner: '%s'." % runner_name)
  473. if self.tests_mode:
  474. return 130
  475. if options['command']:
  476. imported_objects = self.get_imported_objects(options)
  477. exec(options['command'], {}, imported_objects)
  478. return
  479. runner()