summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBert JW Regeer <xistence@0x58.com>2016-12-09 23:01:23 -0700
committerGitHub <noreply@github.com>2016-12-09 23:01:23 -0700
commit98b7bc973092cb92395ecfc50c097793d00e6551 (patch)
tree1975f4d89d0422aaabd09b9bf94ac0da93534f8d
parent2d45def603f038a8533eb9790640982012c0be30 (diff)
parent1fde5f47b8b6208a25e951c6d3887cc73cc3696e (diff)
downloadpyramid-98b7bc973092cb92395ecfc50c097793d00e6551.tar.gz
pyramid-98b7bc973092cb92395ecfc50c097793d00e6551.tar.bz2
pyramid-98b7bc973092cb92395ecfc50c097793d00e6551.zip
Merge pull request #2827 from mmerickel/pserve-watch-files
support a [pserve] config section with a list of files to watch
-rw-r--r--CHANGES.txt4
-rw-r--r--docs/narr/project.rst22
-rw-r--r--pyramid/scripts/pserve.py90
-rw-r--r--pyramid/tests/test_scripts/dummy.py8
-rw-r--r--pyramid/tests/test_scripts/test_pserve.py33
5 files changed, 112 insertions, 45 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index f3883b557..11eab9f26 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -119,6 +119,10 @@ Features
See https://github.com/Pylons/pyramid/pull/2805
+- A new ``[pserve]`` section is supported in your config files with a
+ ``watch_files`` key that can configure ``pserve --reload`` to monitor custom
+ file paths. See https://github.com/Pylons/pyramid/pull/2827
+
- Allow streaming responses to be made from subclasses of
``pyramid.httpexceptions.HTTPException``. Previously the response would
be unrolled while testing for a body, making it impossible to stream
diff --git a/docs/narr/project.rst b/docs/narr/project.rst
index 6c42881f4..77c637571 100644
--- a/docs/narr/project.rst
+++ b/docs/narr/project.rst
@@ -1065,3 +1065,25 @@ hard drive, you should install the
`watchdog <http://pythonhosted.org/watchdog/>` package in development.
``hupper`` will automatically use ``watchdog`` to more efficiently poll the
filesystem.
+
+Monitoring Custom Files
+~~~~~~~~~~~~~~~~~~~~~~~
+
+By default, ``pserve --reload`` will monitor all imported Python code
+(everything in ``sys.modules``) as well as the config file passed to
+``pserve`` (e.g. ``development.ini``). You can instruct ``pserve`` to watch
+other files for changes as well by defining a ``[pserve]`` section in your
+configuration file. For example, let's say your application loads the
+``favicon.ico`` file at startup and stores it in memory to efficiently
+serve it many times. When you change it you want ``pserve`` to restart:
+
+.. code-block:: ini
+
+ [pserve]
+ watch_files =
+ myapp/static/favicon.ico
+
+Paths may be absolute or relative to the configuration file. They may also
+be an :term:`asset specification`. These paths are passed to ``hupper`` which
+has some basic support for globbing. Acceptable glob patterns depend on the
+version of Python being used.
diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py
index 969bc07f1..b8776d44f 100644
--- a/pyramid/scripts/pserve.py
+++ b/pyramid/scripts/pserve.py
@@ -28,9 +28,12 @@ from paste.deploy.loadwsgi import (
)
from pyramid.compat import PY2
+from pyramid.compat import configparser
from pyramid.scripts.common import parse_vars
from pyramid.scripts.common import setup_logging
+from pyramid.path import AssetResolver
+from pyramid.settings import aslist
def main(argv=sys.argv, quiet=False):
command = PServeCommand(argv, quiet=quiet)
@@ -97,12 +100,17 @@ class PServeCommand(object):
dest='verbose',
help="Suppress verbose output")
+ ConfigParser = configparser.ConfigParser # testing
+ loadapp = staticmethod(loadapp) # testing
+ loadserver = staticmethod(loadserver) # testing
+
_scheme_re = re.compile(r'^[a-z][a-z]+:', re.I)
def __init__(self, argv, quiet=False):
self.options, self.args = self.parser.parse_args(argv[1:])
if quiet:
self.options.verbose = 0
+ self.watch_files = []
def out(self, msg): # pragma: no cover
if self.options.verbose > 0:
@@ -112,6 +120,32 @@ class PServeCommand(object):
restvars = self.args[1:]
return parse_vars(restvars)
+ def pserve_file_config(self, filename, global_conf=None):
+ here = os.path.abspath(os.path.dirname(filename))
+ defaults = {}
+ if global_conf:
+ defaults.update(global_conf)
+ defaults['here'] = here
+
+ config = self.ConfigParser(defaults=defaults)
+ config.optionxform = str
+ config.read(filename)
+ try:
+ items = dict(config.items('pserve'))
+ except configparser.NoSectionError:
+ return
+
+ watch_files = aslist(items.get('watch_files', ''), flatten=False)
+
+ # track file paths relative to the ini file
+ resolver = AssetResolver(package=None)
+ for file in watch_files:
+ if ':' in file:
+ file = resolver.resolve(file).abspath()
+ elif not os.path.isabs(file):
+ file = os.path.join(here, file)
+ self.watch_files.append(os.path.abspath(file))
+
def run(self): # pragma: no cover
if not self.args:
self.out('You must give a config file')
@@ -121,8 +155,12 @@ class PServeCommand(object):
vars = self.get_options()
app_name = self.options.app_name
+ base = os.getcwd()
if not self._scheme_re.search(app_spec):
+ config_path = os.path.join(base, app_spec)
app_spec = 'config:' + app_spec
+ else:
+ config_path = None
server_name = self.options.server_name
if self.options.server:
server_spec = 'egg:pyramid'
@@ -130,7 +168,6 @@ class PServeCommand(object):
server_name = self.options.server
else:
server_spec = app_spec
- base = os.getcwd()
# do not open the browser on each reload so check hupper first
if self.options.browser and not hupper.is_active():
@@ -155,22 +192,17 @@ class PServeCommand(object):
)
return 0
+ if config_path:
+ setup_logging(config_path, global_conf=vars)
+ self.pserve_file_config(config_path, global_conf=vars)
+ self.watch_files.append(config_path)
+
if hupper.is_active():
reloader = hupper.get_reloader()
- if app_spec.startswith('config:'):
- reloader.watch_files([app_spec[len('config:'):]])
-
- log_fn = app_spec
- if log_fn.startswith('config:'):
- log_fn = app_spec[len('config:'):]
- elif log_fn.startswith('egg:'):
- log_fn = None
- if log_fn:
- log_fn = os.path.join(base, log_fn)
- setup_logging(log_fn, global_conf=vars)
+ reloader.watch_files(self.watch_files)
- server = self.loadserver(server_spec, name=server_name,
- relative_to=base, global_conf=vars)
+ server = self.loadserver(
+ server_spec, name=server_name, relative_to=base, global_conf=vars)
app = self.loadapp(
app_spec, name=app_name, relative_to=base, global_conf=vars)
@@ -182,26 +214,16 @@ class PServeCommand(object):
msg = 'Starting server.'
self.out(msg)
- def serve():
- try:
- server(app)
- except (SystemExit, KeyboardInterrupt) as e:
- if self.options.verbose > 1:
- raise
- if str(e):
- msg = ' ' + str(e)
- else:
- msg = ''
- self.out('Exiting%s (-v to see traceback)' % msg)
-
- serve()
-
- def loadapp(self, app_spec, name, relative_to, **kw): # pragma: no cover
- return loadapp(app_spec, name=name, relative_to=relative_to, **kw)
-
- def loadserver(self, server_spec, name, relative_to, **kw):# pragma:no cover
- return loadserver(
- server_spec, name=name, relative_to=relative_to, **kw)
+ try:
+ server(app)
+ except (SystemExit, KeyboardInterrupt) as e:
+ if self.options.verbose > 1:
+ raise
+ if str(e):
+ msg = ' ' + str(e)
+ else:
+ msg = ''
+ self.out('Exiting%s (-v to see traceback)' % msg)
# For paste.deploy server instantiation (egg:pyramid#wsgiref)
def wsgiref_server_runner(wsgi_app, global_conf, **kw): # pragma: no cover
diff --git a/pyramid/tests/test_scripts/dummy.py b/pyramid/tests/test_scripts/dummy.py
index f3aa20e7c..ced09d0b0 100644
--- a/pyramid/tests/test_scripts/dummy.py
+++ b/pyramid/tests/test_scripts/dummy.py
@@ -82,8 +82,9 @@ class DummyMultiView(object):
self.__request_attrs__ = attrs
class DummyConfigParser(object):
- def __init__(self, result):
+ def __init__(self, result, defaults=None):
self.result = result
+ self.defaults = defaults
def read(self, filename):
self.filename = filename
@@ -98,8 +99,9 @@ class DummyConfigParser(object):
class DummyConfigParserFactory(object):
items = None
- def __call__(self):
- self.parser = DummyConfigParser(self.items)
+ def __call__(self, defaults=None):
+ self.defaults = defaults
+ self.parser = DummyConfigParser(self.items, defaults)
return self.parser
class DummyCloser(object):
diff --git a/pyramid/tests/test_scripts/test_pserve.py b/pyramid/tests/test_scripts/test_pserve.py
index e84de92d4..18f7c8c2f 100644
--- a/pyramid/tests/test_scripts/test_pserve.py
+++ b/pyramid/tests/test_scripts/test_pserve.py
@@ -1,9 +1,14 @@
+import os
import unittest
+from pyramid.tests.test_scripts import dummy
+
+here = os.path.abspath(os.path.dirname(__file__))
class TestPServeCommand(unittest.TestCase):
def setUp(self):
from pyramid.compat import NativeIO
self.out_ = NativeIO()
+ self.config_factory = dummy.DummyConfigParserFactory()
def out(self, msg):
self.out_.write(msg)
@@ -11,7 +16,6 @@ class TestPServeCommand(unittest.TestCase):
def _get_server(*args, **kwargs):
def server(app):
return ''
-
return server
def _getTargetClass(self):
@@ -23,6 +27,7 @@ class TestPServeCommand(unittest.TestCase):
effargs.extend(args)
cmd = self._getTargetClass()(effargs)
cmd.out = self.out
+ cmd.ConfigParser = self.config_factory
return cmd
def test_run_no_args(self):
@@ -38,20 +43,15 @@ class TestPServeCommand(unittest.TestCase):
self.assertEqual(result, {'a': '1', 'b': '2'})
def test_parse_vars_good(self):
- from pyramid.tests.test_scripts.dummy import DummyApp
-
inst = self._makeOne('development.ini', 'a=1', 'b=2')
inst.loadserver = self._get_server
-
- app = DummyApp()
-
+ app = dummy.DummyApp()
def get_app(*args, **kwargs):
app.global_conf = kwargs.get('global_conf', None)
-
inst.loadapp = get_app
- inst.run()
+ inst.run()
self.assertEqual(app.global_conf, {'a': '1', 'b': '2'})
def test_parse_vars_bad(self):
@@ -59,6 +59,23 @@ class TestPServeCommand(unittest.TestCase):
inst.loadserver = self._get_server
self.assertRaises(ValueError, inst.run)
+ def test_config_file_finds_watch_files(self):
+ inst = self._makeOne('development.ini')
+ self.config_factory.items = [(
+ 'watch_files',
+ 'foo\n/baz\npyramid.tests.test_scripts:*.py',
+ )]
+ inst.pserve_file_config('/base/path.ini', global_conf={'a': '1'})
+ self.assertEqual(self.config_factory.defaults, {
+ 'a': '1',
+ 'here': os.path.abspath('/base'),
+ })
+ self.assertEqual(inst.watch_files, [
+ os.path.abspath('/base/foo'),
+ os.path.abspath('/baz'),
+ os.path.abspath(os.path.join(here, '*.py')),
+ ])
+
class Test_main(unittest.TestCase):
def _callFUT(self, argv):
from pyramid.scripts.pserve import main