Skip to content

Commit a9727f5

Browse files
committed
[IMP] base: "--shell-file" option override for $PYTHONSTARTUP
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
1 parent bf5ccd6 commit a9727f5

File tree

7 files changed

+90
-41
lines changed

7 files changed

+90
-41
lines changed

odoo/addons/base/tests/config/cli

-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@
6262
--no-database-list
6363

6464
--dev xml,reload
65-
--shell-interface ipython
6665
--stop-after-init
6766
--osv-memory-count-limit 71
6867
--transient-age-limit 4

odoo/addons/base/tests/config/non_default.conf

-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,6 @@ list_db = False
8585

8686
# advanced
8787
dev_mode = xml
88-
shell_interface = ipython
8988
stop_after_init = True
9089
osv_memory_count_limit = 71
9190
transient_age_limit = 4.0

odoo/addons/base/tests/shell_file.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
message = 'Hello from Python!'

odoo/addons/base/tests/test_cli.py

+45-12
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,41 @@
1+
import os
12
import re
23
import sys
34
import subprocess as sp
5+
import unittest
6+
from pathlib import Path
47

58
from odoo.cli.command import commands, load_addons_commands, load_internal_commands
6-
from odoo.tools import config
7-
from odoo.tests import BaseCase
8-
9+
from odoo.tools import config, file_path
10+
from odoo.tests import BaseCase, Like
911

1012
class TestCommand(BaseCase):
1113

1214
@classmethod
1315
def setUpClass(cls):
1416
super().setUpClass()
15-
cls.odoo_bin = sys.argv[0]
16-
assert 'odoo-bin' in cls.odoo_bin
17+
cls.odoo_bin = Path(__file__).parents[4].resolve() / 'odoo-bin'
18+
addons_path = config.format('addons_path', config['addons_path'])
19+
cls.run_args = (sys.executable, cls.odoo_bin, f'--addons-path={addons_path}')
1720

1821
def run_command(self, *args, check=True, capture_output=True, text=True, **kwargs):
19-
addons_path = config.format('addons_path', config['addons_path'])
2022
return sp.run(
21-
[
22-
sys.executable,
23-
self.odoo_bin,
24-
f'--addons-path={addons_path}',
25-
*args,
26-
],
23+
[*self.run_args, *args],
2724
capture_output=capture_output,
2825
check=check,
2926
text=text,
3027
**kwargs
3128
)
3229

30+
def popen_command(self, *args, capture_output=True, text=True, **kwargs):
31+
if capture_output:
32+
kwargs['stdout'] = kwargs['stderr'] = sp.PIPE
33+
return sp.Popen(
34+
[*self.run_args, *args],
35+
text=text,
36+
**kwargs
37+
)
38+
3339
def test_docstring(self):
3440
load_internal_commands()
3541
load_addons_commands()
@@ -82,3 +88,30 @@ def test_upgrade_code_standalone(self):
8288
self.assertIn("usage: ", proc.stdout)
8389
self.assertIn("Rewrite the entire source code", proc.stdout)
8490
self.assertFalse(proc.stderr)
91+
92+
@unittest.skipIf(os.name != 'posix', '`os.openpty` only available on POSIX systems')
93+
def test_shell(self):
94+
95+
main, child = os.openpty()
96+
97+
shell = self.popen_command(
98+
'shell',
99+
'--shell-interface=python',
100+
'--shell-file', file_path('base/tests/shell_file.txt'),
101+
stdin=main,
102+
close_fds=True,
103+
)
104+
with os.fdopen(child, 'w', encoding="utf-8") as stdin_file:
105+
stdin_file.write(
106+
'print(message)\n'
107+
'exit()\n'
108+
)
109+
shell.wait()
110+
111+
self.assertEqual(shell.stdout.read().splitlines(), [
112+
Like("No environment set..."),
113+
Like("odoo: <module 'odoo' from '/.../odoo/__init__.py'>"),
114+
Like("openerp: <module 'odoo' from '/.../odoo/__init__.py'>"),
115+
">>> Hello from Python!",
116+
'>>> '
117+
])

odoo/addons/base/tests/test_configmanager.py

-4
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,6 @@ def test_01_default_config(self):
148148

149149
# advanced
150150
'dev_mode': [],
151-
'shell_interface': '',
152151
'stop_after_init': False,
153152
'osv_memory_count_limit': 0,
154153
'transient_age_limit': 1.0,
@@ -266,7 +265,6 @@ def test_02_config_file(self):
266265

267266
# advanced
268267
'dev_mode': ['xml'], # blacklist for save, read from the config file
269-
'shell_interface': 'ipython', # blacklist for save, read from the config file
270268
'stop_after_init': True, # blacklist for save, read from the config file
271269
'osv_memory_count_limit': 71,
272270
'transient_age_limit': 4.0,
@@ -379,7 +377,6 @@ def test_04_odoo16_config_file(self):
379377
'language': None,
380378
'publisher_warranty_url': 'http://services.odoo.com/publisher-warranty/',
381379
'save': False,
382-
'shell_interface': '',
383380
'stop_after_init': False,
384381
'translate_in': '',
385382
'translate_out': '',
@@ -548,7 +545,6 @@ def test_06_cli(self):
548545

549546
# advanced
550547
'dev_mode': ['xml', 'reload'],
551-
'shell_interface': 'ipython',
552548
'stop_after_init': True,
553549
'osv_memory_count_limit': 71,
554550
'transient_age_limit': 4.0,

