From bc26debd9ed2a46fca1b0931c78b4054bd37841d Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 25 Dec 2014 23:42:05 -0800 Subject: Add support for passing unbound class methods to `add_view` --- pyramid/config/views.py | 13 ++++++++++++- pyramid/tests/test_config/test_views.py | 21 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index c01b72e12..3e305055f 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -42,7 +42,8 @@ from pyramid.compat import ( url_quote, WIN, is_bound_method, - is_nonstr_iter + is_nonstr_iter, + im_self, ) from pyramid.exceptions import ( @@ -418,6 +419,16 @@ class DefaultViewMapper(object): self.attr = kw.get('attr') def __call__(self, view): + # Map the attr directly if the passed in view is method and a + # constructor is defined and must be unbound (for backwards + # compatibility) + if inspect.ismethod(view): + is_bound = getattr(view, im_self, None) is not None + + if not is_bound: + self.attr = view.__name__ + view = view.im_class + if inspect.isclass(view): view = self.map_class(view) else: diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index b0d03fb72..664208fad 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1666,6 +1666,27 @@ class TestViewsConfigurationMixin(unittest.TestCase): renderer=null_renderer) self.assertRaises(ConfigurationConflictError, config.commit) + def test_add_view_class_method_no_attr(self): + from pyramid.renderers import null_renderer + from zope.interface import directlyProvides + + class ViewClass(object): + def __init__(self, request): + self.request = request + + def run(self): + return 'OK' + + config = self._makeOne(autocommit=True) + config.add_view(view=ViewClass.run, renderer=null_renderer) + + wrapper = self._getViewCallable(config) + context = DummyContext() + directlyProvides(context, IDummy) + request = self._makeRequest(config) + result = wrapper(context, request) + self.assertEqual(result, 'OK') + def test_derive_view_function(self): from pyramid.renderers import null_renderer def view(request): -- cgit v1.2.3 From 4a7029f6b313b65ba94d0726042ea3adbad38e81 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 26 Dec 2014 22:48:41 -0800 Subject: Raise errors if unbound methods are passed in --- pyramid/compat.py | 17 ++++++++++++ pyramid/config/views.py | 15 +++++------ pyramid/tests/test_compat.py | 46 +++++++++++++++++++++++++++++++++ pyramid/tests/test_config/test_views.py | 21 +++++---------- 4 files changed, 76 insertions(+), 23 deletions(-) create mode 100644 pyramid/tests/test_compat.py diff --git a/pyramid/compat.py b/pyramid/compat.py index bfa345b88..749435ebc 100644 --- a/pyramid/compat.py +++ b/pyramid/compat.py @@ -244,3 +244,20 @@ else: def is_bound_method(ob): return inspect.ismethod(ob) and getattr(ob, im_self, None) is not None +def is_unbound_method(fn): + """ + This consistently verifies that the callable is bound to a + class. + """ + is_bound = is_bound_method(fn) + + if not is_bound and inspect.isroutine(fn): + spec = inspect.getargspec(fn) + has_self = len(spec.args) > 0 and spec.args[0] == 'self' + + if PY3 and inspect.isfunction(fn) and has_self: # pragma: no cover + return True + elif inspect.ismethod(fn): + return True + + return False diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 3e305055f..d498395e1 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -42,6 +42,7 @@ from pyramid.compat import ( url_quote, WIN, is_bound_method, + is_unbound_method, is_nonstr_iter, im_self, ) @@ -419,15 +420,11 @@ class DefaultViewMapper(object): self.attr = kw.get('attr') def __call__(self, view): - # Map the attr directly if the passed in view is method and a - # constructor is defined and must be unbound (for backwards - # compatibility) - if inspect.ismethod(view): - is_bound = getattr(view, im_self, None) is not None - - if not is_bound: - self.attr = view.__name__ - view = view.im_class + if is_unbound_method(view) and self.attr is None: + raise ConfigurationError(( + 'Unbound method calls are not supported, please set the class ' + 'as your `view` and the method as your `attr`' + )) if inspect.isclass(view): view = self.map_class(view) diff --git a/pyramid/tests/test_compat.py b/pyramid/tests/test_compat.py new file mode 100644 index 000000000..2f80100dd --- /dev/null +++ b/pyramid/tests/test_compat.py @@ -0,0 +1,46 @@ +import unittest + +class TestUnboundMethods(unittest.TestCase): + def test_old_style_bound(self): + from pyramid.compat import is_unbound_method + + class OldStyle: + def run(self): + return 'OK' + + self.assertFalse(is_unbound_method(OldStyle().run)) + + def test_new_style_bound(self): + from pyramid.compat import is_unbound_method + + class NewStyle(object): + def run(self): + return 'OK' + + self.assertFalse(is_unbound_method(NewStyle().run)) + + def test_old_style_unbound(self): + from pyramid.compat import is_unbound_method + + class OldStyle: + def run(self): + return 'OK' + + self.assertTrue(is_unbound_method(OldStyle.run)) + + def test_new_style_unbound(self): + from pyramid.compat import is_unbound_method + + class NewStyle(object): + def run(self): + return 'OK' + + self.assertTrue(is_unbound_method(NewStyle.run)) + + def test_normal_func_unbound(self): + from pyramid.compat import is_unbound_method + + def func(): + return 'OK' + + self.assertFalse(is_unbound_method(func)) diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index 664208fad..d1eb1ed3c 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -1669,23 +1669,16 @@ class TestViewsConfigurationMixin(unittest.TestCase): def test_add_view_class_method_no_attr(self): from pyramid.renderers import null_renderer from zope.interface import directlyProvides - - class ViewClass(object): - def __init__(self, request): - self.request = request - - def run(self): - return 'OK' + from pyramid.exceptions import ConfigurationError config = self._makeOne(autocommit=True) - config.add_view(view=ViewClass.run, renderer=null_renderer) + class DummyViewClass(object): + def run(self): pass - wrapper = self._getViewCallable(config) - context = DummyContext() - directlyProvides(context, IDummy) - request = self._makeRequest(config) - result = wrapper(context, request) - self.assertEqual(result, 'OK') + def configure_view(): + config.add_view(view=DummyViewClass.run, renderer=null_renderer) + + self.assertRaises(ConfigurationError, configure_view) def test_derive_view_function(self): from pyramid.renderers import null_renderer -- cgit v1.2.3 From 6d4676137885f63f364a2b2ae6205c6931a57220 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 26 Dec 2014 23:04:56 -0800 Subject: Don't need im_self --- pyramid/config/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyramid/config/views.py b/pyramid/config/views.py index d498395e1..afacc1e0b 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -44,7 +44,6 @@ from pyramid.compat import ( is_bound_method, is_unbound_method, is_nonstr_iter, - im_self, ) from pyramid.exceptions import ( -- cgit v1.2.3 From 03a0d79306b2846313df1983a721d5cccf4ec3ce Mon Sep 17 00:00:00 2001 From: John Anderson Date: Fri, 26 Dec 2014 23:19:32 -0800 Subject: Clean up compat tests --- pyramid/tests/test_compat.py | 36 ++++++++---------------------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/pyramid/tests/test_compat.py b/pyramid/tests/test_compat.py index 2f80100dd..23ccce82e 100644 --- a/pyramid/tests/test_compat.py +++ b/pyramid/tests/test_compat.py @@ -1,46 +1,26 @@ import unittest +from pyramid.compat import is_unbound_method class TestUnboundMethods(unittest.TestCase): def test_old_style_bound(self): - from pyramid.compat import is_unbound_method - - class OldStyle: - def run(self): - return 'OK' - self.assertFalse(is_unbound_method(OldStyle().run)) def test_new_style_bound(self): - from pyramid.compat import is_unbound_method - - class NewStyle(object): - def run(self): - return 'OK' - self.assertFalse(is_unbound_method(NewStyle().run)) def test_old_style_unbound(self): - from pyramid.compat import is_unbound_method - - class OldStyle: - def run(self): - return 'OK' - self.assertTrue(is_unbound_method(OldStyle.run)) def test_new_style_unbound(self): - from pyramid.compat import is_unbound_method - - class NewStyle(object): - def run(self): - return 'OK' - self.assertTrue(is_unbound_method(NewStyle.run)) def test_normal_func_unbound(self): - from pyramid.compat import is_unbound_method - - def func(): - return 'OK' + def func(): return 'OK' self.assertFalse(is_unbound_method(func)) + +class OldStyle: + def run(self): return 'OK' + +class NewStyle(object): + def run(self): return 'OK' -- cgit v1.2.3 From 9e7248258800ef2c7072f497901172ab94988708 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 1 Jan 2015 20:06:48 -0800 Subject: Catch bad `name` and raise a `ValueError` --- pyramid/tests/test_config/test_factories.py | 18 +++++++++++++++++- pyramid/util.py | 10 +++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py index 6e679397f..5ae486c4b 100644 --- a/pyramid/tests/test_config/test_factories.py +++ b/pyramid/tests/test_config/test_factories.py @@ -111,6 +111,22 @@ class TestFactoriesMixin(unittest.TestCase): config = self._makeOne(autocommit=True) self.assertRaises(AttributeError, config.add_request_method) + def test_add_request_method_with_text_type_name(self): + from pyramid.interfaces import IRequestExtensions + from pyramid.compat import text_ + from pyramid.util import InstancePropertyMixin + + config = self._makeOne(autocommit=True) + def boomshaka(r): pass + name = text_(b'La Pe\xc3\xb1a', 'utf-8') + config.add_request_method(boomshaka, name=name) + exts = config.registry.getUtility(IRequestExtensions) + inst = InstancePropertyMixin() + + def set_extensions(): + inst._set_extensions(exts) + self.assertRaises(ValueError, set_extensions) + self.assertTrue(name in exts.methods) class TestDeprecatedFactoriesMixinMethods(unittest.TestCase): def setUp(self): @@ -120,7 +136,7 @@ class TestDeprecatedFactoriesMixinMethods(unittest.TestCase): def tearDown(self): from zope.deprecation import __show__ __show__.on() - + def _makeOne(self, *arg, **kw): from pyramid.config import Configurator config = Configurator(*arg, **kw) diff --git a/pyramid/util.py b/pyramid/util.py index 6de53d559..e9f5760a6 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -111,7 +111,15 @@ class InstancePropertyMixin(object): def _set_extensions(self, extensions): for name, fn in iteritems_(extensions.methods): method = fn.__get__(self, self.__class__) - setattr(self, name, method) + try: + setattr(self, name, method) + except UnicodeEncodeError: + msg = ( + '`name="%s"` is invalid. `name` must be ascii because it is ' + 'used on __name__ of the method' + ) + raise ValueError(msg % name) + self._set_properties(extensions.descriptors) def set_property(self, callable, name=None, reify=False): -- cgit v1.2.3 From e094ab229eed6f8bf9e7a6a4d4406faefece41e4 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 1 Jan 2015 20:26:22 -0800 Subject: Added py3 support --- pyramid/tests/test_config/test_factories.py | 6 +++++- pyramid/util.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py index 5ae486c4b..e93ba6908 100644 --- a/pyramid/tests/test_config/test_factories.py +++ b/pyramid/tests/test_config/test_factories.py @@ -120,13 +120,17 @@ class TestFactoriesMixin(unittest.TestCase): def boomshaka(r): pass name = text_(b'La Pe\xc3\xb1a', 'utf-8') config.add_request_method(boomshaka, name=name) + + name2 = b'La Pe\xc3\xb1a' + config.add_request_method(boomshaka, name=name2) + exts = config.registry.getUtility(IRequestExtensions) inst = InstancePropertyMixin() def set_extensions(): inst._set_extensions(exts) + self.assertRaises(ValueError, set_extensions) - self.assertTrue(name in exts.methods) class TestDeprecatedFactoriesMixinMethods(unittest.TestCase): def setUp(self): diff --git a/pyramid/util.py b/pyramid/util.py index e9f5760a6..6ab621fd4 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -113,7 +113,7 @@ class InstancePropertyMixin(object): method = fn.__get__(self, self.__class__) try: setattr(self, name, method) - except UnicodeEncodeError: + except (UnicodeEncodeError, TypeError): msg = ( '`name="%s"` is invalid. `name` must be ascii because it is ' 'used on __name__ of the method' -- cgit v1.2.3 From db6280393d6f87f391b3c12a23c65fe803556286 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 02:09:20 -0600 Subject: moar docs --- pyramid/renderers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 3c35551ea..805118647 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -125,6 +125,11 @@ def render_to_response(renderer_name, value, request=None, package=None): not passed in, any changes to ``request.response`` attributes made before calling this function will be ignored. + .. versionchanged:: 1.6 + In previous versions, any changes made to ``request.response`` outside + of this function call would affect the returned response. This is no + longer the case. + """ try: registry = request.registry -- cgit v1.2.3 From 803ea0bf2d2c2d0354cc5d89fe627bc87c326081 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 02:09:37 -0600 Subject: Revert "moar docs" This reverts commit db6280393d6f87f391b3c12a23c65fe803556286. --- pyramid/renderers.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyramid/renderers.py b/pyramid/renderers.py index 805118647..3c35551ea 100644 --- a/pyramid/renderers.py +++ b/pyramid/renderers.py @@ -125,11 +125,6 @@ def render_to_response(renderer_name, value, request=None, package=None): not passed in, any changes to ``request.response`` attributes made before calling this function will be ignored. - .. versionchanged:: 1.6 - In previous versions, any changes made to ``request.response`` outside - of this function call would affect the returned response. This is no - longer the case. - """ try: registry = request.registry -- cgit v1.2.3 From e30c3b9138605a16386a3e67d233b72cbbcfc5e8 Mon Sep 17 00:00:00 2001 From: Jeff Dairiki Date: Thu, 22 Jan 2015 11:01:32 -0800 Subject: Prevent DeprecationWarning from setuptools>=11.3 --- CHANGES.txt | 3 +++ pyramid/path.py | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index b334f5258..a7138db1a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -125,6 +125,9 @@ Bug Fixes callback and thus behave just like the ``pyramid.renderers.JSON` renderer. See https://github.com/Pylons/pyramid/pull/1561 +- Prevent "parameters to load are deprecated" ``DeprecationWarning`` + from setuptools>=11.3. + Deprecations ------------ diff --git a/pyramid/path.py b/pyramid/path.py index 470e766f8..8eecc282b 100644 --- a/pyramid/path.py +++ b/pyramid/path.py @@ -337,8 +337,13 @@ class DottedNameResolver(Resolver): value = package.__name__ else: value = package.__name__ + value - return pkg_resources.EntryPoint.parse( - 'x=%s' % value).load(False) + # Calling EntryPoint.load with an argument is deprecated. + # See https://pythonhosted.org/setuptools/history.html#id8 + ep = pkg_resources.EntryPoint.parse('x=%s' % value) + if hasattr(ep, 'resolve'): + return ep.resolve() # setuptools>=10.2 + else: + return ep.load(False) def _zope_dottedname_style(self, value, package): """ package.module.attr style """ -- cgit v1.2.3 From c04115ab48c57d9a259e3c7f968cf71842449cdb Mon Sep 17 00:00:00 2001 From: Jeff Dairiki Date: Thu, 22 Jan 2015 14:30:25 -0800 Subject: Add NO COVER pragmas --- pyramid/path.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyramid/path.py b/pyramid/path.py index 8eecc282b..f2d8fff55 100644 --- a/pyramid/path.py +++ b/pyramid/path.py @@ -341,9 +341,10 @@ class DottedNameResolver(Resolver): # See https://pythonhosted.org/setuptools/history.html#id8 ep = pkg_resources.EntryPoint.parse('x=%s' % value) if hasattr(ep, 'resolve'): - return ep.resolve() # setuptools>=10.2 + # setuptools>=10.2 + return ep.resolve() # pragma: NO COVER else: - return ep.load(False) + return ep.load(False) # pragma: NO COVER def _zope_dottedname_style(self, value, package): """ package.module.attr style """ -- cgit v1.2.3 From b5c0ea42424abf400683baf5dbfc2c41cf049ad1 Mon Sep 17 00:00:00 2001 From: Jeff Dairiki Date: Fri, 6 Feb 2015 07:36:02 -0800 Subject: Sign CONTRIBUTORS.txt --- CONTRIBUTORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index adf2224a5..319d41434 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -240,3 +240,5 @@ Contributors - Adrian Teng, 2014/12/17 - Ilja Everila, 2015/02/05 + +- Geoffrey T. Dairiki, 2015/02/06 -- cgit v1.2.3 From 3279854b8ffc02593a604f84d0f72b8e7d4f24a8 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Fri, 6 Feb 2015 10:05:12 -0600 Subject: update changelog for #1541 --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index a7138db1a..2dee64a84 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -126,7 +126,7 @@ Bug Fixes See https://github.com/Pylons/pyramid/pull/1561 - Prevent "parameters to load are deprecated" ``DeprecationWarning`` - from setuptools>=11.3. + from setuptools>=11.3. See https://github.com/Pylons/pyramid/pull/1541 Deprecations ------------ -- cgit v1.2.3 From 20d708e89c321e5da937160beb7225c0b4fce46f Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 00:33:20 -0700 Subject: When running pcreate without scaffold, list scaffolds This fixes #1297 --- pyramid/scripts/pcreate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index edf2c39f7..a1479a2dd 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -64,7 +64,9 @@ class PCreateCommand(object): if self.options.list: return self.show_scaffolds() if not self.options.scaffold_name: - self.out('You must provide at least one scaffold name') + self.out('You must provide at least one scaffold name: -s ') + self.out('') + self.show_scaffolds() return 2 if not self.args: self.out('You must provide a project name') -- cgit v1.2.3 From c9cb19b9e14e2d1ec9ba17691212ea706f19f61c Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 00:35:36 -0700 Subject: Add changelog entry --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 832a2c216..09a4bbf88 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,11 @@ Next release Features -------- +- pcreate when run without a scaffold argument will now print information on + the missing flag, as well as a list of available scaffolds. + See https://github.com/Pylons/pyramid/pull/1566 and + https://github.com/Pylons/pyramid/issues/1297 + - Added support / testing for 'pypy3' under Tox and Travis. See https://github.com/Pylons/pyramid/pull/1469 -- cgit v1.2.3 From 665027ba49c9869abe8f0b8fe5d771c358a99e6d Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 00:41:44 -0700 Subject: Update usage line to show required -s --- pyramid/scripts/pcreate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index a1479a2dd..c634119bd 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -18,7 +18,7 @@ def main(argv=sys.argv, quiet=False): class PCreateCommand(object): verbosity = 1 # required description = "Render Pyramid scaffolding to an output directory" - usage = "usage: %prog [options] output_directory" + usage = "usage: %prog [options] -s output_directory" parser = optparse.OptionParser(usage, description=description) parser.add_option('-s', '--scaffold', dest='scaffold_name', -- cgit v1.2.3 From 0786c75a63b8d861183a08c1bf74d8afe8b929e7 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 00:42:07 -0700 Subject: Show help if missing arguments This will print the full help, followed by the available scaffolds if the user just calls pcreate without any arguments/flags at all. --- pyramid/scripts/pcreate.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index c634119bd..2d2189686 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -63,6 +63,12 @@ class PCreateCommand(object): def run(self): if self.options.list: return self.show_scaffolds() + if not self.options.scaffold_name and not self.args: + if not self.quiet: + self.parser.print_help() + self.out('') + self.show_scaffolds() + return 2 if not self.options.scaffold_name: self.out('You must provide at least one scaffold name: -s ') self.out('') -- cgit v1.2.3 From 5de795938f4ec23c53cd4678021e36a72d3188cb Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 01:06:29 -0700 Subject: Document the factory requires a positional argument --- docs/narr/hooks.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 17cae2c67..8e6cf8343 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -368,6 +368,9 @@ changed by passing a ``response_factory`` argument to the constructor of the :term:`configurator`. This argument can be either a callable or a :term:`dotted Python name` representing a callable. +The factory takes a single positional argument, which is a :term:`Request` +object. The argument may be the value ``None``. + .. code-block:: python :linenos: -- cgit v1.2.3 From 972dfae78a94ac19e97b96b36dfa91f9f7c3fed4 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 01:22:44 -0700 Subject: Fix failing test --- pyramid/tests/test_scripts/test_pcreate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/tests/test_scripts/test_pcreate.py b/pyramid/tests/test_scripts/test_pcreate.py index 020721ca7..89fdea6be 100644 --- a/pyramid/tests/test_scripts/test_pcreate.py +++ b/pyramid/tests/test_scripts/test_pcreate.py @@ -35,7 +35,7 @@ class TestPCreateCommand(unittest.TestCase): self.assertTrue(out.startswith('No scaffolds available')) def test_run_no_scaffold_name(self): - cmd = self._makeOne() + cmd = self._makeOne('dummy') result = cmd.run() self.assertEqual(result, 2) out = self.out_.getvalue() -- cgit v1.2.3 From da5f5f9ea02c2c9830c7ae016547d2bedd0e0171 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sat, 7 Feb 2015 02:38:54 -0600 Subject: move the IResponseFactory into the public api --- docs/api/interfaces.rst | 3 +++ docs/glossary.rst | 3 ++- docs/narr/hooks.rst | 8 ++++---- pyramid/config/factories.py | 5 ++--- pyramid/interfaces.py | 11 +++++------ pyramid/util.py | 5 ----- 6 files changed, 16 insertions(+), 19 deletions(-) diff --git a/docs/api/interfaces.rst b/docs/api/interfaces.rst index a62976d8a..de2a664a4 100644 --- a/docs/api/interfaces.rst +++ b/docs/api/interfaces.rst @@ -56,6 +56,9 @@ Other Interfaces .. autointerface:: IRenderer :members: + .. autointerface:: IResponseFactory + :members: + .. autointerface:: IViewMapperFactory :members: diff --git a/docs/glossary.rst b/docs/glossary.rst index 911c22075..9c0ea8598 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -18,7 +18,8 @@ Glossary response factory An object which, provided a :term:`request` as a single positional - argument, returns a Pyramid-compatible response. + argument, returns a Pyramid-compatible response. See + :class:`pyramid.interfaces.IResponseFactory`. response An object returned by a :term:`view callable` that represents response diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst index 8e6cf8343..4fd7670b9 100644 --- a/docs/narr/hooks.rst +++ b/docs/narr/hooks.rst @@ -364,12 +364,12 @@ Whenever :app:`Pyramid` returns a response from a view it creates a object. The factory that :app:`Pyramid` uses to create a response object instance can be -changed by passing a ``response_factory`` argument to the constructor of the -:term:`configurator`. This argument can be either a callable or a -:term:`dotted Python name` representing a callable. +changed by passing a :class:`pyramid.interfaces.IResponseFactory` argument to +the constructor of the :term:`configurator`. This argument can be either a +callable or a :term:`dotted Python name` representing a callable. The factory takes a single positional argument, which is a :term:`Request` -object. The argument may be the value ``None``. +object. The argument may be ``None``. .. code-block:: python :linenos: diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index d7a48ba93..15cfb796f 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -102,9 +102,8 @@ class FactoriesConfiguratorMixin(object): """ The object passed as ``factory`` should be an object (or a :term:`dotted Python name` which refers to an object) which will be used by the :app:`Pyramid` as the default response - objects. This factory object must have the same - methods and attributes as the - :class:`pyramid.request.Response` class. + objects. The factory should conform to the + :class:`pyramid.interfaces.IResponseFactory` interface. .. note:: diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index b21c6b9cc..0f1b4efc3 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -582,12 +582,11 @@ class IStaticURLInfo(Interface): """ Generate a URL for the given path """ class IResponseFactory(Interface): - """ A utility which generates a response factory """ - def __call__(): - """ Return a response factory (e.g. a callable that returns an object - implementing IResponse, e.g. :class:`pyramid.response.Response`). It - should accept all the arguments that the Pyramid Response class - accepts.""" + """ A utility which generates a response """ + def __call__(request): + """ Return a response object implementing IResponse, + e.g. :class:`pyramid.response.Response`). It should handle the + case when ``request`` is ``None``.""" class IRequestFactory(Interface): """ A utility which generates a request """ diff --git a/pyramid/util.py b/pyramid/util.py index 4ca2937a1..18cef4602 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -15,10 +15,6 @@ from pyramid.exceptions import ( CyclicDependencyError, ) -from pyramid.interfaces import ( - IResponseFactory, - ) - from pyramid.compat import ( iteritems_, is_nonstr_iter, @@ -29,7 +25,6 @@ from pyramid.compat import ( ) from pyramid.interfaces import IActionInfo -from pyramid.response import Response from pyramid.path import DottedNameResolver as _DottedNameResolver class DottedNameResolver(_DottedNameResolver): -- cgit v1.2.3 From 9449be0316aba5b465ffb2c02a1bb0daafccd8e6 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 01:50:48 -0700 Subject: Use hammer to fix hole --- pyramid/scripts/pcreate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/scripts/pcreate.py b/pyramid/scripts/pcreate.py index 2d2189686..d2c5f8c27 100644 --- a/pyramid/scripts/pcreate.py +++ b/pyramid/scripts/pcreate.py @@ -64,7 +64,7 @@ class PCreateCommand(object): if self.options.list: return self.show_scaffolds() if not self.options.scaffold_name and not self.args: - if not self.quiet: + if not self.quiet: # pragma: no cover self.parser.print_help() self.out('') self.show_scaffolds() -- cgit v1.2.3 From 58b8adf4135656efcc063eb822e4d29f6112d329 Mon Sep 17 00:00:00 2001 From: Bert JW Regeer Date: Sat, 7 Feb 2015 01:51:04 -0700 Subject: Add test for no scaffold no project name This test at least makes sure that if there is no scaffold and no project name that the command exists with error 2 --- pyramid/tests/test_scripts/test_pcreate.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_scripts/test_pcreate.py b/pyramid/tests/test_scripts/test_pcreate.py index 89fdea6be..63e5e6368 100644 --- a/pyramid/tests/test_scripts/test_pcreate.py +++ b/pyramid/tests/test_scripts/test_pcreate.py @@ -12,10 +12,10 @@ class TestPCreateCommand(unittest.TestCase): from pyramid.scripts.pcreate import PCreateCommand return PCreateCommand - def _makeOne(self, *args): + def _makeOne(self, *args, **kw): effargs = ['pcreate'] effargs.extend(args) - cmd = self._getTargetClass()(effargs) + cmd = self._getTargetClass()(effargs, **kw) cmd.out = self.out return cmd @@ -34,6 +34,11 @@ class TestPCreateCommand(unittest.TestCase): out = self.out_.getvalue() self.assertTrue(out.startswith('No scaffolds available')) + def test_run_no_scaffold_no_args(self): + cmd = self._makeOne(quiet=True) + result = cmd.run() + self.assertEqual(result, 2) + def test_run_no_scaffold_name(self): cmd = self._makeOne('dummy') result = cmd.run() -- cgit v1.2.3 From 1e0d648503fd992323737c7c702be204337e1e36 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sat, 7 Feb 2015 12:33:03 -0800 Subject: Raise error at configuration time --- pyramid/config/factories.py | 15 +++++++++--- pyramid/tests/test_config/test_factories.py | 21 +++++++--------- pyramid/tests/test_util.py | 38 +++++++++++++++++++++++++++++ pyramid/util.py | 27 +++++++++++++------- 4 files changed, 77 insertions(+), 24 deletions(-) diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index 15cfb796f..4b2517ff1 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -15,8 +15,12 @@ from pyramid.traversal import DefaultRootFactory from pyramid.util import ( action_method, InstancePropertyMixin, + get_callable_name, ) +from pyramid.compat import native_ + + class FactoriesConfiguratorMixin(object): @action_method def set_root_factory(self, factory): @@ -33,9 +37,10 @@ class FactoriesConfiguratorMixin(object): factory = self.maybe_dotted(factory) if factory is None: factory = DefaultRootFactory + def register(): self.registry.registerUtility(factory, IRootFactory) - self.registry.registerUtility(factory, IDefaultRootFactory) # b/c + self.registry.registerUtility(factory, IDefaultRootFactory) # b/c intr = self.introspectable('root factories', None, @@ -44,7 +49,7 @@ class FactoriesConfiguratorMixin(object): intr['factory'] = factory self.action(IRootFactory, register, introspectables=(intr,)) - _set_root_factory = set_root_factory # bw compat + _set_root_factory = set_root_factory # bw compat @action_method def set_session_factory(self, factory): @@ -60,6 +65,7 @@ class FactoriesConfiguratorMixin(object): achieve the same purpose. """ factory = self.maybe_dotted(factory) + def register(): self.registry.registerUtility(factory, ISessionFactory) intr = self.introspectable('session factory', None, @@ -89,6 +95,7 @@ class FactoriesConfiguratorMixin(object): can be used to achieve the same purpose. """ factory = self.maybe_dotted(factory) + def register(): self.registry.registerUtility(factory, IRequestFactory) intr = self.introspectable('request factory', None, @@ -173,6 +180,8 @@ class FactoriesConfiguratorMixin(object): callable, name=name, reify=reify) elif name is None: name = callable.__name__ + else: + name = get_callable_name(name) def register(): exts = self.registry.queryUtility(IRequestExtensions) @@ -224,9 +233,9 @@ class FactoriesConfiguratorMixin(object): 'set_request_propery() is deprecated as of Pyramid 1.5; use ' 'add_request_method() with the property=True argument instead') + @implementer(IRequestExtensions) class _RequestExtensions(object): def __init__(self): self.descriptors = {} self.methods = {} - diff --git a/pyramid/tests/test_config/test_factories.py b/pyramid/tests/test_config/test_factories.py index 35677a91b..42bb5accc 100644 --- a/pyramid/tests/test_config/test_factories.py +++ b/pyramid/tests/test_config/test_factories.py @@ -128,24 +128,21 @@ class TestFactoriesMixin(unittest.TestCase): def test_add_request_method_with_text_type_name(self): from pyramid.interfaces import IRequestExtensions - from pyramid.compat import text_ - from pyramid.util import InstancePropertyMixin + from pyramid.compat import text_, PY3 + from pyramid.exceptions import ConfigurationError config = self._makeOne(autocommit=True) def boomshaka(r): pass - name = text_(b'La Pe\xc3\xb1a', 'utf-8') - config.add_request_method(boomshaka, name=name) - name2 = b'La Pe\xc3\xb1a' - config.add_request_method(boomshaka, name=name2) + def get_bad_name(): + if PY3: # pragma: nocover + name = b'La Pe\xc3\xb1a' + else: # pragma: nocover + name = text_(b'La Pe\xc3\xb1a', 'utf-8') - exts = config.registry.getUtility(IRequestExtensions) - inst = InstancePropertyMixin() - - def set_extensions(): - inst._set_extensions(exts) + config.add_request_method(boomshaka, name=name) - self.assertRaises(ValueError, set_extensions) + self.assertRaises(ConfigurationError, get_bad_name) class TestDeprecatedFactoriesMixinMethods(unittest.TestCase): def setUp(self): diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index ac5ea0683..405fe927a 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -124,6 +124,21 @@ class Test_InstancePropertyMixin(unittest.TestCase): self.assertEqual(1, foo.x) self.assertEqual(2, foo.y) + def test__make_property_unicode(self): + from pyramid.compat import text_ + from pyramid.exceptions import ConfigurationError + + cls = self._getTargetClass() + if PY3: # pragma: nocover + name = b'La Pe\xc3\xb1a' + else: # pragma: nocover + name = text_(b'La Pe\xc3\xb1a', 'utf-8') + + def make_bad_name(): + cls._make_property(lambda x: 1, name=name, reify=True) + + self.assertRaises(ConfigurationError, make_bad_name) + def test__set_properties_with_dict(self): foo = self._makeOne() x_name, x_fn = foo._make_property(lambda _: 1, name='x', reify=True) @@ -619,7 +634,30 @@ class TestActionInfo(unittest.TestCase): "Line 0 of file filename:\n linerepr ") +class TestCallableName(unittest.TestCase): + def test_valid_ascii(self): + from pyramid.util import get_callable_name + name = u'hello world' + self.assertEquals(get_callable_name(name), name) + + def test_invalid_ascii(self): + from pyramid.util import get_callable_name + from pyramid.compat import text_, PY3 + from pyramid.exceptions import ConfigurationError + + def get_bad_name(): + if PY3: # pragma: nocover + name = b'La Pe\xc3\xb1a' + else: # pragma: nocover + name = text_(b'La Pe\xc3\xb1a', 'utf-8') + + get_callable_name(name) + + self.assertRaises(ConfigurationError, get_bad_name) + + def dummyfunc(): pass + class Dummy(object): pass diff --git a/pyramid/util.py b/pyramid/util.py index c036c1c2e..7e8535aaf 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -22,6 +22,7 @@ from pyramid.compat import ( string_types, text_, PY3, + native_ ) from pyramid.interfaces import IActionInfo @@ -55,7 +56,7 @@ class InstancePropertyMixin(object): raise ValueError('cannot reify a property') elif name is not None: fn = lambda this: callable(this) - fn.__name__ = name + fn.__name__ = get_callable_name(name) fn.__doc__ = callable.__doc__ else: name = callable.__name__ @@ -111,14 +112,7 @@ class InstancePropertyMixin(object): def _set_extensions(self, extensions): for name, fn in iteritems_(extensions.methods): method = fn.__get__(self, self.__class__) - try: - setattr(self, name, method) - except (UnicodeEncodeError, TypeError): - msg = ( - '`name="%s"` is invalid. `name` must be ascii because it is ' - 'used on __name__ of the method' - ) - raise ValueError(msg % name) + setattr(self, name, method) self._set_properties(extensions.descriptors) @@ -558,3 +552,18 @@ def action_method(wrapped): functools.update_wrapper(wrapper, wrapped) wrapper.__docobj__ = wrapped return wrapper + + +def get_callable_name(name): + """ + Verifies that the ``name`` is ascii and will raise a ``ConfigurationError`` + if it is not. + """ + try: + return native_(name, 'ascii') + except (UnicodeEncodeError, UnicodeDecodeError): + msg = ( + '`name="%s"` is invalid. `name` must be ascii because it is ' + 'used on __name__ of the method' + ) + raise ConfigurationError(msg % name) -- cgit v1.2.3 From 4a86b211fe7d294d2c598b42bc80e0c150a08443 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sat, 7 Feb 2015 12:34:31 -0800 Subject: Remove `native_` import, not used anymore --- pyramid/config/factories.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index 4b2517ff1..10678df55 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -18,8 +18,6 @@ from pyramid.util import ( get_callable_name, ) -from pyramid.compat import native_ - class FactoriesConfiguratorMixin(object): @action_method -- cgit v1.2.3 From fd840237d4eb374c0d3f4ac2bb394aefaa43d40c Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sat, 7 Feb 2015 17:33:39 -0800 Subject: Fix py32 support --- pyramid/tests/test_util.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 405fe927a..371cd8703 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -1,9 +1,11 @@ import unittest from pyramid.compat import PY3 + class Test_InstancePropertyMixin(unittest.TestCase): def _makeOne(self): cls = self._getTargetClass() + class Foo(cls): pass return Foo() @@ -637,8 +639,14 @@ class TestActionInfo(unittest.TestCase): class TestCallableName(unittest.TestCase): def test_valid_ascii(self): from pyramid.util import get_callable_name - name = u'hello world' - self.assertEquals(get_callable_name(name), name) + from pyramid.compat import text_, PY3 + + if PY3: # pragma: nocover + name = b'hello world' + else: # pragma: nocover + name = text_(b'hello world', 'utf-8') + + self.assertEquals(get_callable_name(name), 'hello world') def test_invalid_ascii(self): from pyramid.util import get_callable_name -- cgit v1.2.3 From b809c72a6fc6d286373dea1fcfe6f674efea24a5 Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Tue, 10 Feb 2015 13:36:25 -0500 Subject: Prevent timing attacks when checking CSRF token --- pyramid/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/session.py b/pyramid/session.py index a95c3f258..29ffcfc2a 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -126,7 +126,7 @@ def check_csrf_token(request, .. versionadded:: 1.4a2 """ supplied_token = request.params.get(token, request.headers.get(header)) - if supplied_token != request.session.get_csrf_token(): + if strings_differ(request.session.get_csrf_token(), supplied_token): if raises: raise BadCSRFToken('check_csrf_token(): Invalid token') return False -- cgit v1.2.3 From 9756f6111b06de79306d3769edd83f6735275701 Mon Sep 17 00:00:00 2001 From: Donald Stufft Date: Tue, 10 Feb 2015 13:46:33 -0500 Subject: Default to an empty string instead of None --- pyramid/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyramid/session.py b/pyramid/session.py index 29ffcfc2a..c4cfc1949 100644 --- a/pyramid/session.py +++ b/pyramid/session.py @@ -125,7 +125,7 @@ def check_csrf_token(request, .. versionadded:: 1.4a2 """ - supplied_token = request.params.get(token, request.headers.get(header)) + supplied_token = request.params.get(token, request.headers.get(header, "")) if strings_differ(request.session.get_csrf_token(), supplied_token): if raises: raise BadCSRFToken('check_csrf_token(): Invalid token') -- cgit v1.2.3 From b4e9902fe8cc28bac8e3e7dae0d8b2a270cf1640 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 10 Feb 2015 12:58:23 -0600 Subject: update changelog for #1574 --- CHANGES.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 27052cf0f..1e50a623f 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -133,6 +133,9 @@ Bug Fixes - Prevent "parameters to load are deprecated" ``DeprecationWarning`` from setuptools>=11.3. See https://github.com/Pylons/pyramid/pull/1541 +- Avoiding timing attacks against CSRF tokens. + See https://github.com/Pylons/pyramid/pull/1574 + Deprecations ------------ -- cgit v1.2.3 From c534e00ed06ef17506cf5f74553310ec15653834 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Tue, 10 Feb 2015 22:43:39 -0800 Subject: Don't create sdist with tox (latest setuptools doesn't like mixing) --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 29bd48639..202e29e30 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,5 @@ [tox] +skipsdist = True envlist = py26,py27,py32,py33,py34,pypy,pypy3,cover -- cgit v1.2.3 From c45d6aea833245fa4fd9bb81352feb37045dfb07 Mon Sep 17 00:00:00 2001 From: David Glick Date: Thu, 12 Feb 2015 21:10:30 -0800 Subject: Add workaround to make sure echo is enabled after reload (refs #689) Also add myself to CONTRIBUTORS.txt --- CHANGES.txt | 3 +++ CONTRIBUTORS.txt | 2 ++ pyramid/scripts/pserve.py | 14 ++++++++++++++ 3 files changed, 19 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 1e50a623f..6a174bb1c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -88,6 +88,9 @@ Features Bug Fixes --------- +- Work around an issue where ``pserve --reload`` would leave terminal echo + disabled if it reloaded during a pdb session. + - ``pyramid.wsgi.wsgiapp`` and ``pyramid.wsgi.wsgiapp2`` now raise ``ValueError`` when accidentally passed ``None``. See https://github.com/Pylons/pyramid/pull/1320 diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 319d41434..4f9bd6e41 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -242,3 +242,5 @@ Contributors - Ilja Everila, 2015/02/05 - Geoffrey T. Dairiki, 2015/02/06 + +- David Glick, 2015/02/12 diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 314efd839..d2ea1719b 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -36,6 +36,11 @@ from pyramid.scripts.common import parse_vars MAXFD = 1024 +try: + import termios +except ImportError: # pragma: no cover + termios = None + if WIN and not hasattr(os, 'kill'): # pragma: no cover # py 2.6 on windows def kill(pid, sig=None): @@ -709,6 +714,14 @@ def _turn_sigterm_into_systemexit(): # pragma: no cover raise SystemExit signal.signal(signal.SIGTERM, handle_term) +def ensure_echo_on(): # pragma: no cover + if termios: + fd = sys.stdin.fileno() + attr_list = termios.tcgetattr(fd) + if not attr_list[3] & termios.ECHO: + attr_list[3] |= termios.ECHO + termios.tcsetattr(fd, termios.TCSANOW, attr_list) + def install_reloader(poll_interval=1, extra_files=None): # pragma: no cover """ Install the reloading monitor. @@ -718,6 +731,7 @@ def install_reloader(poll_interval=1, extra_files=None): # pragma: no cover ``raise_keyboard_interrupt`` option creates a unignorable signal which causes the whole application to shut-down (rudely). """ + ensure_echo_on() mon = Monitor(poll_interval=poll_interval) if extra_files is None: extra_files = [] -- cgit v1.2.3 From 9343dbc71b268cf3c4ff4ac7e164af76ce39d5ec Mon Sep 17 00:00:00 2001 From: David Glick Date: Thu, 12 Feb 2015 21:16:12 -0800 Subject: remove obsolete note about raise_keyboard_interrupt that's left over from paste --- pyramid/scripts/pserve.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyramid/scripts/pserve.py b/pyramid/scripts/pserve.py index 314efd839..c5e54d670 100644 --- a/pyramid/scripts/pserve.py +++ b/pyramid/scripts/pserve.py @@ -714,9 +714,7 @@ def install_reloader(poll_interval=1, extra_files=None): # pragma: no cover Install the reloading monitor. On some platforms server threads may not terminate when the main - thread does, causing ports to remain open/locked. The - ``raise_keyboard_interrupt`` option creates a unignorable signal - which causes the whole application to shut-down (rudely). + thread does, causing ports to remain open/locked. """ mon = Monitor(poll_interval=poll_interval) if extra_files is None: -- cgit v1.2.3 From c94c39bf9cc6a5c0fd9207046e8feb8b9a917447 Mon Sep 17 00:00:00 2001 From: David Glick Date: Thu, 12 Feb 2015 21:16:43 -0800 Subject: fix instructions for running coverage via tox --- HACKING.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HACKING.txt b/HACKING.txt index 16c17699c..e104869ec 100644 --- a/HACKING.txt +++ b/HACKING.txt @@ -195,7 +195,7 @@ Test Coverage ------------- - The codebase *must* have 100% test statement coverage after each commit. - You can test coverage via ``tox -e coverage``, or alternately by installing + You can test coverage via ``tox -e cover``, or alternately by installing ``nose`` and ``coverage`` into your virtualenv (easiest via ``setup.py dev``) , and running ``setup.py nosetests --with-coverage``. -- cgit v1.2.3 From 03d964a924e0ef183c3cd78a61c043b1f74f5570 Mon Sep 17 00:00:00 2001 From: David Glick Date: Fri, 13 Feb 2015 09:20:33 -0800 Subject: add pull request reference --- CHANGES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.txt b/CHANGES.txt index 6a174bb1c..37803b3ed 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -90,6 +90,7 @@ Bug Fixes - Work around an issue where ``pserve --reload`` would leave terminal echo disabled if it reloaded during a pdb session. + See https://github.com/Pylons/pyramid/pull/1577 - ``pyramid.wsgi.wsgiapp`` and ``pyramid.wsgi.wsgiapp2`` now raise ``ValueError`` when accidentally passed ``None``. -- cgit v1.2.3 From 04cc91a7ac2d203e5acda41aa7c4975f78171274 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 16 Feb 2015 22:09:35 -0600 Subject: add InstancePropertyHelper and apply_request_extensions --- docs/api/request.rst | 1 + pyramid/config/factories.py | 4 +- pyramid/interfaces.py | 3 +- pyramid/request.py | 26 ++++- pyramid/router.py | 3 +- pyramid/scripting.py | 8 +- pyramid/tests/test_request.py | 45 +++++++- pyramid/tests/test_router.py | 8 +- pyramid/tests/test_scripting.py | 16 ++- pyramid/tests/test_util.py | 236 +++++++++++++++++++++++++++++++--------- pyramid/util.py | 75 +++++++------ 11 files changed, 321 insertions(+), 104 deletions(-) diff --git a/docs/api/request.rst b/docs/api/request.rst index dd68fa09c..b325ad076 100644 --- a/docs/api/request.rst +++ b/docs/api/request.rst @@ -369,3 +369,4 @@ that used as ``request.GET``, ``request.POST``, and ``request.params``), see :class:`pyramid.interfaces.IMultiDict`. +.. autofunction:: apply_request_extensions(request) diff --git a/pyramid/config/factories.py b/pyramid/config/factories.py index 10678df55..f0b6252ae 100644 --- a/pyramid/config/factories.py +++ b/pyramid/config/factories.py @@ -14,8 +14,8 @@ from pyramid.traversal import DefaultRootFactory from pyramid.util import ( action_method, - InstancePropertyMixin, get_callable_name, + InstancePropertyHelper, ) @@ -174,7 +174,7 @@ class FactoriesConfiguratorMixin(object): property = property or reify if property: - name, callable = InstancePropertyMixin._make_property( + name, callable = InstancePropertyHelper.make_property( callable, name=name, reify=reify) elif name is None: name = callable.__name__ diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 0f1b4efc3..d7422bdde 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -591,8 +591,7 @@ class IResponseFactory(Interface): class IRequestFactory(Interface): """ A utility which generates a request """ def __call__(environ): - """ Return an object implementing IRequest, e.g. an instance - of ``pyramid.request.Request``""" + """ Return an instance of ``pyramid.request.Request``""" def blank(path): """ Return an empty request object (see diff --git a/pyramid/request.py b/pyramid/request.py index b2e2efe05..3cbe5d9e3 100644 --- a/pyramid/request.py +++ b/pyramid/request.py @@ -8,6 +8,7 @@ from webob import BaseRequest from pyramid.interfaces import ( IRequest, + IRequestExtensions, IResponse, ISessionFactory, ) @@ -16,6 +17,7 @@ from pyramid.compat import ( text_, bytes_, native_, + iteritems_, ) from pyramid.decorator import reify @@ -26,7 +28,10 @@ from pyramid.security import ( AuthorizationAPIMixin, ) from pyramid.url import URLMethodsMixin -from pyramid.util import InstancePropertyMixin +from pyramid.util import ( + InstancePropertyHelper, + InstancePropertyMixin, +) class TemplateContext(object): pass @@ -307,3 +312,22 @@ def call_app_with_subpath_as_path_info(request, app): new_request.environ['PATH_INFO'] = new_path_info return new_request.get_response(app) + +def apply_request_extensions(request, extensions=None): + """Apply request extensions (methods and properties) to an instance of + :class:`pyramid.interfaces.IRequest`. This method is dependent on the + ``request`` containing a properly initialized registry. + + After invoking this method, the ``request`` should have the methods + and properties that were defined using + :meth:`pyramid.config.Configurator.add_request_method`. + """ + if extensions is None: + extensions = request.registry.queryUtility(IRequestExtensions) + if extensions is not None: + for name, fn in iteritems_(extensions.methods): + method = fn.__get__(request, request.__class__) + setattr(request, name, method) + + InstancePropertyHelper.apply_properties( + request, extensions.descriptors) diff --git a/pyramid/router.py b/pyramid/router.py index ba4f85b18..0b1ecade7 100644 --- a/pyramid/router.py +++ b/pyramid/router.py @@ -27,6 +27,7 @@ from pyramid.events import ( from pyramid.exceptions import PredicateMismatch from pyramid.httpexceptions import HTTPNotFound from pyramid.request import Request +from pyramid.request import apply_request_extensions from pyramid.threadlocal import manager from pyramid.traversal import ( @@ -213,7 +214,7 @@ class Router(object): try: extensions = self.request_extensions if extensions is not None: - request._set_extensions(extensions) + apply_request_extensions(request, extensions=extensions) response = handle_request(request) if request.response_callbacks: diff --git a/pyramid/scripting.py b/pyramid/scripting.py index fdb4aa430..d9587338f 100644 --- a/pyramid/scripting.py +++ b/pyramid/scripting.py @@ -1,12 +1,12 @@ from pyramid.config import global_registries from pyramid.exceptions import ConfigurationError -from pyramid.request import Request from pyramid.interfaces import ( - IRequestExtensions, IRequestFactory, IRootFactory, ) +from pyramid.request import Request +from pyramid.request import apply_request_extensions from pyramid.threadlocal import manager as threadlocal_manager from pyramid.traversal import DefaultRootFactory @@ -77,9 +77,7 @@ def prepare(request=None, registry=None): request.registry = registry threadlocals = {'registry':registry, 'request':request} threadlocal_manager.push(threadlocals) - extensions = registry.queryUtility(IRequestExtensions) - if extensions is not None: - request._set_extensions(extensions) + apply_request_extensions(request) def closer(): threadlocal_manager.pop() root_factory = registry.queryUtility(IRootFactory, diff --git a/pyramid/tests/test_request.py b/pyramid/tests/test_request.py index 48af98f59..f142e4536 100644 --- a/pyramid/tests/test_request.py +++ b/pyramid/tests/test_request.py @@ -435,7 +435,50 @@ class Test_call_app_with_subpath_as_path_info(unittest.TestCase): self.assertEqual(request.environ['SCRIPT_NAME'], '/' + encoded) self.assertEqual(request.environ['PATH_INFO'], '/' + encoded) -class DummyRequest: +class Test_apply_request_extensions(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, request, extensions=None): + from pyramid.request import apply_request_extensions + return apply_request_extensions(request, extensions=extensions) + + def test_it_with_registry(self): + from pyramid.interfaces import IRequestExtensions + extensions = Dummy() + extensions.methods = {'foo': lambda x, y: y} + extensions.descriptors = {'bar': property(lambda x: 'bar')} + self.config.registry.registerUtility(extensions, IRequestExtensions) + request = DummyRequest() + request.registry = self.config.registry + self._callFUT(request) + self.assertEqual(request.bar, 'bar') + self.assertEqual(request.foo('abc'), 'abc') + + def test_it_override_extensions(self): + from pyramid.interfaces import IRequestExtensions + ignore = Dummy() + ignore.methods = {'x': lambda x, y, z: 'asdf'} + ignore.descriptors = {'bar': property(lambda x: 'asdf')} + self.config.registry.registerUtility(ignore, IRequestExtensions) + request = DummyRequest() + request.registry = self.config.registry + + extensions = Dummy() + extensions.methods = {'foo': lambda x, y: y} + extensions.descriptors = {'bar': property(lambda x: 'bar')} + self._callFUT(request, extensions=extensions) + self.assertRaises(AttributeError, lambda: request.x) + self.assertEqual(request.bar, 'bar') + self.assertEqual(request.foo('abc'), 'abc') + +class Dummy(object): + pass + +class DummyRequest(object): def __init__(self, environ=None): if environ is None: environ = {} diff --git a/pyramid/tests/test_router.py b/pyramid/tests/test_router.py index 30ebd5918..b57c248d5 100644 --- a/pyramid/tests/test_router.py +++ b/pyramid/tests/test_router.py @@ -317,6 +317,7 @@ class TestRouter(unittest.TestCase): from pyramid.interfaces import IRequestExtensions from pyramid.interfaces import IRequest from pyramid.request import Request + from pyramid.util import InstancePropertyHelper context = DummyContext() self._registerTraverserFactory(context) class Extensions(object): @@ -324,11 +325,12 @@ class TestRouter(unittest.TestCase): self.methods = {} self.descriptors = {} extensions = Extensions() - L = [] + ext_method = lambda r: 'bar' + name, fn = InstancePropertyHelper.make_property(ext_method, name='foo') + extensions.descriptors[name] = fn request = Request.blank('/') request.request_iface = IRequest request.registry = self.registry - request._set_extensions = lambda *x: L.extend(x) def request_factory(environ): return request self.registry.registerUtility(extensions, IRequestExtensions) @@ -342,7 +344,7 @@ class TestRouter(unittest.TestCase): router.request_factory = request_factory start_response = DummyStartResponse() router(environ, start_response) - self.assertEqual(L, [extensions]) + self.assertEqual(view.request.foo, 'bar') def test_call_view_registered_nonspecific_default_path(self): from pyramid.interfaces import IViewClassifier diff --git a/pyramid/tests/test_scripting.py b/pyramid/tests/test_scripting.py index a36d1ed71..1e952062b 100644 --- a/pyramid/tests/test_scripting.py +++ b/pyramid/tests/test_scripting.py @@ -122,11 +122,15 @@ class Test_prepare(unittest.TestCase): self.assertEqual(request.context, context) def test_it_with_extensions(self): - exts = Dummy() + from pyramid.util import InstancePropertyHelper + exts = DummyExtensions() + ext_method = lambda r: 'bar' + name, fn = InstancePropertyHelper.make_property(ext_method, 'foo') + exts.descriptors[name] = fn request = DummyRequest({}) registry = request.registry = self._makeRegistry([exts, DummyFactory]) info = self._callFUT(request=request, registry=registry) - self.assertEqual(request.extensions, exts) + self.assertEqual(request.foo, 'bar') root, closer = info['root'], info['closer'] closer() @@ -199,11 +203,13 @@ class DummyThreadLocalManager: def pop(self): self.popped.append(True) -class DummyRequest: +class DummyRequest(object): matchdict = None matched_route = None def __init__(self, environ): self.environ = environ - def _set_extensions(self, exts): - self.extensions = exts +class DummyExtensions: + def __init__(self): + self.descriptors = {} + self.methods = {} diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 371cd8703..459c729a0 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -2,6 +2,188 @@ import unittest from pyramid.compat import PY3 +class Test_InstancePropertyHelper(unittest.TestCase): + def _makeOne(self): + cls = self._getTargetClass() + return cls() + + def _getTargetClass(self): + from pyramid.util import InstancePropertyHelper + return InstancePropertyHelper + + def test_callable(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker) + foo.bar = 1 + self.assertEqual(1, foo.worker) + foo.bar = 2 + self.assertEqual(2, foo.worker) + + def test_callable_with_name(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, name='x') + foo.bar = 1 + self.assertEqual(1, foo.x) + foo.bar = 2 + self.assertEqual(2, foo.x) + + def test_callable_with_reify(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, reify=True) + foo.bar = 1 + self.assertEqual(1, foo.worker) + foo.bar = 2 + self.assertEqual(1, foo.worker) + + def test_callable_with_name_reify(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, name='x') + helper.set_property(foo, worker, name='y', reify=True) + foo.bar = 1 + self.assertEqual(1, foo.y) + self.assertEqual(1, foo.x) + foo.bar = 2 + self.assertEqual(2, foo.x) + self.assertEqual(1, foo.y) + + def test_property_without_name(self): + def worker(obj): pass + foo = Dummy() + helper = self._getTargetClass() + self.assertRaises(ValueError, helper.set_property, foo, property(worker)) + + def test_property_with_name(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, property(worker), name='x') + foo.bar = 1 + self.assertEqual(1, foo.x) + foo.bar = 2 + self.assertEqual(2, foo.x) + + def test_property_with_reify(self): + def worker(obj): pass + foo = Dummy() + helper = self._getTargetClass() + self.assertRaises(ValueError, helper.set_property, + foo, property(worker), name='x', reify=True) + + def test_override_property(self): + def worker(obj): pass + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, name='x') + def doit(): + foo.x = 1 + self.assertRaises(AttributeError, doit) + + def test_override_reify(self): + def worker(obj): pass + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, name='x', reify=True) + foo.x = 1 + self.assertEqual(1, foo.x) + foo.x = 2 + self.assertEqual(2, foo.x) + + def test_reset_property(self): + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, lambda _: 1, name='x') + self.assertEqual(1, foo.x) + helper.set_property(foo, lambda _: 2, name='x') + self.assertEqual(2, foo.x) + + def test_reset_reify(self): + """ This is questionable behavior, but may as well get notified + if it changes.""" + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, lambda _: 1, name='x', reify=True) + self.assertEqual(1, foo.x) + helper.set_property(foo, lambda _: 2, name='x', reify=True) + self.assertEqual(1, foo.x) + + def test_make_property(self): + from pyramid.decorator import reify + helper = self._getTargetClass() + name, fn = helper.make_property(lambda x: 1, name='x', reify=True) + self.assertEqual(name, 'x') + self.assertTrue(isinstance(fn, reify)) + + def test_apply_properties_with_iterable(self): + foo = Dummy() + helper = self._getTargetClass() + x = helper.make_property(lambda _: 1, name='x', reify=True) + y = helper.make_property(lambda _: 2, name='y') + helper.apply_properties(foo, [x, y]) + self.assertEqual(1, foo.x) + self.assertEqual(2, foo.y) + + def test_apply_properties_with_dict(self): + foo = Dummy() + helper = self._getTargetClass() + x_name, x_fn = helper.make_property(lambda _: 1, name='x', reify=True) + y_name, y_fn = helper.make_property(lambda _: 2, name='y') + helper.apply_properties(foo, {x_name: x_fn, y_name: y_fn}) + self.assertEqual(1, foo.x) + self.assertEqual(2, foo.y) + + def test_make_property_unicode(self): + from pyramid.compat import text_ + from pyramid.exceptions import ConfigurationError + + cls = self._getTargetClass() + if PY3: # pragma: nocover + name = b'La Pe\xc3\xb1a' + else: # pragma: nocover + name = text_(b'La Pe\xc3\xb1a', 'utf-8') + + def make_bad_name(): + cls.make_property(lambda x: 1, name=name, reify=True) + + self.assertRaises(ConfigurationError, make_bad_name) + + def test_add_property(self): + helper = self._makeOne() + helper.add_property(lambda obj: obj.bar, name='x', reify=True) + helper.add_property(lambda obj: obj.bar, name='y') + self.assertEqual(len(helper.properties), 2) + foo = Dummy() + helper.apply(foo) + foo.bar = 1 + self.assertEqual(foo.x, 1) + self.assertEqual(foo.y, 1) + foo.bar = 2 + self.assertEqual(foo.x, 1) + self.assertEqual(foo.y, 2) + + def test_apply_multiple_times(self): + helper = self._makeOne() + helper.add_property(lambda obj: 1, name='x') + foo, bar = Dummy(), Dummy() + helper.apply(foo) + self.assertEqual(foo.x, 1) + helper.add_property(lambda obj: 2, name='x') + helper.apply(bar) + self.assertEqual(foo.x, 1) + self.assertEqual(bar.x, 2) + class Test_InstancePropertyMixin(unittest.TestCase): def _makeOne(self): cls = self._getTargetClass() @@ -111,58 +293,6 @@ class Test_InstancePropertyMixin(unittest.TestCase): foo.set_property(lambda _: 2, name='x', reify=True) self.assertEqual(1, foo.x) - def test__make_property(self): - from pyramid.decorator import reify - cls = self._getTargetClass() - name, fn = cls._make_property(lambda x: 1, name='x', reify=True) - self.assertEqual(name, 'x') - self.assertTrue(isinstance(fn, reify)) - - def test__set_properties_with_iterable(self): - foo = self._makeOne() - x = foo._make_property(lambda _: 1, name='x', reify=True) - y = foo._make_property(lambda _: 2, name='y') - foo._set_properties([x, y]) - self.assertEqual(1, foo.x) - self.assertEqual(2, foo.y) - - def test__make_property_unicode(self): - from pyramid.compat import text_ - from pyramid.exceptions import ConfigurationError - - cls = self._getTargetClass() - if PY3: # pragma: nocover - name = b'La Pe\xc3\xb1a' - else: # pragma: nocover - name = text_(b'La Pe\xc3\xb1a', 'utf-8') - - def make_bad_name(): - cls._make_property(lambda x: 1, name=name, reify=True) - - self.assertRaises(ConfigurationError, make_bad_name) - - def test__set_properties_with_dict(self): - foo = self._makeOne() - x_name, x_fn = foo._make_property(lambda _: 1, name='x', reify=True) - y_name, y_fn = foo._make_property(lambda _: 2, name='y') - foo._set_properties({x_name: x_fn, y_name: y_fn}) - self.assertEqual(1, foo.x) - self.assertEqual(2, foo.y) - - def test__set_extensions(self): - inst = self._makeOne() - def foo(self, result): - return result - n, bar = inst._make_property(lambda _: 'bar', name='bar') - class Extensions(object): - def __init__(self): - self.methods = {'foo':foo} - self.descriptors = {'bar':bar} - extensions = Extensions() - inst._set_extensions(extensions) - self.assertEqual(inst.bar, 'bar') - self.assertEqual(inst.foo('abc'), 'abc') - class Test_WeakOrderedSet(unittest.TestCase): def _makeOne(self): from pyramid.config import WeakOrderedSet @@ -646,7 +776,7 @@ class TestCallableName(unittest.TestCase): else: # pragma: nocover name = text_(b'hello world', 'utf-8') - self.assertEquals(get_callable_name(name), 'hello world') + self.assertEqual(get_callable_name(name), 'hello world') def test_invalid_ascii(self): from pyramid.util import get_callable_name diff --git a/pyramid/util.py b/pyramid/util.py index 7e8535aaf..63d113361 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -34,14 +34,21 @@ class DottedNameResolver(_DottedNameResolver): _marker = object() -class InstancePropertyMixin(object): - """ Mixin that will allow an instance to add properties at - run-time as if they had been defined via @property or @reify - on the class itself. +class InstancePropertyHelper(object): + """A helper object for assigning properties and descriptors to instances. + It is not normally possible to do this because descriptors must be + defined on the class itself. + + This class is optimized for adding multiple properties at once to an + instance. This is done by calling :meth:`.add_property` once + per-property and then invoking :meth:`.apply` on target objects. + """ + def __init__(self): + self.properties = {} @classmethod - def _make_property(cls, callable, name=None, reify=False): + def make_property(cls, callable, name=None, reify=False): """ Convert a callable into one suitable for adding to the instance. This will return a 2-tuple containing the computed (name, property) pair. @@ -69,25 +76,12 @@ class InstancePropertyMixin(object): return name, fn - def _set_properties(self, properties): - """ Create several properties on the instance at once. - - This is a more efficient version of - :meth:`pyramid.util.InstancePropertyMixin.set_property` which - can accept multiple ``(name, property)`` pairs generated via - :meth:`pyramid.util.InstancePropertyMixin._make_property`. - - ``properties`` is a sequence of two-tuples *or* a data structure - with an ``.items()`` method which returns a sequence of two-tuples - (presumably a dictionary). It will be used to add several - properties to the instance in a manner that is more efficient - than simply calling ``set_property`` repeatedly. - """ + @classmethod + def apply_properties(cls, target, properties): attrs = dict(properties) - if attrs: - parent = self.__class__ - cls = type(parent.__name__, (parent, object), attrs) + parent = target.__class__ + newcls = type(parent.__name__, (parent, object), attrs) # We assign __provides__, __implemented__ and __providedBy__ below # to prevent a memory leak that results from from the usage of this # instance's eventual use in an adapter lookup. Adapter lookup @@ -106,15 +100,34 @@ class InstancePropertyMixin(object): # attached to it val = getattr(parent, name, _marker) if val is not _marker: - setattr(cls, name, val) - self.__class__ = cls + setattr(newcls, name, val) + target.__class__ = newcls + + @classmethod + def set_property(cls, target, callable, name=None, reify=False): + """A helper method to apply a single property to an instance.""" + prop = cls.make_property(callable, name=name, reify=reify) + cls.apply_properties(target, [prop]) + + def add_property(self, callable, name=None, reify=False): + """Add a new property configuration. + + This should be used in combination with :meth:`.apply` as a + more efficient version of :meth:`.set_property`. + """ + name, fn = self.make_property(callable, name=name, reify=reify) + self.properties[name] = fn - def _set_extensions(self, extensions): - for name, fn in iteritems_(extensions.methods): - method = fn.__get__(self, self.__class__) - setattr(self, name, method) + def apply(self, target): + """ Apply all configured properties to the ``target`` instance.""" + if self.properties: + self.apply_properties(target, self.properties) - self._set_properties(extensions.descriptors) +class InstancePropertyMixin(object): + """ Mixin that will allow an instance to add properties at + run-time as if they had been defined via @property or @reify + on the class itself. + """ def set_property(self, callable, name=None, reify=False): """ Add a callable or a property descriptor to the instance. @@ -168,8 +181,8 @@ class InstancePropertyMixin(object): >>> foo.y # notice y keeps the original value 1 """ - prop = self._make_property(callable, name=name, reify=reify) - self._set_properties([prop]) + InstancePropertyHelper.set_property( + self, callable, name=name, reify=reify) class WeakOrderedSet(object): """ Maintain a set of items. -- cgit v1.2.3 From 46bc7fd9e221a084ca2f4d0cb8b158d2e239c373 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 16 Feb 2015 22:14:24 -0600 Subject: update changelog for #1581 --- CHANGES.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 37803b3ed..8cee9c09d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,12 @@ Next release Features -------- +- Add ``pyramid.request.apply_request_extensions`` function which can be + used in testing to apply any request extensions configured via + ``config.add_request_method``. Previously it was only possible to test + the extensions by going through Pyramid's router. + See https://github.com/Pylons/pyramid/pull/1581 + - pcreate when run without a scaffold argument will now print information on the missing flag, as well as a list of available scaffolds. See https://github.com/Pylons/pyramid/pull/1566 and -- cgit v1.2.3 From 2f0ba093f1bd50fd43e0a55f244b90d1fe50ff19 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Mon, 16 Feb 2015 23:02:43 -0600 Subject: docstring on apply_properties --- pyramid/util.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyramid/util.py b/pyramid/util.py index 63d113361..5721a93fc 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -78,6 +78,9 @@ class InstancePropertyHelper(object): @classmethod def apply_properties(cls, target, properties): + """Accept a list or dict of ``properties`` generated from + :meth:`.make_property` and apply them to a ``target`` object. + """ attrs = dict(properties) if attrs: parent = target.__class__ -- cgit v1.2.3 From 780889f18d17b86fc12625166a245c7f9947cbe6 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 01:05:04 -0600 Subject: remove the token from the ICacheBuster api This exposes the QueryStringCacheBuster and PathSegmentCacheBuster public APIs alongside the md5-variants. These should be more cleanly subclassed by people wishing to extend their implementations. --- docs/api/static.rst | 6 +++ docs/narr/assets.rst | 15 ++++---- pyramid/config/views.py | 4 +- pyramid/interfaces.py | 13 ++----- pyramid/static.py | 65 +++++++++++++++++++++++++-------- pyramid/tests/test_config/test_views.py | 12 +++--- pyramid/tests/test_static.py | 16 ++++---- 7 files changed, 82 insertions(+), 49 deletions(-) diff --git a/docs/api/static.rst b/docs/api/static.rst index 543e526ad..b6b279139 100644 --- a/docs/api/static.rst +++ b/docs/api/static.rst @@ -9,6 +9,12 @@ :members: :inherited-members: + .. autoclass:: PathSegmentCacheBuster + :members: + + .. autoclass:: QueryStringCacheBuster + :members: + .. autoclass:: PathSegmentMd5CacheBuster :members: diff --git a/docs/narr/assets.rst b/docs/narr/assets.rst index fc908c2b4..d6bc8cbb8 100644 --- a/docs/narr/assets.rst +++ b/docs/narr/assets.rst @@ -446,19 +446,20 @@ In order to implement your own cache buster, you can write your own class from scratch which implements the :class:`~pyramid.interfaces.ICacheBuster` interface. Alternatively you may choose to subclass one of the existing implementations. One of the most likely scenarios is you'd want to change the -way the asset token is generated. To do this just subclass an existing -implementation and replace the :meth:`~pyramid.interfaces.ICacheBuster.token` -method. Here is an example which just uses Git to get the hash of the -currently checked out code: +way the asset token is generated. To do this just subclass either +:class:`~pyramid.static.PathSegmentCacheBuster` or +:class:`~pyramid.static.QueryStringCacheBuster` and define a +``tokenize(pathspec)`` method. Here is an example which just uses Git to get +the hash of the currently checked out code: .. code-block:: python :linenos: import os import subprocess - from pyramid.static import PathSegmentMd5CacheBuster + from pyramid.static import PathSegmentCacheBuster - class GitCacheBuster(PathSegmentMd5CacheBuster): + class GitCacheBuster(PathSegmentCacheBuster): """ Assuming your code is installed as a Git checkout, as opposed to as an egg from an egg repository like PYPI, you can use this cachebuster to @@ -470,7 +471,7 @@ currently checked out code: ['git', 'rev-parse', 'HEAD'], cwd=here).strip() - def token(self, pathspec): + def tokenize(self, pathspec): return self.sha1 Choosing a Cache Buster diff --git a/pyramid/config/views.py b/pyramid/config/views.py index 85e252f2f..24c592f7a 100644 --- a/pyramid/config/views.py +++ b/pyramid/config/views.py @@ -1980,9 +1980,9 @@ class StaticURLInfo(object): cb = self._default_cachebust() if cb: def cachebust(subpath, kw): - token = cb.token(spec + subpath) subpath_tuple = tuple(subpath.split('/')) - subpath_tuple, kw = cb.pregenerate(token, subpath_tuple, kw) + subpath_tuple, kw = cb.pregenerate( + spec + subpath, subpath_tuple, kw) return '/'.join(subpath_tuple), kw else: cachebust = None diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index d7422bdde..1508f282e 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1192,18 +1192,11 @@ class ICacheBuster(Interface): .. versionadded:: 1.6 """ - def token(pathspec): - """ - Computes and returns a token string used for cache busting. - ``pathspec`` is the path specification for the resource to be cache - busted. """ - - def pregenerate(token, subpath, kw): + def pregenerate(pathspec, subpath, kw): """ Modifies a subpath and/or keyword arguments from which a static asset - URL will be computed during URL generation. The ``token`` argument is - a token string computed by - :meth:`~pyramid.interfaces.ICacheBuster.token` for a particular asset. + URL will be computed during URL generation. The ``pathspec`` argument + is the path specification for the resource to be cache busted. The ``subpath`` argument is a tuple of path elements that represent the portion of the asset URL which is used to find the asset. The ``kw`` argument is a dict of keywords that are to be passed eventually to diff --git a/pyramid/static.py b/pyramid/static.py index c4a9e3cc4..460639a89 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -174,7 +174,7 @@ class Md5AssetTokenGenerator(object): def __init__(self): self.token_cache = {} - def token(self, pathspec): + def tokenize(self, pathspec): # An astute observer will notice that this use of token_cache doesn't # look particularly thread safe. Basic read/write operations on Python # dicts, however, are atomic, so simply accessing and writing values @@ -192,38 +192,55 @@ class Md5AssetTokenGenerator(object): self.token_cache[pathspec] = token = _generate_md5(pathspec) return token -class PathSegmentMd5CacheBuster(Md5AssetTokenGenerator): +class PathSegmentCacheBuster(object): """ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which - inserts an md5 checksum token for cache busting in the path portion of an - asset URL. Generated md5 checksums are cached in order to speed up - subsequent calls. + inserts a token for cache busting in the path portion of an asset URL. + + To use this class, subclass it and provide a ``tokenize`` method which + accepts a ``pathspec`` and returns a token. .. versionadded:: 1.6 """ - def pregenerate(self, token, subpath, kw): + def pregenerate(self, pathspec, subpath, kw): + token = self.tokenize(pathspec) return (token,) + subpath, kw def match(self, subpath): return subpath[1:] -class QueryStringMd5CacheBuster(Md5AssetTokenGenerator): +class PathSegmentMd5CacheBuster(PathSegmentCacheBuster, + Md5AssetTokenGenerator): + """ + An implementation of :class:`~pyramid.interfaces.ICacheBuster` which + inserts an md5 checksum token for cache busting in the path portion of an + asset URL. Generated md5 checksums are cached in order to speed up + subsequent calls. + + .. versionadded:: 1.6 + """ + def __init__(self): + PathSegmentCacheBuster.__init__(self) + Md5AssetTokenGenerator.__init__(self) + +class QueryStringCacheBuster(object): """ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds - an md5 checksum token for cache busting in the query string of an asset - URL. Generated md5 checksums are cached in order to speed up subsequent - calls. + a token for cache busting in the query string of an asset URL. The optional ``param`` argument determines the name of the parameter added to the query string and defaults to ``'x'``. + To use this class, subclass it and provide a ``tokenize`` method which + accepts a ``pathspec`` and returns a token. + .. versionadded:: 1.6 """ def __init__(self, param='x'): - super(QueryStringMd5CacheBuster, self).__init__() self.param = param - def pregenerate(self, token, subpath, kw): + def pregenerate(self, pathspec, subpath, kw): + token = self.tokenize(pathspec) query = kw.setdefault('_query', {}) if isinstance(query, dict): query[self.param] = token @@ -231,7 +248,24 @@ class QueryStringMd5CacheBuster(Md5AssetTokenGenerator): kw['_query'] = tuple(query) + ((self.param, token),) return subpath, kw -class QueryStringConstantCacheBuster(QueryStringMd5CacheBuster): +class QueryStringMd5CacheBuster(QueryStringCacheBuster, + Md5AssetTokenGenerator): + """ + An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds + an md5 checksum token for cache busting in the query string of an asset + URL. Generated md5 checksums are cached in order to speed up subsequent + calls. + + The optional ``param`` argument determines the name of the parameter added + to the query string and defaults to ``'x'``. + + .. versionadded:: 1.6 + """ + def __init__(self, param='x'): + QueryStringCacheBuster.__init__(self, param=param) + Md5AssetTokenGenerator.__init__(self) + +class QueryStringConstantCacheBuster(QueryStringCacheBuster): """ An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds an arbitrary token for cache busting in the query string of an asset URL. @@ -245,9 +279,8 @@ class QueryStringConstantCacheBuster(QueryStringMd5CacheBuster): .. versionadded:: 1.6 """ def __init__(self, token, param='x'): + QueryStringCacheBuster.__init__(self, param=param) self._token = token - self.param = param - def token(self, pathspec): + def tokenize(self, pathspec): return self._token - diff --git a/pyramid/tests/test_config/test_views.py b/pyramid/tests/test_config/test_views.py index d1eb1ed3c..36c86f78c 100644 --- a/pyramid/tests/test_config/test_views.py +++ b/pyramid/tests/test_config/test_views.py @@ -3995,7 +3995,7 @@ class TestStaticURLInfo(unittest.TestCase): def test_add_cachebust_default(self): config = self._makeConfig() inst = self._makeOne() - inst._default_cachebust = DummyCacheBuster + inst._default_cachebust = lambda: DummyCacheBuster('foo') inst.add(config, 'view', 'mypackage:path', cachebust=True) cachebust = config.registry._static_url_registrations[0][3] subpath, kw = cachebust('some/path', {}) @@ -4014,7 +4014,7 @@ class TestStaticURLInfo(unittest.TestCase): config = self._makeConfig() inst = self._makeOne() inst.add(config, 'view', 'mypackage:path', - cachebust=DummyCacheBuster()) + cachebust=DummyCacheBuster('foo')) cachebust = config.registry._static_url_registrations[0][3] subpath, kw = cachebust('some/path', {}) self.assertEqual(subpath, 'some/path') @@ -4127,10 +4127,10 @@ class DummyMultiView: """ """ class DummyCacheBuster(object): - def token(self, pathspec): - return 'foo' - def pregenerate(self, token, subpath, kw): - kw['x'] = token + def __init__(self, token): + self.token = token + def pregenerate(self, pathspec, subpath, kw): + kw['x'] = self.token return subpath, kw def parse_httpdate(s): diff --git a/pyramid/tests/test_static.py b/pyramid/tests/test_static.py index 2f4de249e..a3df74b44 100644 --- a/pyramid/tests/test_static.py +++ b/pyramid/tests/test_static.py @@ -393,13 +393,13 @@ class TestMd5AssetTokenGenerator(unittest.TestCase): return cls() def test_package_resource(self): - fut = self._makeOne().token + fut = self._makeOne().tokenize expected = '76d653a3a044e2f4b38bb001d283e3d9' token = fut('pyramid.tests:fixtures/static/index.html') self.assertEqual(token, expected) def test_filesystem_resource(self): - fut = self._makeOne().token + fut = self._makeOne().tokenize expected = 'd5155f250bef0e9923e894dbc713c5dd' with open(self.fspath, 'w') as f: f.write("Are we rich yet?") @@ -407,7 +407,7 @@ class TestMd5AssetTokenGenerator(unittest.TestCase): self.assertEqual(token, expected) def test_cache(self): - fut = self._makeOne().token + fut = self._makeOne().tokenize expected = 'd5155f250bef0e9923e894dbc713c5dd' with open(self.fspath, 'w') as f: f.write("Are we rich yet?") @@ -425,11 +425,11 @@ class TestPathSegmentMd5CacheBuster(unittest.TestCase): def _makeOne(self): from pyramid.static import PathSegmentMd5CacheBuster as cls inst = cls() - inst.token = lambda pathspec: 'foo' + inst.tokenize = lambda pathspec: 'foo' return inst def test_token(self): - fut = self._makeOne().token + fut = self._makeOne().tokenize self.assertEqual(fut('whatever'), 'foo') def test_pregenerate(self): @@ -448,11 +448,11 @@ class TestQueryStringMd5CacheBuster(unittest.TestCase): inst = cls(param) else: inst = cls() - inst.token = lambda pathspec: 'foo' + inst.tokenize = lambda pathspec: 'foo' return inst def test_token(self): - fut = self._makeOne().token + fut = self._makeOne().tokenize self.assertEqual(fut('whatever'), 'foo') def test_pregenerate(self): @@ -490,7 +490,7 @@ class TestQueryStringConstantCacheBuster(TestQueryStringMd5CacheBuster): return inst def test_token(self): - fut = self._makeOne().token + fut = self._makeOne().tokenize self.assertEqual(fut('whatever'), 'foo') def test_pregenerate(self): -- cgit v1.2.3 From 4a9c13647b93c79ba3414c32c96906bc43e325d3 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 02:40:07 -0600 Subject: use super with mixins... for reasons --- pyramid/static.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pyramid/static.py b/pyramid/static.py index 460639a89..4ff02f798 100644 --- a/pyramid/static.py +++ b/pyramid/static.py @@ -220,8 +220,7 @@ class PathSegmentMd5CacheBuster(PathSegmentCacheBuster, .. versionadded:: 1.6 """ def __init__(self): - PathSegmentCacheBuster.__init__(self) - Md5AssetTokenGenerator.__init__(self) + super(PathSegmentMd5CacheBuster, self).__init__() class QueryStringCacheBuster(object): """ @@ -262,8 +261,7 @@ class QueryStringMd5CacheBuster(QueryStringCacheBuster, .. versionadded:: 1.6 """ def __init__(self, param='x'): - QueryStringCacheBuster.__init__(self, param=param) - Md5AssetTokenGenerator.__init__(self) + super(QueryStringMd5CacheBuster, self).__init__(param=param) class QueryStringConstantCacheBuster(QueryStringCacheBuster): """ @@ -279,7 +277,7 @@ class QueryStringConstantCacheBuster(QueryStringCacheBuster): .. versionadded:: 1.6 """ def __init__(self, token, param='x'): - QueryStringCacheBuster.__init__(self, param=param) + super(QueryStringConstantCacheBuster, self).__init__(param=param) self._token = token def tokenize(self, pathspec): -- cgit v1.2.3 From 5fdf9a5f63b7731963de7f49df6c29077155525f Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Tue, 17 Feb 2015 11:39:10 -0600 Subject: update changelog --- CHANGES.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 8cee9c09d..596e5f506 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -20,7 +20,10 @@ Features - Cache busting for static resources has been added and is available via a new argument to ``pyramid.config.Configurator.add_static_view``: ``cachebust``. - See https://github.com/Pylons/pyramid/pull/1380 + Core APIs are shipped for both cache busting via query strings and + path segments and may be extended to fit into custom asset pipelines. + See https://github.com/Pylons/pyramid/pull/1380 and + https://github.com/Pylons/pyramid/pull/1583 - Add ``pyramid.config.Configurator.root_package`` attribute and init parameter to assist with includeable packages that wish to resolve -- cgit v1.2.3