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