Michael Merickel
2017-03-29 e1d1af88e314fe59d9197182f8c2b56ecdcbd115
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)