odoo/cli/shell.py

+44-20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
21
import code
32
import logging
3+
import optparse
44
import os
55
import signal
66
import sys
@@ -39,15 +39,15 @@ def raise_keyboard_interrupt(*a):
3939

4040

4141
class Console(code.InteractiveConsole):
42-
def __init__(self, locals=None, filename="<console>"):
43-
code.InteractiveConsole.__init__(self, locals, filename)
42+
def __init__(self, local_vars=None, filename="<console>"):
43+
code.InteractiveConsole.__init__(self, locals=local_vars, filename=filename)
4444
try:
4545
import readline
4646
import rlcompleter
4747
except ImportError:
4848
print('readline or rlcompleter not available, autocomplete disabled.')
4949
else:
50-
readline.set_completer(rlcompleter.Completer(locals).complete)
50+
readline.set_completer(rlcompleter.Completer(local_vars).complete)
5151
readline.parse_and_bind("tab: complete")
5252

5353

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

5858
def init(self, args):
5959
config.parser.prog = f'{Path(sys.argv[0]).name} {self.name}'
60+
61+
group = optparse.OptionGroup(config.parser, "Shell options")
62+
group.add_option(
63+
'--shell-file', dest='shell_file', type='string', default='', my_default='',
64+
help="Specify a python script to be run after the start of the shell. "
65+
"Overrides the env variable PYTHONSTARTUP."
66+
)
67+
group.add_option(
68+
'--shell-interface', dest='shell_interface', type='string',
69+
help="Specify a preferred REPL to use in shell mode. "
70+
"Supported REPLs are: [ipython|ptpython|bpython|python]"
71+
)
72+
config.parser.add_option_group(group)
6073
config.parse_config(args, setup_logging=True)
6174
cli_server.report_configuration()
6275
server.start(preload=[], stop=True)
@@ -72,6 +85,8 @@ def console(self, local_vars):
7285
for i in sorted(local_vars):
7386
print('%s: %s' % (i, local_vars[i]))
7487

88+
pythonstartup = config.options.get('shell_file') or os.environ.get('PYTHONSTARTUP')
89+
7590
preferred_interface = config.options.get('shell_interface')
7691
if preferred_interface:
7792
shells_to_try = [preferred_interface, 'python']
@@ -80,27 +95,36 @@ def console(self, local_vars):
8095

8196
for shell in shells_to_try:
8297
try:
83-
return getattr(self, shell)(local_vars)
98+
shell_func = getattr(self, shell)
99+
return shell_func(local_vars, pythonstartup)
84100
except ImportError:
85101
pass
86102
except Exception:
87-
_logger.warning("Could not start '%s' shell." % shell)
103+
_logger.warning("Could not start '%s' shell.", shell)
88104
_logger.debug("Shell error:", exc_info=True)
89105

90-
def ipython(self, local_vars):
91-
from IPython import start_ipython
92-
start_ipython(argv=[], user_ns=local_vars)
93-
94-
def ptpython(self, local_vars):
95-
from ptpython.repl import embed
96-
embed({}, local_vars)
97-
98-
def bpython(self, local_vars):
99-
from bpython import embed
100-
embed(local_vars)
101-
102-
def python(self, local_vars):
103-
Console(locals=local_vars).interact()
106+
def ipython(self, local_vars, pythonstartup=None):
107+
from IPython import start_ipython # noqa: PLC0415
108+
argv = (
109+
['--TerminalIPythonApp.display_banner=False']
110+
+ ([f'--TerminalIPythonApp.exec_files={pythonstartup}'] if pythonstartup else [])
111+
)
112+
start_ipython(argv=argv, user_ns=local_vars)
113+
114+
def ptpython(self, local_vars, pythonstartup=None):
115+
from ptpython.repl import embed # noqa: PLC0415
116+
embed({}, local_vars, startup_paths=[pythonstartup] if pythonstartup else False)
117+
118+
def bpython(self, local_vars, pythonstartup=None):
119+
from bpython import embed # noqa: PLC0415
120+
embed(local_vars, args=['-q', '-i', pythonstartup] if pythonstartup else None)
121+
122+
def python(self, local_vars, pythonstartup=None):
123+
console = Console(local_vars)
124+
if pythonstartup:
125+
with open(pythonstartup, encoding='utf-8') as f:
126+
console.runsource(f.read(), filename=pythonstartup, symbol='exec')
127+
console.interact(banner='')
104128

105129
def shell(self, dbname):
106130
local_vars = {

odoo/tools/config.py

-3
Original file line numberDiff line numberDiff line change
@@ -383,9 +383,6 @@ def _build_cli(self):
383383
"- reload: restart server on change in the source code "
384384
"- werkzeug: open a html debugger on http request error "
385385
"- xml: read views from the source code, and not the db ")
386-
group.add_option('--shell-interface', dest='shell_interface', my_default='', file_exportable=False,
387-
help="Specify a preferred REPL to use in shell mode. Supported REPLs are: "
388-
"[ipython|ptpython|bpython|python]")
389386
group.add_option("--stop-after-init", action="store_true", dest="stop_after_init", my_default=False, file_exportable=False,
390387
help="stop the server after its initialization")
391388
group.add_option("--osv-memory-count-limit", dest="osv_memory_count_limit", my_default=0,

0 commit comments

Comments
 (0)