Skip to content

Commit

Permalink
[IMP] base: "--shell-file" option override for $PYTHONSTARTUP
Browse files Browse the repository at this point in the history
The $PYTHONSTARTUP env variable can contain a python script that is used
by Python shell at the start of any interactive session.

- fix this feature from being broken in python and ptpython
- include a new `--shell-file=` shell parameter to override the env
  variable $PYTHONSTARTUP
- group the two shell parameters in a new options group
- remove custom python shells' banners from the start of the session
- shell options can now be saved in the configuration file.

Python docs: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONSTARTUP

Documentation PR: odoo/documentation#11334
task-4306704
  • Loading branch information
lordkrandel committed Nov 21, 2024
1 parent 0f6370d commit 8dbe56f
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 40 deletions.
1 change: 0 additions & 1 deletion odoo/addons/base/tests/config/cli
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@
--no-database-list

--dev xml,reload
--shell-interface ipython
--stop-after-init
--osv-memory-count-limit 71
--transient-age-limit 4
Expand Down
1 change: 0 additions & 1 deletion odoo/addons/base/tests/config/non_default.conf
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ list_db = False

# advanced
dev_mode = xml
shell_interface = ipython
stop_after_init = True
osv_memory_count_limit = 71
transient_age_limit = 4.0
Expand Down
1 change: 1 addition & 0 deletions odoo/addons/base/tests/shell_file.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
message = 'Hello from Python!'
55 changes: 44 additions & 11 deletions odoo/addons/base/tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
import os
import re
import sys
import subprocess as sp
import unittest
from pathlib import Path

from odoo.cli.command import commands, load_addons_commands, load_internal_commands
from odoo.tools import config
from odoo.tests import BaseCase

from odoo.tools import config, file_path
from odoo.tests import BaseCase, Like

class TestCommand(BaseCase):

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.odoo_bin = sys.argv[0]
assert 'odoo-bin' in cls.odoo_bin
cls.odoo_bin = Path(__file__).parents[4].resolve() / 'odoo-bin'
cls.run_args = (sys.executable, cls.odoo_bin, f'--addons-path={config["addons_path"]}')

def run_command(self, *args, check=True, capture_output=True, text=True, **kwargs):
return sp.run(
[
sys.executable,
self.odoo_bin,
f'--addons-path={config["addons_path"]}',
*args,
],
[*self.run_args, *args],
capture_output=capture_output,
check=check,
text=text,
**kwargs
)

def popen_command(self, *args, capture_output=True, text=True, **kwargs):
if capture_output:
kwargs['stdout'] = kwargs['stderr'] = sp.PIPE
return sp.Popen(
[*self.run_args, *args],
text=text,
**kwargs
)

def test_docstring(self):
load_internal_commands()
load_addons_commands()
Expand Down Expand Up @@ -60,3 +66,30 @@ def test_help(self):
if line.startswith(" ") and (result := re.search(r' (\w+)\s+(\w.*)$', line)):
actual.add(result.groups()[0])
self.assertGreaterEqual(actual, expected, msg="Help is not showing required commands")

@unittest.skipIf(os.name != 'posix', '`os.openpty` only available on POSIX systems')
def test_shell(self):

main, child = os.openpty()

shell = self.popen_command(
'shell',
'--shell-interface=python',
'--shell-file', file_path('base/tests/shell_file.txt'),
stdin=main,
close_fds=True,
)
with os.fdopen(child, 'w', encoding="utf-8") as stdin_file:
stdin_file.write(
'print(message)\n'
'exit()\n'
)
shell.wait()

self.assertEqual(shell.stdout.read().splitlines(), [
Like("No environment set..."),
Like("odoo: <module 'odoo' from '/.../odoo/__init__.py'>"),
Like("openerp: <module 'odoo' from '/.../odoo/__init__.py'>"),
">>> Hello from Python!",
'>>> '
])
4 changes: 0 additions & 4 deletions odoo/addons/base/tests/test_configmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ def test_01_default_config(self):

# advanced
'dev_mode': [],
'shell_interface': '',
'stop_after_init': False,
'osv_memory_count_limit': 0,
'transient_age_limit': 1.0,
Expand Down Expand Up @@ -266,7 +265,6 @@ def test_02_config_file(self):

# advanced
'dev_mode': ['xml'], # blacklist for save, read from the config file
'shell_interface': 'ipython', # blacklist for save, read from the config file
'stop_after_init': True, # blacklist for save, read from the config file
'osv_memory_count_limit': 71,
'transient_age_limit': 4.0,
Expand Down Expand Up @@ -379,7 +377,6 @@ def test_04_odoo16_config_file(self):
'language': None,
'publisher_warranty_url': 'http://services.odoo.com/publisher-warranty/',
'save': False,
'shell_interface': '',
'stop_after_init': False,
'translate_in': '',
'translate_out': '',
Expand Down Expand Up @@ -548,7 +545,6 @@ def test_06_cli(self):

