Merge pull request #45 from ryanpetrello/next

A bunch of cleanup, and the removal of a few featuresPulling after review from Jon and Mark.
This commit is contained in:
Ryan Petrello
2012-03-07 14:16:57 -08:00
23 changed files with 172 additions and 462 deletions

View File

@@ -1,2 +0,0 @@
[run]
omit = pecan/commands/*.py, pecan/templates/__init__.py, pecan/testing.py

View File

@@ -9,7 +9,7 @@ from paste.urlparser import StaticURLParser
from weberror.errormiddleware import ErrorMiddleware
from weberror.evalexception import EvalException
from core import abort, error_for, override_template, Pecan, redirect, render, request, response, ValidationException
from core import abort, error_for, override_template, Pecan, load_app, redirect, render, request, response, ValidationException
from decorators import expose
from hooks import RequestViewerHook
from templating import error_formatters
@@ -18,7 +18,7 @@ from configuration import set_config
from configuration import _runtime_conf as conf
__all__ = [
'make_app', 'Pecan', 'request', 'response', 'override_template', 'expose', 'conf', 'set_config'
'make_app', 'load_app', 'Pecan', 'request', 'response', 'override_template', 'expose', 'conf', 'set_config'
]
def make_app(root, static_root=None, debug=False, errorcfg={}, wrap_app=None, logging=False, **kw):

View File

@@ -1,6 +1,7 @@
"""
PasteScript base command for Pecan.
"""
from pecan import load_app
from pecan.configuration import _runtime_conf, set_config
from paste.script import command as paste_command
@@ -31,37 +32,18 @@ class Command(paste_command.Command):
except paste_command.BadCommand, ex:
ex.args[0] = self.parser.error(ex.args[0])
raise
def get_package_names(self, config):
if not hasattr(config.app, 'modules'):
return []
return config.app.modules
def import_module(self, package, name):
parent = __import__(package, fromlist=[name])
return getattr(parent, name, None)
def load_configuration(self, name):
set_config(name)
return _runtime_conf
def load_app(self, config):
for package_name in self.get_package_names(config):
module = self.import_module(package_name, 'app')
if hasattr(module, 'setup_app'):
return module.setup_app(config)
raise paste_command.BadCommand('No app.setup_app found in any app modules')
def load_model(self, config):
for package_name in self.get_package_names(config):
module = self.import_module(package_name, 'model')
if module:
return module
return None
def load_app(self):
return load_app(self.validate_file(self.args))
def logging_file_config(self, config_file):
if os.path.splitext(config_file)[1].lower() == '.ini':
paste_command.Command.logging_file_config(self, config_file)
def validate_file(self, argv):
if not argv or not os.path.isfile(argv[0]):
raise paste_command.BadCommand('This command needs a valid config file.')
return argv[0]
def command(self):
pass

View File

@@ -45,25 +45,11 @@ class ServeCommand(_ServeCommand, Command):
setattr(self.options, 'server', None)
setattr(self.options, 'server_name', None)
config_file = self.validate_file(self.args)
# for file-watching to work, we need a filename, not a module
if self.requires_config_file and self.args:
self.config = self.load_configuration(config_file)
self.args[0] = self.config.__file__
if self.options.reload is None:
self.options.reload = getattr(self.config.app, 'reload', False)
# run the base command
_ServeCommand.command(self)
def loadserver(self, server_spec, name, relative_to, **kw):
return (lambda app: httpserver.serve(app, self.config.server.host, self.config.server.port))
return (lambda app: httpserver.serve(app, app.config.server.host, app.config.server.port))
def loadapp(self, app_spec, name, relative_to, **kw):
return self.load_app(self.config)
def validate_file(self, argv):
if not argv or not os.path.isfile(argv[0]):
raise paste_command.BadCommand('This command needs a valid config file.')
return argv[0]
return self.load_app()

View File

@@ -24,17 +24,14 @@ class ShellCommand(Command):
def command(self):
# load the application
config = self.load_configuration(self.args[0])
setattr(config.app, 'reload', False)
app = self.load_app(config)
app = self.load_app()
# prepare the locals
locs = dict(__name__='pecan-admin')
locs['wsgiapp'] = app
locs['app'] = TestApp(app)
# find the model for the app
model = self.load_model(config)
model = self.load_model(app.config)
if model:
locs['model'] = model
@@ -67,3 +64,10 @@ class ShellCommand(Command):
except ImportError:
pass
shell.interact(shell_banner + banner)
def load_model(self, config):
for package_name in getattr(config.app, 'modules', []):
module = __import__(package_name, fromlist=['model'])
if hasattr(module, 'model'):
return module.model
return None

View File

@@ -5,6 +5,27 @@ import os
IDENTIFIER = re.compile(r'[a-z_](\w)*$', re.IGNORECASE)
DEFAULT = {
# Server Specific Configurations
'server' : {
'port' : '8080',
'host' : '0.0.0.0'
},
# Pecan Application Configurations
'app' : {
'root' : None,
'modules' : [],
'static_root' : 'public',
'template_path' : '',
'debug' : False,
'force_canonical' : True,
'errors' : {
'__force_dict__' : True
}
}
}
class ConfigDict(dict):
pass
@@ -80,15 +101,6 @@ class Config(object):
conf_obj = dict(self)
return self.__dictify__(conf_obj, prefix)
def update_with_module(self, module):
'''
Updates this configuration with a module.
:param module: The module to update this configuration with. Either a string or the module itself.
'''
self.update(conf_from_module(module))
def __getattr__(self, name):
try:
return self.__values__[name]
@@ -123,20 +135,6 @@ class Config(object):
def __repr__(self):
return 'Config(%s)' % str(self.__values__)
def conf_from_module(module):
'''
Creates a configuration dictionary from a module.
:param module: The module, either as a string or the module itself.
'''
if isinstance(module, str):
module = import_module(module)
module_dict = dict(inspect.getmembers(module))
return conf_from_dict(module_dict)
def conf_from_file(filepath):
'''
@@ -173,48 +171,31 @@ def conf_from_dict(conf_dict):
return conf
def import_module(conf):
'''
Imports the a configuration as a module.
:param conf: The string to the configuration. Automatically strips off ".py" file extensions.
'''
if '.' in conf:
parts = conf.split('.')
name = '.'.join(parts[:-1])
fromlist = parts[-1:]
try:
module = __import__(name, fromlist=fromlist)
conf_mod = getattr(module, parts[-1])
except AttributeError, e:
raise ImportError('No module named %s' % conf)
else:
conf_mod = __import__(conf)
return conf_mod
def initconf():
'''
Initializes the default configuration and exposes it at ``pecan.configuration.conf``,
which is also exposed at ``pecan.conf``.
'''
import default_config
conf = conf_from_module(default_config)
return conf
return conf_from_dict(DEFAULT)
def set_config(name):
def set_config(config, overwrite=False):
'''
Updates the global configuration a filename.
:param name: filename, as a string.
:param config: Can be a dictionary containing configuration, or a string which
represents a (relative) configuration filename.
'''
_runtime_conf.update(conf_from_file(name))
if overwrite is True:
_runtime_conf.__values__ == {}
if isinstance(config, basestring):
_runtime_conf.update(conf_from_file(config))
elif isinstance(config, dict):
_runtime_conf.update(conf_from_dict(config))
else:
raise TypeError('%s is neither a dictionary of a string.' % config)
_runtime_conf = initconf()

View File

@@ -1,3 +1,4 @@
from configuration import _runtime_conf, set_config
from templating import RendererFactory
from routing import lookup_controller, NonCanonicalPath
from util import _cfg, splitext, encode_if_needed
@@ -171,6 +172,26 @@ class ValidationException(ForwardRequestException):
ForwardRequestException.__init__(self, location)
def load_app(config):
'''
Used to load a ``Pecan`` application and its environment based on passed
configuration.
:param config: Can be a dictionary containing configuration, or a string which
represents a (relative) configuration filename.
:returns a pecan.Pecan object
'''
set_config(config, overwrite=True)
for package_name in getattr(_runtime_conf.app, 'modules', []):
module = __import__(package_name, fromlist=['app'])
if hasattr(module, 'app') and hasattr(module.app, 'setup_app'):
app = module.app.setup_app(_runtime_conf)
app.config = _runtime_conf
return app
raise RuntimeError('No app.setup_app found in any of the configured app.modules')
class Pecan(object):
'''
Base Pecan application object. Generally created using ``pecan.make_app``,
@@ -438,11 +459,14 @@ class Pecan(object):
elif cfg.get('content_type') is not None and \
request.pecan['content_type'] not in cfg.get('content_types', {}):
print "Controller '%s' defined does not support content_type '%s'. Supported type(s): %s" % (
import warnings
warnings.warn("Controller '%s' defined does not support content_type '%s'. Supported type(s): %s" % (
controller.__name__,
request.pecan['content_type'],
cfg.get('content_types', {}).keys()
)
),
RuntimeWarning
)
raise exc.HTTPNotFound
# get a sorted list of hooks, by priority

View File

@@ -1,25 +0,0 @@
# Server Specific Configurations
server = {
'port' : '8080',
'host' : '0.0.0.0'
}
# Pecan Application Configurations
app = {
'root' : None,
'modules' : [],
'static_root' : 'public',
'template_path' : '',
'debug' : False,
'force_canonical' : True,
'errors' : {
'__force_dict__' : True
}
}
# Custom Configurations must be in Python dictionary format in user config
#
# foo = {'bar':'baz'}
#
# All configurations are accessible at::
# pecan.conf

View File

@@ -1,14 +1,4 @@
from configuration import set_config, import_module, _runtime_conf as conf
def deploy(config_module_or_path):
set_config(config_module_or_path)
for module in getattr(conf.app, 'modules'):
try:
module_app = import_module('%s.app' % module)
if hasattr(module_app, 'setup_app'):
return module_app.setup_app(conf)
except ImportError:
continue
raise Exception, 'No app.setup_app found in any of the configured app.modules'
from core import load_app
def deploy(config):
return load_app(config)

View File

@@ -1,226 +0,0 @@
"""
Plugin for py.test that sets up the app.
App configuration inspired by the Pylons nose equivalent:
https://github.com/Pylons/pylons/blob/master/pylons/test.py
Handling of multiprocessing inspired by pytest-cov.
"""
from configuration import set_config, _runtime_conf as conf
from tempfile import mkdtemp
import py
import py.test
import os
import shutil
import socket
import sys
def pytest_addoption(parser):
"""
Adds the custom "with-config" option to take in the config file.
"""
group = parser.getgroup('pecan')
group._addoption('--with-config',
dest='config_file',
metavar='path',
default='./test.py',
action='store',
type='string',
help='configuration file for pecan tests')
def pytest_configure(config):
"""
Loads the Pecan plugin if using a configuration file.
"""
if config.getvalue('config_file'):
config.pluginmanager.register(PecanPlugin(config), '_pecan')
class PecanPlugin(object):
"""
Plugin for a Pecan application. Sets up and tears down the
WSGI application based on the configuration and session type.
"""
def __init__(self, config):
self.config = config
self.impl = None
def pytest_namespace(self):
"""
Add the session variables to the namespace.
"""
return {
'temp_dir': None,
'wsgi_app': None
}
def pytest_sessionstart(self, session):
"""
Set up the testing session.
"""
self.impl = PecanPluginImpl.create_from_session(session)
self.impl.sessionstart(session)
def pytest_configure_node(self, node):
"""
Configures a new slave node.
"""
if self.impl:
self.impl.configure_node(node)
def pytest_runtest_setup(self, item):
if self.impl:
self.impl.ensure_config_loaded()
def pytest_testnodedown(self, node, error):
"""
Tears down an exiting node.
"""
if self.impl:
self.impl.testnodedown(node, error)
def pytest_sessionfinish(self, session, exitstatus):
"""
Cleans up the testing session.
"""
if self.impl:
self.impl.sessionfinish(session, exitstatus)
class PecanPluginImpl(object):
"""
Actual implementation of the Pecan plugin. This ensures the proper
environment is configured for each session type.
"""
def __init__(self, config):
self.config = config
self.log = py.log.Producer('pecan-%s' % self.name)
if not config.option.debug:
py.log.setconsumer(self.log._keywords, None)
self.log('Created %s instance' % self.__class__.__name__)
@property
def name(self):
return 'main'
def _setup_app(self):
self.log('Invoking setup_app')
path = os.getcwd()
if path not in sys.path:
sys.path.insert(0, path)
set_config(self.config.getvalue('config_file'))
py.test.wsgi_app = self._load_app(conf)
def _get_package_names(self, config):
if not hasattr(config.app, 'modules'):
return []
return [module.__name__ for module in config.app.modules if hasattr(module, '__name__')]
def _can_import(self, name):
try:
__import__(name)
return True
except ImportError:
return False
def _load_app(self, config):
for package_name in self._get_package_names(config):
module_name = '%s.app' % package_name
if self._can_import(module_name):
module = sys.modules[module_name]
if hasattr(module, 'setup_app'):
return module.setup_app(config)
raise RuntimeError('No app.setup_app found in any of the configured app.modules')
def _create_temp_directory(self):
temp_dir = mkdtemp()
self.log('Created temporary directory %s' % temp_dir)
py.test.temp_dir = temp_dir
def _delete_temp_directory(self):
if py.test.temp_dir and os.path.exists(py.test.temp_dir):
self.log('Removing temporary directory %s' % py.test.temp_dir)
shutil.rmtree(py.test.temp_dir)
def sessionstart(self, session):
self.log('Starting session')
self._create_temp_directory()
def ensure_config_loaded(self):
if not hasattr(py.test, 'wsgi_app') or py.test.wsgi_app is None:
self._setup_app()
def configure_node(self, node):
pass
def testnodedown(self, node, error):
pass
def sessionfinish(self, session, exitstatus):
self.log('Stopping session')
self._delete_temp_directory()
@staticmethod
def create_from_session(session):
if session.config.option.dist != 'no':
impl_cls = MasterPecanPluginImpl
elif getattr(session.config, 'slaveinput', {}).get('slaveid'):
impl_cls = SlavePecanPluginImpl
else:
impl_cls = PecanPluginImpl
return impl_cls(session.config)
class MasterPecanPluginImpl(PecanPluginImpl):
"""
Plugin implementation for distributed master.
"""
def sessionstart(self, session):
self.log('Starting master session')
self._create_temp_directory()
self._setup_app()
def configure_node(self, node):
self.log('Configuring slave node %s' % node.gateway.id)
node.slaveinput['pecan_master_host'] = socket.gethostname()
node.slaveinput['pecan_temp_dir'] = py.test.temp_dir
def sessionfinish(self, session, exitstatus):
self.log('Stopping master session')
self._delete_temp_directory()
class SlavePecanPluginImpl(PecanPluginImpl):
"""
Plugin implementation for distributed slaves.
"""
@property
def name(self):
return self.config.slaveinput['slaveid']
def _is_collocated(self, session):
return (socket.gethostname() == session.config.slaveinput['pecan_master_host'])
def _set_temp_directory(self, session):
self.log('Setting temporary directory to %s' % session.config.slaveinput['pecan_temp_dir'])
py.test.temp_dir = session.config.slaveinput['pecan_temp_dir']
def sessionstart(self, session):
self.log('Starting slave session')
if self._is_collocated(session):
self._set_temp_directory(session)
else:
self._create_temp_directory()
self._setup_app()
def sessionfinish(self, session, exitstatus):
self.log('Stopping slave session')
if not self._is_collocated(session):
self._delete_temp_directory()

View File

@@ -39,7 +39,11 @@ def lookup_controller(obj, url_path):
cross_boundary(prev_obj, obj)
break
except TypeError, te:
print 'Got exception calling lookup(): %s (%s)' % (te, te.args)
import warnings
warnings.warn(
'Got exception calling lookup(): %s (%s)' % (te, te.args),
RuntimeWarning
)
else:
raise exc.HTTPNotFound

5
pecan/testing.py Normal file
View File

@@ -0,0 +1,5 @@
from pecan import load_app
from webtest import TestApp
def load_test_app(config):
return TestApp(load_app(config))

View File

@@ -943,7 +943,7 @@ class TestLogging(TestCase):
assert len(writes) == 1
class TestEngines(object):
class TestEngines(TestCase):
template_path = os.path.join(os.path.dirname(__file__), 'templates')

View File

@@ -4,14 +4,10 @@ from unittest import TestCase
from pecan import configuration
from pecan import conf as _runtime_conf
__here__ = os.path.dirname(__file__)
class TestConf(TestCase):
def setUp(self):
test_config_d = os.path.join(os.path.dirname(__file__), 'test_config')
if test_config_d not in sys.path:
sys.path.append(test_config_d)
def test_update_config_fail_identifier(self):
"""Fail when naming does not pass correctness"""
bad_dict = {'bad name':'value'}
@@ -21,7 +17,10 @@ class TestConf(TestCase):
"""Update an empty configuration with the default values"""
conf = configuration.initconf()
conf.update_with_module('config')
conf.update(configuration.conf_from_file(os.path.join(
__here__,
'test_config/config.py'
)))
self.assertTrue(conf.app.debug)
self.assertEqual(conf.app.root, None)
@@ -35,7 +34,10 @@ class TestConf(TestCase):
"""Update an empty configuration with the default values"""
conf = configuration.initconf()
conf.update_with_module('empty')
conf.update(configuration.conf_from_file(os.path.join(
__here__,
'test_config/empty.py'
)))
self.assertFalse(conf.app.debug)
self.assertEqual(conf.app.root, None)
@@ -48,7 +50,10 @@ class TestConf(TestCase):
def test_update_force_dict(self):
"""Update an empty configuration with the default values"""
conf = configuration.initconf()
conf.update_with_module('forcedict')
conf.update(configuration.conf_from_file(os.path.join(
__here__,
'test_config/forcedict.py'
)))
self.assertFalse(conf.app.debug)
self.assertEqual(conf.app.root, None)
@@ -70,12 +75,6 @@ class TestConf(TestCase):
conf['attr'] = d
self.assertTrue(conf.attr.attr)
def test_config_dirname(self):
from pecan import default_config
conf = configuration.initconf()
conf['path'] = '%(confdir)s'
self.assertEqual(conf.path, os.path.dirname(default_config.__file__))
def test_config_repr(self):
conf = configuration.Config({'a':1})
self.assertEqual(repr(conf),"Config({'a': 1})")
@@ -92,16 +91,50 @@ class TestConf(TestCase):
def test_config_illegal_ids(self):
conf = configuration.Config({})
conf.update_with_module('bad.module_and_underscore')
conf.update(configuration.conf_from_file(os.path.join(
__here__,
'test_config/bad/module_and_underscore.py'
)))
self.assertEqual([], list(conf))
def test_config_bad_module(self):
def test_config_missing_file(self):
path = ('doesnotexist.py',)
conf = configuration.Config({})
self.assertRaises(ImportError, conf.update_with_module, 'doesnotexist')
self.assertRaises(ImportError, conf.update_with_module, 'bad.doesnotexists')
self.assertRaises(ImportError, conf.update_with_module, 'bad.bad.doesnotexist')
self.assertRaises(SyntaxError, conf.update_with_module, 'bad.syntaxerror')
self.assertRaises(ImportError, conf.update_with_module, 'bad.importerror')
self.assertRaises(IOError, configuration.conf_from_file, os.path.join(
__here__,
'test_config',
*path
))
def test_config_missing_file_on_path(self):
path = ('bad', 'bad', 'doesnotexist.py',)
conf = configuration.Config({})
self.assertRaises(IOError, configuration.conf_from_file, os.path.join(
__here__,
'test_config',
*path
))
def test_config_with_syntax_error(self):
path = ('bad', 'syntaxerror.py')
conf = configuration.Config({})
self.assertRaises(SyntaxError, configuration.conf_from_file, os.path.join(
__here__,
'test_config',
*path
))
def test_config_with_bad_import(self):
path = ('bad', 'importerror.py')
conf = configuration.Config({})
self.assertRaises(ImportError, configuration.conf_from_file, os.path.join(
__here__,
'test_config',
*path
))
def test_config_set_from_file(self):
path = os.path.join(os.path.dirname(__file__), 'test_config', 'empty.py')

View File

@@ -1,58 +0,0 @@
from pecan.deploy import deploy
from unittest import TestCase
import os
import sys
class TestDeploy(TestCase):
def setUp(self):
test_config_d = os.path.join(os.path.dirname(__file__), 'test_config', 'sample_apps')
if test_config_d not in sys.path:
sys.path.append(test_config_d)
def test_module_lookup(self):
"""
1. A config file has:
app { 'modules': ['valid_module'] }
2. The module, `valid_module` has an app.py that defines a `def setup.py`
"""
test_config_file = os.path.join(os.path.dirname(__file__), 'test_config', 'sample_apps', 'sample_app_config.py')
assert deploy(test_config_file) == 'DEPLOYED!'
def test_module_lookup_find_best_match(self):
"""
1. A config file has:
app { 'modules': ['invalid_module', 'valid_module'] }
2. The module, `valid_module` has an app.py that defines a `def setup_app`
"""
test_config_file = os.path.join(os.path.dirname(__file__), 'test_config', 'sample_apps', 'sample_app_config.py')
assert deploy(test_config_file) == 'DEPLOYED!'
def test_missing_app_file_lookup(self):
"""
1. A config file has:
app { 'modules': ['valid_module'] }
2. The module has no `app.py` file.
"""
test_config_file = os.path.join(os.path.dirname(__file__), 'test_config', 'sample_apps', 'sample_app_config_missing.py')
self.assertRaises(
Exception,
deploy,
test_config_file
)
def test_missing_setup_app(self):
"""
1. A config file has:
app { 'modules': ['valid_module'] }
2. The module, `valid_module` has an `app.py` that contains no `def setup_app`
"""
test_config_file = os.path.join(os.path.dirname(__file__), 'test_config', 'sample_apps', 'sample_app_config_missing_app.py')
self.assertRaises(
Exception,
deploy,
test_config_file
)

View File

@@ -1,4 +1,5 @@
from pecan import Pecan, expose, request, response, redirect
from unittest import TestCase
from webtest import TestApp
try:
from simplejson import dumps
@@ -6,7 +7,7 @@ except:
from json import dumps
class TestGeneric(object):
class TestGeneric(TestCase):
def test_simple_generic(self):
class RootController(object):

View File

@@ -5,11 +5,12 @@ from pecan.hooks import PecanHook, TransactionHook, HookController, Requ
from pecan.configuration import Config
from pecan.decorators import transactional, after_commit, after_rollback
from copy import copy
from unittest import TestCase
from formencode import Schema, validators
from webtest import TestApp
class TestHooks(object):
class TestHooks(TestCase):
def test_basic_single_hook(self):
run_hook = []
@@ -446,7 +447,7 @@ class TestHooks(object):
assert run_hook[5] == 'inside'
assert run_hook[6] == 'after'
class TestTransactionHook(object):
class TestTransactionHook(TestCase):
def test_transaction_hook(self):
run_hook = []
@@ -1109,7 +1110,7 @@ class TestTransactionHook(object):
assert run_hook[3] == 'clear'
class TestRequestViewerHook(object):
class TestRequestViewerHook(TestCase):
def test_hook_from_config(self):
from pecan.configuration import _runtime_conf as conf

View File

@@ -48,7 +48,7 @@ def test_simple_rule():
assert len(result) == 1
class TestJsonify(object):
class TestJsonify(TestCase):
def test_simple_jsonify(self):
Person = make_person()

View File

@@ -1,5 +1,6 @@
from pecan import abort, expose, make_app, request, response
from pecan.rest import RestController
from unittest import TestCase
from webtest import TestApp
try:
from simplejson import dumps, loads
@@ -9,7 +10,7 @@ except:
import formencode
class TestRestController(object):
class TestRestController(TestCase):
def test_basic_rest(self):

View File

@@ -9,7 +9,7 @@ try:
except:
from sets import Set as set
class TestSecure(object):
class TestSecure(TestCase):
def test_simple_secure(self):
authorized = False

View File

@@ -1,8 +1,9 @@
import os
from pecan import expose, make_app
from unittest import TestCase
from webtest import TestApp
class TestStatic(object):
class TestStatic(TestCase):
def test_simple_static(self):
class RootController(object):

View File

@@ -1,5 +1,6 @@
from formencode import ForEach, Schema, validators
from webtest import TestApp
from unittest import TestCase
import os.path
@@ -12,7 +13,7 @@ except ImportError:
from json import dumps
class TestValidation(object):
class TestValidation(TestCase):
template_path = os.path.join(os.path.dirname(__file__), 'templates')

View File

@@ -1,3 +1,10 @@
[nosetests]
match=^test
where=pecan
nocapture=1
cover-package=pecan
cover-erase=1
[egg_info]
tag_build = dev
tag_svn_revision = true