0001"""Paster Commands, for use with paster in your project
0002
0003The command(s) listed here are for use with Paste to enable easy creation of
0004various core Pylons templates.
0005
0006Currently available commands are::
0007
0008 controller, restcontroller, shell
0009"""
0010import os
0011import sys
0012
0013import paste.fixture
0014import paste.registry
0015import paste.deploy.config
0016from paste.deploy import loadapp, appconfig
0017from paste.script.command import Command, BadCommand
0018from paste.script.filemaker import FileOp
0019from paste.script.pluginlib import find_egg_info_dir
0020
0021import pylons.util as util
0022
0023__all__ = ['ControllerCommand', 'RestControllerCommand', 'ShellCommand']
0024
0025def can_import(name):
0026 """Attempt to __import__ the specified package/module, returning True when
0027 succeeding, otherwise False"""
0028 try:
0029 __import__(name)
0030 return True
0031 except ImportError:
0032 return False
0033
0034
0035def is_minimal_template(package):
0036 """Determine if the specified Pylons project (package) uses the Pylons
0037 Minimal Tempalte"""
0038 minimal_template = False
0039 try:
0040 # Check if PACKAGE.lib.base exists
0041 __import__(package + '.lib.base')
0042 except ImportError, ie:
0043 if 'No module named lib.base' in str(ie):
0044 minimal_template = True
0045 except:
0046 # PACKAGE.lib.base exists but throws an error
0047 pass
0048 return minimal_template
0049
0050
0051def validate_name(name):
0052 """Validate that the name for the controller isn't present on the
0053 path already"""
0054 if not name:
0055 # This happens when the name is an existing directory
0056 raise BadCommand('Please give the name of a controller.')
0057 # 'setup' is a valid controller name, but when paster controller is ran
0058 # from the root directory of a project, importing setup will import the
0059 # project's setup.py causing a sys.exit(). Blame relative imports
0060 if name != 'setup' and can_import(name):
0061 raise BadCommand(
0062 "\n\nA module named '%s' is already present in your "
0063 "PYTHON_PATH.\nChoosing a conflicting name will likely cause "
0064 "import problems in\nyour controller at some point. It's "
0065 "suggested that you choose an\nalternate name, and if you'd "
0066 "like that name to be accessible as\n'%s', add a route "
0067 "to your projects config/routing.py file similar\nto:\n"
0068 " map.connect('%s', controller='my_%s')" % (name, name, name, name))
0070 return True
0071
0072
0073class ControllerCommand(Command):
0074 """Create a Controller and accompanying functional test
0075
0076 The Controller command will create the standard controller template
0077 file and associated functional test to speed creation of controllers.
0078
0079 Example usage::
0080
0081 yourproj% paster controller comments
0082 Creating yourproj/yourproj/controllers/comments.py
0083 Creating yourproj/yourproj/tests/functional/test_comments.py
0084
0085 If you'd like to have controllers underneath a directory, just include
0086 the path as the controller name and the necessary directories will be
0087 created for you::
0088
0089 yourproj% paster controller admin/trackback
0090 Creating yourproj/controllers/admin
0091 Creating yourproj/yourproj/controllers/admin/trackback.py
0092 Creating yourproj/yourproj/tests/functional/test_admin_trackback.py
0093 """
0094 summary = __doc__.splitlines()[0]
0095 usage = '\n' + __doc__
0096
0097 min_args = 1
0098 max_args = 1
0099 group_name = 'pylons'
0100
0101 default_verbosity = 3
0102
0103 parser = Command.standard_parser(simulate=True)
0104 parser.add_option('--no-test',
0105 action='store_true',
0106 dest='no_test',
0107 help="Don't create the test; just the controller")
0108
0109 def command(self):
0110 """Main command to create controller"""
0111 try:
0112 file_op = FileOp(source_dir=os.path.join(
0113 os.path.dirname(__file__), 'templates'))
0114 try:
0115 name, directory = file_op.parse_path_name_args(self.args[0])
0116 except:
0117 raise BadCommand('No egg_info directory was found')
0118
0119 # Check the name isn't the same as the package
0120 base_package = file_op.find_dir('controllers', True)[0]
0121 if base_package.lower() == name.lower():
0122 raise BadCommand(
0123 'Your controller name should not be the same as '
0124 'the package name %r.' % base_package)
0125 # Validate the name
0126 name = name.replace('-', '_')
0127 validate_name(name)
0128
0129 # Determine the module's import statement
0130 if is_minimal_template(base_package):
0131 importstatement = "from %s.controllers import *" % base_package
0132 else:
0133 importstatement = "from %s.lib.base import *" % base_package
0134
0135 # Setup the controller
0136 fullname = os.path.join(directory, name)
0137 controller_name = util.class_name_from_module_name(
0138 name.split('/')[-1])
0139 if not fullname.startswith(os.sep):
0140 fullname = os.sep + fullname
0141 testname = fullname.replace(os.sep, '_')[1:]
0142 file_op.template_vars.update(
0143 {'name': controller_name,
0144 'fname': os.path.join(directory, name),
0145 'importstatement': importstatement})
0146 file_op.copy_file(template='controller.py_tmpl',
0147 dest=os.path.join('controllers', directory),
0148 filename=name)
0149 if not self.options.no_test:
0150 file_op.copy_file(template='test_controller.py_tmpl',
0151 dest=os.path.join('tests', 'functional'),
0152 filename='test_'+testname)
0153 except BadCommand, e:
0154 raise BadCommand('An error occurred. %s' % e)
0155 except:
0156 msg = str(sys.exc_info()[1])
0157 raise BadCommand('An unknown error occurred. %s' % msg)
0158
0159
0160class RestControllerCommand(Command):
0161 """Create a REST Controller and accompanying functional test
0162
0163 The RestController command will create a REST-based Controller file for use
0164 with the map.resource REST-based dispatching. This template includes the
0165 methods that map.resource dispatches to in addition to doc strings for
0166 clarification on when the methods will be called.
0167
0168 The first argument should be the singular form of the REST resource. The
0169 second argument is the plural form of the word. If its a nested controller,
0170 put the directory information in front as shown in the second example
0171 below.
0172
0173 Example usage::
0174
0175 yourproj% paster restcontroller comment comments
0176 Creating yourproj/yourproj/controllers/comments.py
0177 Creating yourproj/yourproj/tests/functional/test_comments.py
0178
0179 If you'd like to have controllers underneath a directory, just include
0180 the path as the controller name and the necessary directories will be
0181 created for you::
0182
0183 yourproj% paster restcontroller admin/tracback admin/trackbacks
0184 Creating yourproj/controllers/admin
0185 Creating yourproj/yourproj/controllers/admin/trackbacks.py
0186 Creating yourproj/yourproj/tests/functional/test_admin_trackbacks.py
0187 """
0188 summary = __doc__.splitlines()[0]
0189 usage = '\n' + __doc__
0190
0191 min_args = 2
0192 max_args = 2
0193 group_name = 'pylons'
0194
0195 default_verbosity = 3
0196
0197 parser = Command.standard_parser(simulate=True)
0198 parser.add_option('--no-test',
0199 action='store_true',
0200 dest='no_test',
0201 help="Don't create the test; just the controller")
0202
0203 def command(self):
0204 """Main command to create controller"""
0205 try:
0206 file_op = FileOp(source_dir=os.path.join(
0207 os.path.dirname(__file__), 'templates'))
0208 try:
0209 singularname, singulardirectory = file_op.parse_path_name_args(self.args[0])
0211 pluralname, pluraldirectory = file_op.parse_path_name_args(self.args[1])
0213 except:
0214 raise BadCommand('No egg_info directory was found')
0215
0216 # Check the name isn't the same as the package
0217 base_package = file_op.find_dir('controllers', True)[0]
0218 if base_package.lower() == pluralname.lower():
0219 raise BadCommand(
0220 'Your controller name should not be the same as '
0221 'the package name %r.'% base_package)
0222 # Validate the name
0223 for name in [singularname, pluralname]:
0224 name = name.replace('-', '_')
0225 validate_name(name)
0226
0227 # Determine the module's import statement
0228 if is_minimal_template(base_package):
0229 importstatement = "from %s.controllers import *" % base_package
0230 else:
0231 importstatement = "from %s.lib.base import *" % base_package
0232
0233 # Setup the controller
0234 fullname = os.path.join(pluraldirectory, pluralname)
0235 controller_name = util.class_name_from_module_name(
0236 pluralname.split('/')[-1])
0237 if not fullname.startswith(os.sep):
0238 fullname = os.sep + fullname
0239 testname = fullname.replace(os.sep, '_')[1:]
0240
0241 nameprefix = ''
0242 if pluraldirectory:
0243 nameprefix = pluraldirectory.replace(os.path.sep, '_') + '_'
0244
0245 controller_c = ''
0246 if nameprefix:
0247 controller_c = ", controller='%s', \n\t" % '/'.join([pluraldirectory, pluralname])
0249 controller_c += "path_prefix='/%s', name_prefix='%s_'" % (pluraldirectory, pluraldirectory)
0251 command = "map.resource('%s', '%s'%s)\n" % (singularname, pluralname, controller_c)
0253
0254 file_op.template_vars.update(
0255 {'classname': controller_name,
0256 'pluralname': pluralname,
0257 'singularname': singularname,
0258 'name': controller_name,
0259 'nameprefix': nameprefix,
0260 'resource_command': command.replace('\n\t', '\n%s#%s' % (' '*4, ' '*9)),
0262 'fname': os.path.join(pluraldirectory, pluralname),
0263 'importstatement': importstatement})
0264
0265 resource_command = ("\nTo create the appropriate RESTful mapping, "
0266 "add a map statement to your\n")
0267 resource_command += ("config/routing.py file near the top like "
0268 "this:\n\n")
0269 resource_command += command
0270 file_op.copy_file(template='restcontroller.py_tmpl',
0271 dest=os.path.join('controllers', pluraldirectory),
0272 filename=pluralname)
0273 if not self.options.no_test:
0274 file_op.copy_file(template='test_controller.py_tmpl',
0275 dest=os.path.join('tests', 'functional'),
0276 filename='test_'+testname)
0277 print resource_command
0278 except BadCommand, e:
0279 raise BadCommand('An error occurred. %s' % e)
0280 except:
0281 msg = str(sys.exc_info()[1])
0282 raise BadCommand('An unknown error occurred. %s' % msg)
0283
0284
0285class ShellCommand(Command):
0286 """Open an interactive shell with the Pylons app loaded
0287
0288 The optional CONFIG_FILE argument specifies the config file to use for
0289 the interactive shell. CONFIG_FILE defaults to 'development.ini'.
0290
0291 This allows you to test your mapper, models, and simulate web requests
0292 using ``paste.fixture``.
0293
0294 Example::
0295
0296 $ paster shell my-development.ini
0297 """
0298 summary = __doc__.splitlines()[0]
0299 usage = '\n' + __doc__
0300
0301 min_args = 0
0302 max_args = 1
0303 group_name = 'pylons'
0304
0305 parser = Command.standard_parser(simulate=True)
0306 parser.add_option('-d', '--disable-ipython',
0307 action='store_true',
0308 dest='disable_ipython',
0309 help="Don't use IPython if it is available")
0310
0311 parser.add_option('-q',
0312 action='count',
0313 dest='quiet',
0314 default=0,
0315 help="Do not load logging configuration from the config file")
0316
0317 def command(self):
0318 """Main command to create a new shell"""
0319 self.verbose = 3
0320 if len(self.args) == 0:
0321 # Assume the .ini file is ./development.ini
0322 config_file = 'development.ini'
0323 if not os.path.isfile(config_file):
0324 raise BadCommand('%sError: CONFIG_FILE not found at: .%s%s\n'
0325 'Please specify a CONFIG_FILE' % (self.parser.get_usage(), os.path.sep,
0327 config_file))
0328 else:
0329 config_file = self.args[0]
0330
0331 config_name = 'config:%s' % config_file
0332 here_dir = os.getcwd()
0333 locs = dict(__name__="pylons-admin")
0334
0335 # XXX: Note, initializing CONFIG here is Legacy support. pylons.config
0336 # will automatically be initialized and restored via the registry
0337 # restorer along with the other StackedObjectProxys
0338 # Load app config into paste.deploy to simulate request config
0339 # Setup the Paste CONFIG object, adding app_conf/global_conf for legacy
0340 # code
0341 conf = appconfig(config_name, relative_to=here_dir)
0342 conf.update(dict(app_conf=conf.local_conf,
0343 global_conf=conf.global_conf))
0344 paste.deploy.config.CONFIG.push_thread_config(conf)
0345
0346 # Load locals and populate with objects for use in shell
0347 sys.path.insert(0, here_dir)
0348
0349 # Load the wsgi app first so that everything is initialized right
0350 wsgiapp = loadapp(config_name, relative_to=here_dir)
0351 test_app = paste.fixture.TestApp(wsgiapp)
0352
0353 # Query the test app to setup the environment
0354 tresponse = test_app.get('/_test_vars')
0355 request_id = int(tresponse.body)
0356
0357 # Disable restoration during test_app requests
0358 test_app.pre_request_hook = lambda self: paste.registry.restorer.restoration_end()
0360 test_app.post_request_hook = lambda self: paste.registry.restorer.restoration_begin(request_id)
0362
0363 # Restore the state of the Pylons special objects
0364 # (StackedObjectProxies)
0365 paste.registry.restorer.restoration_begin(request_id)
0366
0367 # Determine the package name from the .egg-info top_level.txt.
0368 egg_info = find_egg_info_dir(here_dir)
0369 f = open(os.path.join(egg_info, 'top_level.txt'))
0370 packages = [l.strip() for l in f.readlines()
0371 if l.strip() and not l.strip().startswith('#')]
0372 f.close()
0373
0374 # Start the rest of our imports now that the app is loaded
0375 found_base = False
0376 for pkg_name in packages:
0377 # Import all objects from the base module
0378 base_module = pkg_name + '.lib.base'
0379 found_base = can_import(base_module)
0380 if not found_base:
0381 # Minimal template
0382 base_module = pkg_name + '.controllers'
0383 found_base = can_import(base_module)
0384
0385 if found_base:
0386 break
0387
0388 if not found_base:
0389 raise ImportError("Could not import base module. Are you sure "
0390 "this is a Pylons app?")
0391
0392 base = sys.modules[base_module]
0393 base_public = [__name for __name in dir(base) if not __name.startswith('_') or __name == '_']
0395 for name in base_public:
0396 locs[name] = getattr(base, name)
0397 locs.update(dict(wsgiapp=wsgiapp, app=test_app))
0398
0399 mapper = tresponse.config.get('routes.map')
0400 if mapper:
0401 locs['mapper'] = mapper
0402
0403 banner = " All objects from %s are available\n" % base_module
0404 banner += " Additional Objects:\n"
0405 if mapper:
0406 banner += " %-10s - %s\n" % ('mapper', 'Routes mapper object')
0407 banner += " %-10s - %s\n" % ('wsgiapp',
0408 "This project's WSGI App instance")
0409 banner += " %-10s - %s\n" % ('app',
0410 'paste.fixture wrapped around wsgiapp')
0411
0412 if not self.options.quiet:
0413 # Configure logging from the config file
0414 self.logging_file_config(config_file)
0415
0416 try:
0417 if self.options.disable_ipython:
0418 raise ImportError()
0419
0420 # try to use IPython if possible
0421 from IPython.Shell import IPShellEmbed
0422
0423 shell = IPShellEmbed(argv=self.args)
0424 shell.set_banner(shell.IP.BANNER + '\n\n' + banner)
0425 try:
0426 shell(local_ns=locs, global_ns={})
0427 finally:
0428 paste.registry.restorer.restoration_end()
0429 except ImportError:
0430 import code
0431 newbanner = "Pylons Interactive Shell\nPython %s\n\n" % sys.version
0432 banner = newbanner + banner
0433 shell = code.InteractiveConsole(locals=locs)
0434 try:
0435 import readline
0436 except ImportError:
0437 pass
0438 try:
0439 shell.interact(banner)
0440 finally:
0441 paste.registry.restorer.restoration_end()