commit | author | age
|
337960
|
1 |
from code import interact |
5ad647
|
2 |
import argparse |
f3a567
|
3 |
import os |
337960
|
4 |
import sys |
d58614
|
5 |
import textwrap |
cb9202
|
6 |
import pkg_resources |
337960
|
7 |
|
f3a567
|
8 |
from pyramid.compat import exec_ |
337960
|
9 |
from pyramid.util import DottedNameResolver |
CM |
10 |
from pyramid.paster import bootstrap |
160865
|
11 |
|
208e7b
|
12 |
from pyramid.settings import aslist |
MM |
13 |
|
e1d1af
|
14 |
from pyramid.scripts.common import get_config_loader |
49fb77
|
15 |
from pyramid.scripts.common import parse_vars |
JA |
16 |
|
d29151
|
17 |
def main(argv=sys.argv, quiet=False): |
CM |
18 |
command = PShellCommand(argv, quiet) |
d58614
|
19 |
return command.run() |
cb9202
|
20 |
|
337960
|
21 |
|
b7350e
|
22 |
def python_shell_runner(env, help, interact=interact): |
MM |
23 |
cprt = 'Type "help" for more information.' |
|
24 |
banner = "Python %s on %s\n%s" % (sys.version, sys.platform, cprt) |
|
25 |
banner += '\n\n' + help + '\n' |
|
26 |
interact(banner, local=env) |
|
27 |
|
|
28 |
|
337960
|
29 |
class PShellCommand(object): |
d58614
|
30 |
description = """\ |
CM |
31 |
Open an interactive shell with a Pyramid app loaded. This command |
|
32 |
accepts one positional argument named "config_uri" which specifies the |
|
33 |
PasteDeploy config file to use for the interactive shell. The format is |
|
34 |
"inifile#name". If the name is left off, the Pyramid default application |
0a3a20
|
35 |
will be assumed. Example: "pshell myapp.ini#main". |
337960
|
36 |
|
d58614
|
37 |
If you do not point the loader directly at the section of the ini file |
CM |
38 |
containing your Pyramid application, the command will attempt to |
|
39 |
find the app for you. If you are loading a pipeline that contains more |
|
40 |
than one Pyramid application within it, the loader will use the |
|
41 |
last one. |
337960
|
42 |
""" |
e1d1af
|
43 |
bootstrap = staticmethod(bootstrap) # for testing |
MM |
44 |
get_config_loader = staticmethod(get_config_loader) # for testing |
cb9202
|
45 |
pkg_resources = pkg_resources # for testing |
337960
|
46 |
|
5ad647
|
47 |
parser = argparse.ArgumentParser( |
df57ec
|
48 |
description=textwrap.dedent(description), |
c9b2fa
|
49 |
formatter_class=argparse.RawDescriptionHelpFormatter, |
d58614
|
50 |
) |
5ad647
|
51 |
parser.add_argument('-p', '--python-shell', |
SP |
52 |
action='store', |
|
53 |
dest='python_shell', |
|
54 |
default='', |
|
55 |
help=('Select the shell to use. A list of possible ' |
|
56 |
'shells is available using the --list-shells ' |
|
57 |
'option.')) |
|
58 |
parser.add_argument('-l', '--list-shells', |
|
59 |
dest='list', |
|
60 |
action='store_true', |
|
61 |
help='List all available shells.') |
|
62 |
parser.add_argument('--setup', |
|
63 |
dest='setup', |
|
64 |
help=("A callable that will be passed the environment " |
|
65 |
"before it is made available to the shell. This " |
|
66 |
"option will override the 'setup' key in the " |
|
67 |
"[pshell] ini section.")) |
|
68 |
parser.add_argument('config_uri', |
307ee4
|
69 |
nargs='?', |
SP |
70 |
default=None, |
5ad647
|
71 |
help='The URI to the configuration file.') |
3c4310
|
72 |
parser.add_argument( |
SP |
73 |
'config_vars', |
|
74 |
nargs='*', |
|
75 |
default=(), |
|
76 |
help="Variables required by the config file. For example, " |
|
77 |
"`http_port=%%(http_port)s` would expect `http_port=8080` to be " |
|
78 |
"passed here.", |
|
79 |
) |
337960
|
80 |
|
b7350e
|
81 |
default_runner = python_shell_runner # testing |
337960
|
82 |
|
CM |
83 |
loaded_objects = {} |
|
84 |
object_help = {} |
208e7b
|
85 |
preferred_shells = [] |
337960
|
86 |
setup = None |
823ac4
|
87 |
pystartup = os.environ.get('PYTHONSTARTUP') |
337960
|
88 |
|
d29151
|
89 |
def __init__(self, argv, quiet=False): |
CM |
90 |
self.quiet = quiet |
5ad647
|
91 |
self.args = self.parser.parse_args(argv[1:]) |
337960
|
92 |
|
e1d1af
|
93 |
def pshell_file_config(self, loader, defaults): |
MM |
94 |
settings = loader.get_settings('pshell', defaults) |
337960
|
95 |
resolver = DottedNameResolver(None) |
CM |
96 |
self.loaded_objects = {} |
|
97 |
self.object_help = {} |
|
98 |
self.setup = None |
e1d1af
|
99 |
for k, v in settings.items(): |
337960
|
100 |
if k == 'setup': |
CM |
101 |
self.setup = v |
208e7b
|
102 |
elif k == 'default_shell': |
MM |
103 |
self.preferred_shells = [x.lower() for x in aslist(v)] |
337960
|
104 |
else: |
CM |
105 |
self.loaded_objects[k] = resolver.maybe_resolve(v) |
|
106 |
self.object_help[k] = v |
|
107 |
|
d29151
|
108 |
def out(self, msg): # pragma: no cover |
CM |
109 |
if not self.quiet: |
|
110 |
print(msg) |
|
111 |
|
337960
|
112 |
def run(self, shell=None): |
1fc1b8
|
113 |
if self.args.list: |
208e7b
|
114 |
return self.show_shells() |
1fc1b8
|
115 |
if not self.args.config_uri: |
d29151
|
116 |
self.out('Requires a config file argument') |
d58614
|
117 |
return 2 |
1fc1b8
|
118 |
config_uri = self.args.config_uri |
e1d1af
|
119 |
config_vars = parse_vars(self.args.config_vars) |
MM |
120 |
loader = self.get_config_loader(config_uri) |
|
121 |
loader.setup_logging(config_vars) |
|
122 |
self.pshell_file_config(loader, config_vars) |
337960
|
123 |
|
e1d1af
|
124 |
env = self.bootstrap(config_uri, options=config_vars) |
337960
|
125 |
|
CM |
126 |
# remove the closer from the env |
b932a4
|
127 |
self.closer = env.pop('closer') |
337960
|
128 |
|
CM |
129 |
# setup help text for default environment |
|
130 |
env_help = dict(env) |
|
131 |
env_help['app'] = 'The WSGI application.' |
|
132 |
env_help['root'] = 'Root of the default resource tree.' |
|
133 |
env_help['registry'] = 'Active Pyramid registry.' |
|
134 |
env_help['request'] = 'Active request object.' |
|
135 |
env_help['root_factory'] = ( |
|
136 |
'Default root factory used to create `root`.') |
|
137 |
|
|
138 |
# override use_script with command-line options |
5ad647
|
139 |
if self.args.setup: |
SP |
140 |
self.setup = self.args.setup |
337960
|
141 |
|
CM |
142 |
if self.setup: |
|
143 |
# store the env before muddling it with the script |
|
144 |
orig_env = env.copy() |
|
145 |
|
|
146 |
# call the setup callable |
|
147 |
resolver = DottedNameResolver(None) |
|
148 |
setup = resolver.maybe_resolve(self.setup) |
|
149 |
setup(env) |
|
150 |
|
|
151 |
# remove any objects from default help that were overidden |
934460
|
152 |
for k, v in env.items(): |
337960
|
153 |
if k not in orig_env or env[k] != orig_env[k]: |
1c1c90
|
154 |
if getattr(v, '__doc__', False): |
JD |
155 |
env_help[k] = v.__doc__.replace("\n", " ") |
|
156 |
else: |
|
157 |
env_help[k] = v |
337960
|
158 |
|
CM |
159 |
# load the pshell section of the ini file |
|
160 |
env.update(self.loaded_objects) |
|
161 |
|
|
162 |
# eliminate duplicates from env, allowing custom vars to override |
|
163 |
for k in self.loaded_objects: |
|
164 |
if k in env_help: |
|
165 |
del env_help[k] |
|
166 |
|
|
167 |
# generate help text |
|
168 |
help = '' |
|
169 |
if env_help: |
|
170 |
help += 'Environment:' |
|
171 |
for var in sorted(env_help.keys()): |
|
172 |
help += '\n %-12s %s' % (var, env_help[var]) |
|
173 |
|
|
174 |
if self.object_help: |
|
175 |
help += '\n\nCustom Variables:' |
|
176 |
for var in sorted(self.object_help.keys()): |
|
177 |
help += '\n %-12s %s' % (var, self.object_help[var]) |
|
178 |
|
|
179 |
if shell is None: |
b932a4
|
180 |
try: |
JA |
181 |
shell = self.make_shell() |
|
182 |
except ValueError as e: |
|
183 |
self.out(str(e)) |
|
184 |
self.closer() |
|
185 |
return 1 |
337960
|
186 |
|
f3a567
|
187 |
if self.pystartup and os.path.isfile(self.pystartup): |
MM |
188 |
with open(self.pystartup, 'rb') as fp: |
|
189 |
exec_(fp.read().decode('utf-8'), env) |
|
190 |
if '__builtins__' in env: |
|
191 |
del env['__builtins__'] |
|
192 |
|
337960
|
193 |
try: |
CM |
194 |
shell(env, help) |
|
195 |
finally: |
b932a4
|
196 |
self.closer() |
3808f7
|
197 |
|
208e7b
|
198 |
def show_shells(self): |
MM |
199 |
shells = self.find_all_shells() |
b7350e
|
200 |
sorted_names = sorted(shells.keys(), key=lambda x: x.lower()) |
cb9202
|
201 |
|
208e7b
|
202 |
self.out('Available shells:') |
b7350e
|
203 |
for name in sorted_names: |
MM |
204 |
self.out(' %s' % (name,)) |
208e7b
|
205 |
return 0 |
MM |
206 |
|
|
207 |
def find_all_shells(self): |
b7350e
|
208 |
pkg_resources = self.pkg_resources |
MM |
209 |
|
208e7b
|
210 |
shells = {} |
b7350e
|
211 |
for ep in pkg_resources.iter_entry_points('pyramid.pshell_runner'): |
cb9202
|
212 |
name = ep.name |
208e7b
|
213 |
shell_factory = ep.load() |
MM |
214 |
shells[name] = shell_factory |
|
215 |
return shells |
|
216 |
|
|
217 |
def make_shell(self): |
|
218 |
shells = self.find_all_shells() |
cb9202
|
219 |
|
3808f7
|
220 |
shell = None |
5ad647
|
221 |
user_shell = self.args.python_shell.lower() |
cb9202
|
222 |
|
3808f7
|
223 |
if not user_shell: |
208e7b
|
224 |
preferred_shells = self.preferred_shells |
MM |
225 |
if not preferred_shells: |
|
226 |
# by default prioritize all shells above python |
|
227 |
preferred_shells = [k for k in shells.keys() if k != 'python'] |
|
228 |
max_weight = len(preferred_shells) |
|
229 |
def order(x): |
|
230 |
# invert weight to reverse sort the list |
|
231 |
# (closer to the front is higher priority) |
|
232 |
try: |
|
233 |
return preferred_shells.index(x[0].lower()) - max_weight |
|
234 |
except ValueError: |
|
235 |
return 1 |
|
236 |
sorted_shells = sorted(shells.items(), key=order) |
3808f7
|
237 |
|
b7350e
|
238 |
if len(sorted_shells) > 0: |
MM |
239 |
shell = sorted_shells[0][1] |
|
240 |
|
cb9202
|
241 |
else: |
b7350e
|
242 |
runner = shells.get(user_shell) |
3808f7
|
243 |
|
b7350e
|
244 |
if runner is not None: |
MM |
245 |
shell = runner |
208e7b
|
246 |
|
MM |
247 |
if shell is None: |
b932a4
|
248 |
raise ValueError( |
JA |
249 |
'could not find a shell named "%s"' % user_shell |
|
250 |
) |
3808f7
|
251 |
|
MM |
252 |
if shell is None: |
b7350e
|
253 |
# should never happen, but just incase entry points are borked |
MM |
254 |
shell = self.default_runner |
3808f7
|
255 |
|
b74535
|
256 |
return shell |
MM |
257 |
|
337960
|
258 |
|
40d54e
|
259 |
if __name__ == '__main__': # pragma: no cover |
MM |
260 |
sys.exit(main() or 0) |