# advanced
'dev_mode': ['xml', 'reload'],
'shell_interface': 'ipython',
'stop_after_init': True,
'osv_memory_count_limit': 71,
'transient_age_limit': 4.0,
Expand Down
64 changes: 44 additions & 20 deletions odoo/cli/shell.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import code
import logging
import optparse
import os
import signal
import sys
Expand Down Expand Up @@ -39,15 +39,15 @@ def raise_keyboard_interrupt(*a):


class Console(code.InteractiveConsole):
def __init__(self, locals=None, filename="<console>"):
code.InteractiveConsole.__init__(self, locals, filename)
def __init__(self, local_vars=None, filename="<console>"):
code.InteractiveConsole.__init__(self, locals=local_vars, filename=filename)
try:
import readline
import rlcompleter
except ImportError:
print('readline or rlcompleter not available, autocomplete disabled.')
else:
readline.set_completer(rlcompleter.Completer(locals).complete)
readline.set_completer(rlcompleter.Completer(local_vars).complete)
readline.parse_and_bind("tab: complete")


Expand All @@ -57,6 +57,19 @@ class Shell(Command):

def init(self, args):
config.parser.prog = f'{Path(sys.argv[0]).name} {self.name}'

group = optparse.OptionGroup(config.parser, "Shell options")
group.add_option(
'--shell-file', dest='shell_file', type='string', default='', my_default='',
help="Specify a python script to be run after the start of the shell. "
"Overrides the env variable PYTHONSTARTUP."
)
group.add_option(
'--shell-interface', dest='shell_interface', type='string',
help="Specify a preferred REPL to use in shell mode. "
"Supported REPLs are: [ipython|ptpython|bpython|python]"
)
config.parser.add_option_group(group)
config.parse_config(args, setup_logging=True)
cli_server.report_configuration()
server.start(preload=[], stop=True)
Expand All @@ -72,6 +85,8 @@ def console(self, local_vars):
for i in sorted(local_vars):
print('%s: %s' % (i, local_vars[i]))

pythonstartup = config.options.get('shell_file') or os.environ.get('PYTHONSTARTUP')

preferred_interface = config.options.get('shell_interface')
if preferred_interface:
shells_to_try = [preferred_interface, 'python']
Expand All @@ -80,27 +95,36 @@ def console(self, local_vars):

for shell in shells_to_try:
try:
return getattr(self, shell)(local_vars)
shell_func = getattr(self, shell)
return shell_func(local_vars, pythonstartup)
except ImportError:
pass
except Exception:
_logger.warning("Could not start '%s' shell." % shell)
_logger.warning("Could not start '%s' shell.", shell)
_logger.debug("Shell error:", exc_info=True)

def ipython(self, local_vars):
from IPython import start_ipython
start_ipython(argv=[], user_ns=local_vars)

def ptpython(self, local_vars):
from ptpython.repl import embed
embed({}, local_vars)

def bpython(self, local_vars):
from bpython import embed
embed(local_vars)

def python(self, local_vars):
Console(locals=local_vars).interact()
def ipython(self, local_vars, pythonstartup=None):
from IPython import start_ipython # noqa: PLC0415
argv = (
['--TerminalIPythonApp.display_banner=False']
+ ([f'--TerminalIPythonApp.exec_files={pythonstartup}'] if pythonstartup else [])
)
start_ipython(argv=argv, user_ns=local_vars)

def ptpython(self, local_vars, pythonstartup=None):
from ptpython.repl import embed # noqa: PLC0415
embed({}, local_vars, startup_paths=[pythonstartup] if pythonstartup else False)

def bpython(self, local_vars, pythonstartup=None):
from bpython import embed # noqa: PLC0415
embed(local_vars, args=['-q', '-i', pythonstartup] if pythonstartup else None)

def python(self, local_vars, pythonstartup=None):
console = Console(local_vars)
if pythonstartup:
with open(pythonstartup, encoding='utf-8') as f:
console.runsource(f.read(), filename=pythonstartup, symbol='exec')
console.interact(banner='')

def shell(self, dbname):
local_vars = {
Expand Down
3 changes: 0 additions & 3 deletions odoo/tools/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,9 +381,6 @@ def _build_cli(self):
"- reload: restart server on change in the source code "
"- werkzeug: open a html debugger on http request error "
"- xml: read views from the source code, and not the db ")
group.add_option('--shell-interface', dest='shell_interface', my_default='', file_exportable=False,
help="Specify a preferred REPL to use in shell mode. Supported REPLs are: "
"[ipython|ptpython|bpython|python]")
group.add_option("--stop-after-init", action="store_true", dest="stop_after_init", my_default=False, file_exportable=False,
help="stop the server after its initialization")
group.add_option("--osv-memory-count-limit", dest="osv_memory_count_limit", my_default=0,
Expand Down

0 comments on commit 8dbe56f

Please sign in to comment.