From 078859d058ac8c1617349a12ea29b9d1d0187485 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 8 Dec 2011 17:10:18 -0500 Subject: provide caller_path support for both asset resolver and dotted name resolver, make it the default --- docs/api/path.rst | 7 +++ pyramid/config/__init__.py | 11 ++-- pyramid/path.py | 128 ++++++++++++++++++++++++++++++++------------- pyramid/tests/test_path.py | 110 +++++++++++++++++++++++++------------- pyramid/util.py | 6 ++- 5 files changed, 184 insertions(+), 78 deletions(-) diff --git a/docs/api/path.rst b/docs/api/path.rst index 045d77da2..d46c35d8e 100644 --- a/docs/api/path.rst +++ b/docs/api/path.rst @@ -5,6 +5,13 @@ .. automodule:: pyramid.path + .. attribute:: CALLER_PACKAGE + + A constant used by the constructor of + :class:`pyramid.path.DottedNameResolver` and + :class:`pyramid.path.AssetResolver` (see their docstrings for more + info). + .. autoclass:: DottedNameResolver :members: diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 11f1758fc..04f9b6fb5 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -278,8 +278,8 @@ class Configurator( package = caller_package() name_resolver = DottedNameResolver(package) self.name_resolver = name_resolver - self.package_name = name_resolver.package_name - self.package = name_resolver.package + self.package_name = name_resolver.get_package_name() + self.package = name_resolver.get_package() self.registry = registry self.autocommit = autocommit self.route_prefix = route_prefix @@ -339,6 +339,7 @@ class Configurator( self._fix_registry() if introspector is not None: + # use nondefault introspector self.introspector = introspector self._set_settings(settings) @@ -484,9 +485,9 @@ class Configurator( def _del_introspector(self): del self.registry.introspector - introspector = property(_get_introspector, - _set_introspector, - _del_introspector) + introspector = property( + _get_introspector, _set_introspector, _del_introspector + ) @property def action_info(self): diff --git a/pyramid/path.py b/pyramid/path.py index 05a54fff7..540b1c5df 100644 --- a/pyramid/path.py +++ b/pyramid/path.py @@ -74,11 +74,16 @@ def package_path(package): pass return prefix +class _CALLER_PACKAGE(object): + def __repr__(self): # for docs + return 'pyramid.path.CALLER_PACKAGE' + +CALLER_PACKAGE = _CALLER_PACKAGE() + class Resolver(object): - def __init__(self, package=None): - if package is None: - self.package_name = None - self.package = None + def __init__(self, package=CALLER_PACKAGE): + if package in (None, CALLER_PACKAGE): + self.package = package else: if isinstance(package, string_types): try: @@ -89,7 +94,21 @@ class Resolver(object): ) package = sys.modules[package] self.package = package_of(package) - self.package_name = self.package.__name__ + + def get_package_name(self): + if self.package is CALLER_PACKAGE: + package_name = caller_package().__name__ + else: + package_name = self.package.__name__ + return package_name + + def get_package(self): + if self.package is CALLER_PACKAGE: + package = caller_package() + else: + package = self.package + return package + class AssetResolver(Resolver): """ A class used to resolve an :term:`asset specification` to an @@ -106,16 +125,25 @@ class AssetResolver(Resolver): - The value ``None`` + - The constant value :attr:`pyramid.path.CALLER_PACKAGE`. + + The default value is :attr:`pyramid.path.CALLER_PACKAGE`. + The ``package`` is used when a relative asset specification is supplied to the :meth:`~pyramid.path.AssetResolver.resolve` method. An asset specification without a colon in it is treated as relative. - If the value ``None`` is supplied as the package name, the resolver will + If the value ``None`` is supplied as the ``package``, the resolver will only be able to resolve fully qualified (not relative) asset specifications. Any attempt to resolve a relative asset specification when the ``package`` is ``None`` will result in an :exc:`ValueError` exception. + If the value :attr:`pyramid.path.CALLER_PACKAGE` is supplied as the + ``package``, the resolver will treat relative asset specifications as + relative to the caller of the :meth:`~pyramid.path.AssetResolver.resolve` + method. + If a *module* or *module name* (as opposed to a package or package name) is supplied as ``package``, its containing package is computed and this package used to derive the package name (all names are resolved relative @@ -157,22 +185,25 @@ class AssetResolver(Resolver): print resolver.abspath() # -> /path/to/myproject/templates/foo.pt - If the AssetResolver is constructed without a ``package`` argument, - and a relative asset specification is passed to ``resolve``, a - :exc:`ValueError` exception is raised. + If the AssetResolver is constructed without a ``package`` argument of + ``None``, and a relative asset specification is passed to + ``resolve``, an :exc:`ValueError` exception is raised. """ if os.path.isabs(spec): return FSAssetDescriptor(spec) path = spec if ':' in path: - pkg_name, path = spec.split(':', 1) + package_name, path = spec.split(':', 1) else: - pkg_name = self.package_name - if pkg_name is None: + if self.package is CALLER_PACKAGE: + package_name = caller_package().__name__ + else: + package_name = getattr(self.package, '__name__', None) + if package_name is None: raise ValueError( 'relative spec %r irresolveable without package' % (spec,) ) - return PkgResourcesAssetDescriptor(pkg_name, path) + return PkgResourcesAssetDescriptor(package_name, path) class DottedNameResolver(Resolver): """ A class used to resolve a :term:`dotted Python name` to a package or @@ -189,16 +220,25 @@ class DottedNameResolver(Resolver): - The value ``None`` + - The constant value :attr:`pyramid.path.CALLER_PACKAGE`. + + The default value is :attr:`pyramid.path.CALLER_PACKAGE`. + The ``package`` is used when a relative dotted name is supplied to the :meth:`~pyramid.path.DottedNameResolver.resolve` method. A dotted name which has a ``.`` (dot) or ``:`` (colon) as its first character is treated as relative. - If the value ``None`` is supplied as the package name, the resolver will + If the value ``None`` is supplied as the ``package``, the resolver will only be able to resolve fully qualified (not relative) names. Any attempt to resolve a relative name when the ``package`` is ``None`` will result in an :exc:`ValueError` exception. + If the value :attr:`pyramid.path.CALLER_PACKAGE` is supplied as the + ``package``, the resolver will treat relative dotted names as relative to + the caller of the :meth:`~pyramid.path.DottedNameResolver.resolve` + method. + If a *module* or *module name* (as opposed to a package or package name) is supplied as ``package``, its containing package is computed and this package used to derive the package name (all names are resolved relative @@ -215,11 +255,8 @@ class DottedNameResolver(Resolver): passed the string ``xml.dom``, and ``.minidom`` is supplied to the :meth:`~pyramid.path.DottedNameResolver.resolve` method, the resulting import would be for ``xml.minidom``. - - When a dotted name cannot be resolved, a :exc:`ValueError` error is - raised. """ - def resolve(self, name): + def resolve(self, dotted): """ This method resolves a dotted name reference to a global Python object (an object which can be imported) to the object itself. @@ -239,52 +276,71 @@ class DottedNameResolver(Resolver): mechanism will be chosen, otherwise the ``zope.dottedname`` resolution mechanism will be chosen. - If the ``name`` argument passed to this method is not a string, a + If the ``dotted`` argument passed to this method is not a string, a :exc:`ValueError` will be raised. + + When a dotted name cannot be resolved, a :exc:`ValueError` error is + raised. + + Example: + + .. code-block:: python + + r = DottedNameResolver() + v = r.resolve('xml') # v is the xml module + """ - if not isinstance(name, string_types): - raise ValueError('%r is not a string' % (name,)) - return self.maybe_resolve(name) + if not isinstance(dotted, string_types): + raise ValueError('%r is not a string' % (dotted,)) + package = self.package + if package is CALLER_PACKAGE: + package = caller_package() + return self._resolve(dotted, package) def maybe_resolve(self, dotted): """ This method behaves just like :meth:`~pyramid.path.DottedNameResolver.resolve`, except if the - ``name`` value passed is not a string, it is simply returned. For + ``dotted`` value passed is not a string, it is simply returned. For example: .. code-block:: python import xml r = DottedNameResolver() - v = r.resolve(xml) + v = r.maybe_resolve(xml) # v is the xml module; no exception raised """ if isinstance(dotted, string_types): - if ':' in dotted: - return self._pkg_resources_style(dotted) - else: - return self._zope_dottedname_style(dotted) + package = self.package + if package is CALLER_PACKAGE: + package = caller_package() + return self._resolve(dotted, package) return dotted + def _resolve(self, dotted, package): + if ':' in dotted: + return self._pkg_resources_style(dotted, package) + else: + return self._zope_dottedname_style(dotted, package) - def _pkg_resources_style(self, value): + def _pkg_resources_style(self, value, package): """ package.module:attr style """ if value.startswith('.') or value.startswith(':'): - if not self.package_name: + if not package: raise ValueError( - 'relative name %r irresolveable without ' - 'package_name' % (value,)) + 'relative name %r irresolveable without package' % (value,) + ) if value in ['.', ':']: - value = self.package_name + value = package.__name__ else: - value = self.package_name + value + value = package.__name__ + value return pkg_resources.EntryPoint.parse( 'x=%s' % value).load(False) - def _zope_dottedname_style(self, value): + def _zope_dottedname_style(self, value, package): """ package.module.attr style """ - module = self.package_name + module = getattr(package, '__name__', None) # package may be None if not module: module = None if value == '.': diff --git a/pyramid/tests/test_path.py b/pyramid/tests/test_path.py index f99436eb8..ac51c92d7 100644 --- a/pyramid/tests/test_path.py +++ b/pyramid/tests/test_path.py @@ -170,6 +170,34 @@ class TestPackageName(unittest.TestCase): result = self._callFUT(__main__) self.assertEqual(result, '__main__') +class TestResolver(unittest.TestCase): + def _getTargetClass(self): + from pyramid.path import Resolver + return Resolver + + def _makeOne(self, package): + return self._getTargetClass()(package) + + def test_get_package_caller_package(self): + import pyramid.tests + from pyramid.path import CALLER_PACKAGE + self.assertEqual(self._makeOne(CALLER_PACKAGE).get_package(), + pyramid.tests) + + def test_get_package_name_caller_package(self): + from pyramid.path import CALLER_PACKAGE + self.assertEqual(self._makeOne(CALLER_PACKAGE).get_package_name(), + 'pyramid.tests') + + def test_get_package_string(self): + import pyramid.tests + self.assertEqual(self._makeOne('pyramid.tests').get_package(), + pyramid.tests) + + def test_get_package_name_string(self): + self.assertEqual(self._makeOne('pyramid.tests').get_package_name(), + 'pyramid.tests') + class TestAssetResolver(unittest.TestCase): def _getTargetClass(self): from pyramid.path import AssetResolver @@ -182,14 +210,12 @@ class TestAssetResolver(unittest.TestCase): import sys tests = sys.modules['pyramid.tests'] inst = self._makeOne(tests) - self.assertEqual(inst.package_name, 'pyramid.tests') self.assertEqual(inst.package, tests) def test_ctor_as_str(self): import sys tests = sys.modules['pyramid.tests'] inst = self._makeOne('pyramid.tests') - self.assertEqual(inst.package_name, 'pyramid.tests') self.assertEqual(inst.package, tests) def test_resolve_abspath(self): @@ -216,6 +242,14 @@ class TestAssetResolver(unittest.TestCase): def test_resolve_relspec_no_package(self): inst = self._makeOne(None) self.assertRaises(ValueError, inst.resolve, 'test_asset.py') + + def test_resolve_relspec_caller_package(self): + from pyramid.path import PkgResourcesAssetDescriptor + from pyramid.path import CALLER_PACKAGE + inst = self._makeOne(CALLER_PACKAGE) + r = inst.resolve('test_asset.py') + self.assertEqual(r.__class__, PkgResourcesAssetDescriptor) + self.failUnless(r.exists()) class TestPkgResourcesAssetDescriptor(unittest.TestCase): def _getTargetClass(self): @@ -338,66 +372,66 @@ class TestDottedNameResolver(unittest.TestCase): def test_zope_dottedname_style_resolve_builtin(self): typ = self._makeOne() if PY3: # pragma: no cover - result = typ._zope_dottedname_style('builtins.str') + result = typ._zope_dottedname_style('builtins.str', None) else: - result = typ._zope_dottedname_style('__builtin__.str') + result = typ._zope_dottedname_style('__builtin__.str', None) self.assertEqual(result, str) def test_zope_dottedname_style_resolve_absolute(self): typ = self._makeOne() result = typ._zope_dottedname_style( - 'pyramid.tests.test_path.TestDottedNameResolver') + 'pyramid.tests.test_path.TestDottedNameResolver', None) self.assertEqual(result, self.__class__) def test_zope_dottedname_style_irrresolveable_absolute(self): typ = self._makeOne() self.assertRaises(ImportError, typ._zope_dottedname_style, - 'pyramid.test_path.nonexisting_name') + 'pyramid.test_path.nonexisting_name', None) def test__zope_dottedname_style_resolve_relative(self): import pyramid.tests - typ = self._makeOne(package=pyramid.tests) + typ = self._makeOne() result = typ._zope_dottedname_style( - '.test_path.TestDottedNameResolver') + '.test_path.TestDottedNameResolver', pyramid.tests) self.assertEqual(result, self.__class__) def test__zope_dottedname_style_resolve_relative_leading_dots(self): import pyramid.tests.test_configuration - typ = self._makeOne(package=pyramid.tests) + typ = self._makeOne() result = typ._zope_dottedname_style( - '..tests.test_path.TestDottedNameResolver') + '..tests.test_path.TestDottedNameResolver', pyramid.tests) self.assertEqual(result, self.__class__) def test__zope_dottedname_style_resolve_relative_is_dot(self): import pyramid.tests - typ = self._makeOne(package=pyramid.tests) - result = typ._zope_dottedname_style('.') + typ = self._makeOne() + result = typ._zope_dottedname_style('.', pyramid.tests) self.assertEqual(result, pyramid.tests) def test__zope_dottedname_style_irresolveable_relative_is_dot(self): typ = self._makeOne() - e = self.config_exc(typ._zope_dottedname_style, '.') + e = self.config_exc(typ._zope_dottedname_style, '.', None) self.assertEqual( e.args[0], "relative name '.' irresolveable without package") def test_zope_dottedname_style_resolve_relative_nocurrentpackage(self): typ = self._makeOne() - e = self.config_exc(typ._zope_dottedname_style, '.whatever') + e = self.config_exc(typ._zope_dottedname_style, '.whatever', None) self.assertEqual( e.args[0], "relative name '.whatever' irresolveable without package") def test_zope_dottedname_style_irrresolveable_relative(self): import pyramid.tests - typ = self._makeOne(package=pyramid.tests) + typ = self._makeOne() self.assertRaises(ImportError, typ._zope_dottedname_style, - '.notexisting') + '.notexisting', pyramid.tests) def test__zope_dottedname_style_resolveable_relative(self): import pyramid - typ = self._makeOne(package=pyramid) - result = typ._zope_dottedname_style('.tests') + typ = self._makeOne() + result = typ._zope_dottedname_style('.tests', pyramid) from pyramid import tests self.assertEqual(result, tests) @@ -405,48 +439,48 @@ class TestDottedNameResolver(unittest.TestCase): typ = self._makeOne() self.assertRaises( ImportError, - typ._zope_dottedname_style, 'pyramid.fudge.bar') + typ._zope_dottedname_style, 'pyramid.fudge.bar', None) def test__zope_dottedname_style_resolveable_absolute(self): typ = self._makeOne() result = typ._zope_dottedname_style( - 'pyramid.tests.test_path.TestDottedNameResolver') + 'pyramid.tests.test_path.TestDottedNameResolver', None) self.assertEqual(result, self.__class__) def test__pkg_resources_style_resolve_absolute(self): typ = self._makeOne() result = typ._pkg_resources_style( - 'pyramid.tests.test_path:TestDottedNameResolver') + 'pyramid.tests.test_path:TestDottedNameResolver', None) self.assertEqual(result, self.__class__) def test__pkg_resources_style_irrresolveable_absolute(self): typ = self._makeOne() self.assertRaises(ImportError, typ._pkg_resources_style, - 'pyramid.tests:nonexisting') + 'pyramid.tests:nonexisting', None) def test__pkg_resources_style_resolve_relative(self): import pyramid.tests - typ = self._makeOne(package=pyramid.tests) + typ = self._makeOne() result = typ._pkg_resources_style( - '.test_path:TestDottedNameResolver') + '.test_path:TestDottedNameResolver', pyramid.tests) self.assertEqual(result, self.__class__) def test__pkg_resources_style_resolve_relative_is_dot(self): import pyramid.tests - typ = self._makeOne(package=pyramid.tests) - result = typ._pkg_resources_style('.') + typ = self._makeOne() + result = typ._pkg_resources_style('.', pyramid.tests) self.assertEqual(result, pyramid.tests) def test__pkg_resources_style_resolve_relative_nocurrentpackage(self): typ = self._makeOne() self.assertRaises(ValueError, typ._pkg_resources_style, - '.whatever') + '.whatever', None) def test__pkg_resources_style_irrresolveable_relative(self): import pyramid - typ = self._makeOne(package=pyramid) + typ = self._makeOne() self.assertRaises(ImportError, typ._pkg_resources_style, - ':notexisting') + ':notexisting', pyramid) def test_resolve_not_a_string(self): typ = self._makeOne() @@ -469,17 +503,27 @@ class TestDottedNameResolver(unittest.TestCase): typ = self._makeOne() self.assertRaises(ImportError, typ.resolve, 'cant.be.found') + def test_resolve_caller_package(self): + from pyramid.path import CALLER_PACKAGE + typ = self._makeOne(CALLER_PACKAGE) + self.assertEqual(typ.resolve('.test_path.TestDottedNameResolver'), + self.__class__) + + def test_maybe_resolve_caller_package(self): + from pyramid.path import CALLER_PACKAGE + typ = self._makeOne(CALLER_PACKAGE) + self.assertEqual(typ.maybe_resolve('.test_path.TestDottedNameResolver'), + self.__class__) + def test_ctor_string_module_resolveable(self): import pyramid.tests typ = self._makeOne('pyramid.tests.test_path') self.assertEqual(typ.package, pyramid.tests) - self.assertEqual(typ.package_name, 'pyramid.tests') def test_ctor_string_package_resolveable(self): import pyramid.tests typ = self._makeOne('pyramid.tests') self.assertEqual(typ.package, pyramid.tests) - self.assertEqual(typ.package_name, 'pyramid.tests') def test_ctor_string_irresolveable(self): self.assertRaises(ValueError, self._makeOne, 'cant.be.found') @@ -489,19 +533,15 @@ class TestDottedNameResolver(unittest.TestCase): import pyramid.tests.test_path typ = self._makeOne(pyramid.tests.test_path) self.assertEqual(typ.package, pyramid.tests) - self.assertEqual(typ.package_name, 'pyramid.tests') def test_ctor_package(self): import pyramid.tests typ = self._makeOne(pyramid.tests) self.assertEqual(typ.package, pyramid.tests) - self.assertEqual(typ.package_name, 'pyramid.tests') def test_ctor_None(self): typ = self._makeOne(None) self.assertEqual(typ.package, None) - self.assertEqual(typ.package_name, None) - class DummyPkgResource(object): pass diff --git a/pyramid/util.py b/pyramid/util.py index 2018fb73e..76968bbbd 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -8,9 +8,11 @@ from pyramid.compat import ( PY3, ) -from pyramid.path import DottedNameResolver # bw compat +from pyramid.path import DottedNameResolver as _DottedNameResolver -DottedNameResolver = DottedNameResolver # for pyflakes +class DottedNameResolver(_DottedNameResolver): + def __init__(self, package=None): # default to package = None for bw compat + return _DottedNameResolver.__init__(self, package) class WeakOrderedSet(object): """ Maintain a set of items. -- cgit v1.2.3