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