diff options
| -rw-r--r-- | CHANGES.txt | 5 | ||||
| -rw-r--r-- | TODO.txt | 2 | ||||
| -rw-r--r-- | docs/api.rst | 1 | ||||
| -rw-r--r-- | docs/api/path.rst | 13 | ||||
| -rw-r--r-- | docs/glossary.rst | 6 | ||||
| -rw-r--r-- | docs/whatsnew-1.3.rst | 6 | ||||
| -rw-r--r-- | pyramid/asset.py | 1 | ||||
| -rw-r--r-- | pyramid/config/__init__.py | 3 | ||||
| -rw-r--r-- | pyramid/interfaces.py | 38 | ||||
| -rw-r--r-- | pyramid/path.py | 301 | ||||
| -rw-r--r-- | pyramid/tests/test_asset.py | 25 | ||||
| -rw-r--r-- | pyramid/tests/test_path.py | 350 | ||||
| -rw-r--r-- | pyramid/tests/test_util.py | 183 | ||||
| -rw-r--r-- | pyramid/util.py | 144 |
14 files changed, 727 insertions, 351 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index 67cac4b2b..4be846ece 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -38,6 +38,11 @@ Features - Allow extra keyword arguments to be passed to the ``pyramid.config.Configurator.action`` method. +- New APIs: ``pyramid.path.AssetResolver`` and + ``pyramid.path.DottedNameResolver``. The former can be used to resolve + asset specifications, the latter can be used to resolve dotted names to + modules or packages. + Bug Fixes --------- @@ -34,6 +34,8 @@ Must-Have - Implement analogue of "paster request"? +- AssetResolver method to guess relative based on caller? + Nice-to-Have ------------ diff --git a/docs/api.rst b/docs/api.rst index dc75e45e0..979e8f490 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -21,6 +21,7 @@ documentation is organized alphabetically by module name. api/interfaces api/location api/paster + api/path api/registry api/renderers api/request diff --git a/docs/api/path.rst b/docs/api/path.rst new file mode 100644 index 000000000..045d77da2 --- /dev/null +++ b/docs/api/path.rst @@ -0,0 +1,13 @@ +.. _path_module: + +:mod:`pyramid.path` +--------------------------- + +.. automodule:: pyramid.path + + .. autoclass:: DottedNameResolver + :members: + + .. autoclass:: AssetResolver + :members: + diff --git a/docs/glossary.rst b/docs/glossary.rst index 399b78cdf..e4de15bd6 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -977,3 +977,9 @@ Glossary a running Pyramid application. An introspectable is associated with a :term:`action` by virtue of the :meth:`pyramid.config.Configurator.action` method. + + asset descriptor + An instance representing an :term:`asset specification` provided by the + :meth:`pyramid.path.AssetResolver.resolve` method. It supports the + methods and attributes documented in + :class:`pyramid.interfaces.IAssetDescriptor`. diff --git a/docs/whatsnew-1.3.rst b/docs/whatsnew-1.3.rst index 44c55e208..9bb10d1d0 100644 --- a/docs/whatsnew-1.3.rst +++ b/docs/whatsnew-1.3.rst @@ -43,6 +43,12 @@ New APIs were added to support introspection Minor Feature Additions ----------------------- +- New APIs: :class:`pyramid.path.AssetResolver` and + :class:`pyramid.path.DottedNameResolver`. The former can be used to + resolve an :term:`asset specification` to an API that can be used to read + the asset's data, the latter can be used to resolve a :term:`dotted Python + name` to a module or a package. + - A ``mako.directories`` setting is no longer required to use Mako templates Rationale: Mako template renderers can be specified using an absolute asset spec. An entire application can be written with such asset specs, diff --git a/pyramid/asset.py b/pyramid/asset.py index 63be78c72..1cb65fbb1 100644 --- a/pyramid/asset.py +++ b/pyramid/asset.py @@ -40,3 +40,4 @@ def abspath_from_asset_spec(spec, pname='__main__'): if pname is None: return filename return pkg_resources.resource_filename(pname, filename) + diff --git a/pyramid/config/__init__.py b/pyramid/config/__init__.py index 315cdef07..11f1758fc 100644 --- a/pyramid/config/__init__.py +++ b/pyramid/config/__init__.py @@ -53,7 +53,6 @@ from pyramid.settings import aslist from pyramid.threadlocal import manager from pyramid.util import ( - DottedNameResolver, WeakOrderedSet, object_description, ) @@ -76,6 +75,8 @@ from pyramid.config.util import ( from pyramid.config.views import ViewsConfiguratorMixin from pyramid.config.zca import ZCAConfiguratorMixin +from pyramid.path import DottedNameResolver + empty = text_('') _marker = object() diff --git a/pyramid/interfaces.py b/pyramid/interfaces.py index 0261ae3db..6762d788d 100644 --- a/pyramid/interfaces.py +++ b/pyramid/interfaces.py @@ -1013,6 +1013,44 @@ class IActionInfo(Interface): """ Return a representation of the action information (including source code from file, if possible) """ +class IAssetDescriptor(Interface): + """ + Describes an :term:`asset`. + """ + + def absspec(): + """ + Returns the absolute asset specification for this asset + (e.g. ``mypackage:templates/foo.pt``). + """ + + def abspath(): + """ + Returns an absolute path in the filesystem to the asset. + """ + + def stream(): + """ + Returns an input stream for reading asset contents. Raises an + exception if the asset is a directory or does not exist. + """ + + def isdir(): + """ + Returns True if the asset is a directory, otherwise returns False. + """ + + def listdir(): + """ + Returns iterable of filenames of directory contents. Raises an + exception if asset is not a directory. + """ + + def exists(): + """ + Returns True if asset exists, otherwise returns False. + """ + # configuration phases: a lower phase number means the actions associated # with this phase will be executed earlier than those with later phase # numbers. The default phase number is 0, FTR. diff --git a/pyramid/path.py b/pyramid/path.py index 86c1c5857..05a54fff7 100644 --- a/pyramid/path.py +++ b/pyramid/path.py @@ -3,6 +3,12 @@ import pkg_resources import sys import imp +from zope.interface import implementer + +from pyramid.interfaces import IAssetDescriptor + +from pyramid.compat import string_types + ignore_types = [ imp.C_EXTENSION, imp.C_BUILTIN ] init_names = [ '__init__%s' % x[0] for x in imp.get_suffixes() if x[0] and x[2] not in ignore_types ] @@ -68,3 +74,298 @@ def package_path(package): pass return prefix +class Resolver(object): + def __init__(self, package=None): + if package is None: + self.package_name = None + self.package = None + else: + if isinstance(package, string_types): + try: + __import__(package) + except ImportError: + raise ValueError( + 'The dotted name %r cannot be imported' % (package,) + ) + package = sys.modules[package] + self.package = package_of(package) + self.package_name = self.package.__name__ + +class AssetResolver(Resolver): + """ A class used to resolve an :term:`asset specification` to an + :term:`asset descriptor`. + + .. warning:: This API is new as of Pyramid 1.3. + + The constructor accepts a single argument named ``package`` which may be + any of: + + - A fully qualified (not relative) dotted name to a module or package + + - a Python module or package object + + - The value ``None`` + + 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 + 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 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 + to packages, never to modules). For example, if the ``package`` argument + to this type was passed the string ``xml.dom.expatbuilder``, and + ``template.pt`` is supplied to the + :meth:`~pyramid.path.AssetResolver.resolve` method, the resulting absolute + asset spec would be ``xml.minidom:template.pt``, because + ``xml.dom.expatbuilder`` is a module object, not a package object. + + If a *package* or *package name* (as opposed to a module or module name) + is supplied as ``package``, this package will be used to compute relative + asset specifications. For example, if the ``package`` argument to this + type was passed the string ``xml.dom``, and ``template.pt`` is supplied + to the :meth:`~pyramid.path.AssetResolver.resolve` method, the resulting + absolute asset spec would be ``xml.minidom:template.pt``. + """ + def resolve(self, spec): + """ + Resolve the asset spec named as ``spec`` to an object that has the + attributes and methods described in + `pyramid.interfaces.IAssetDescriptor`. + + If ``spec`` is an absolute filename + (e.g. ``/path/to/myproject/templates/foo.pt``) or an absolute asset + spec (e.g. ``myproject:templates.foo.pt``), an asset descriptor is + returned without taking into account the ``package`` passed to this + class' constructor. + + If ``spec`` is a *relative* asset specification (an asset + specification without a ``:`` in it, e.g. ``templates/foo.pt``), the + ``package`` argument of the constructor is used as the the package + portion of the asset spec. For example: + + .. code-block:: python + + a = AssetResolver('myproject') + resolver = a.resolve('templates/foo.pt') + 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 os.path.isabs(spec): + return FSAssetDescriptor(spec) + path = spec + if ':' in path: + pkg_name, path = spec.split(':', 1) + else: + pkg_name = self.package_name + if pkg_name is None: + raise ValueError( + 'relative spec %r irresolveable without package' % (spec,) + ) + return PkgResourcesAssetDescriptor(pkg_name, path) + +class DottedNameResolver(Resolver): + """ A class used to resolve a :term:`dotted Python name` to a package or + module object. + + .. warning:: This API is new as of Pyramid 1.3. + + The constructor accepts a single argument named ``package`` which may be + any of: + + - A fully qualified (not relative) dotted name to a module or package + + - a Python module or package object + + - The value ``None`` + + 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 + 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 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 + to packages, never to modules). For example, if the ``package`` argument + to this type was passed the string ``xml.dom.expatbuilder``, and + ``.mindom`` is supplied to the + :meth:`~pyramid.path.DottedNameResolver.resolve` method, the resulting + import would be for ``xml.minidom``, because ``xml.dom.expatbuilder`` is + a module object, not a package object. + + If a *package* or *package name* (as opposed to a module or module name) + is supplied as ``package``, this package will be used to relative compute + dotted names. For example, if the ``package`` argument to this type was + 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): + """ + This method resolves a dotted name reference to a global Python + object (an object which can be imported) to the object itself. + + Two dotted name styles are supported: + + - ``pkg_resources``-style dotted names where non-module attributes + of a package are separated from the rest of the path using a ``:`` + e.g. ``package.module:attr``. + + - ``zope.dottedname``-style dotted names where non-module + attributes of a package are separated from the rest of the path + using a ``.`` e.g. ``package.module.attr``. + + These styles can be used interchangeably. If the supplied name + contains a ``:`` (colon), the ``pkg_resources`` resolution + 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 + :exc:`ValueError` will be raised. + """ + if not isinstance(name, string_types): + raise ValueError('%r is not a string' % (name,)) + return self.maybe_resolve(name) + + 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 + example: + + .. code-block:: python + + import xml + r = DottedNameResolver() + v = r.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) + return dotted + + + def _pkg_resources_style(self, value): + """ package.module:attr style """ + if value.startswith('.') or value.startswith(':'): + if not self.package_name: + raise ValueError( + 'relative name %r irresolveable without ' + 'package_name' % (value,)) + if value in ['.', ':']: + value = self.package_name + else: + value = self.package_name + value + return pkg_resources.EntryPoint.parse( + 'x=%s' % value).load(False) + + def _zope_dottedname_style(self, value): + """ package.module.attr style """ + module = self.package_name + if not module: + module = None + if value == '.': + if module is None: + raise ValueError( + 'relative name %r irresolveable without package' % (value,) + ) + name = module.split('.') + else: + name = value.split('.') + if not name[0]: + if module is None: + raise ValueError( + 'relative name %r irresolveable without ' + 'package' % (value,) + ) + module = module.split('.') + name.pop(0) + while not name[0]: + module.pop() + name.pop(0) + name = module + name + + used = name.pop(0) + found = __import__(used) + for n in name: + used += '.' + n + try: + found = getattr(found, n) + except AttributeError: + __import__(used) + found = getattr(found, n) # pragma: no cover + + return found + +@implementer(IAssetDescriptor) +class PkgResourcesAssetDescriptor(object): + pkg_resources = pkg_resources + + def __init__(self, pkg_name, path): + self.pkg_name = pkg_name + self.path = path + + def absspec(self): + return '%s:%s' % (self.pkg_name, self.path) + + def abspath(self): + return self.pkg_resources.resource_filename(self.pkg_name, self.path) + + def stream(self): + return self.pkg_resources.resource_stream(self.pkg_name, self.path) + + def isdir(self): + return self.pkg_resources.resource_isdir(self.pkg_name, self.path) + + def listdir(self): + return self.pkg_resources.resource_listdir(self.pkg_name, self.path) + + def exists(self): + return self.pkg_resources.resource_exists(self.pkg_name, self.path) + +@implementer(IAssetDescriptor) +class FSAssetDescriptor(object): + + def __init__(self, path): + self.path = os.path.abspath(path) + + def absspec(self): + raise NotImplementedError + + def abspath(self): + return self.path + + def stream(self): + return open(self.path, 'rb') + + def isdir(self): + return os.path.isdir(self.path) + + def listdir(self): + return os.listdir(self.path) + + def exists(self): + return os.path.exists(self.path) diff --git a/pyramid/tests/test_asset.py b/pyramid/tests/test_asset.py index badb91d91..d3ebd5f7d 100644 --- a/pyramid/tests/test_asset.py +++ b/pyramid/tests/test_asset.py @@ -1,4 +1,7 @@ import unittest +import os + +here = os.path.abspath(os.path.dirname(__file__)) class Test_resolve_asset_spec(unittest.TestCase): def _callFUT(self, spec, package_name='__main__'): @@ -6,11 +9,8 @@ class Test_resolve_asset_spec(unittest.TestCase): return resolve_asset_spec(spec, package_name) def test_abspath(self): - import os - here = os.path.dirname(__file__) - path = os.path.abspath(here) - package_name, filename = self._callFUT(path, 'apackage') - self.assertEqual(filename, path) + package_name, filename = self._callFUT(here, 'apackage') + self.assertEqual(filename, here) self.assertEqual(package_name, None) def test_rel_spec(self): @@ -57,11 +57,8 @@ class Test_abspath_from_asset_spec(unittest.TestCase): self.assertEqual(result, '/abc') def test_pkgrelative(self): - import os - here = os.path.dirname(__file__) - path = os.path.abspath(here) result = self._callFUT('abc', 'pyramid.tests') - self.assertEqual(result, os.path.join(path, 'abc')) + self.assertEqual(result, os.path.join(here, 'abc')) class Test_asset_spec_from_abspath(unittest.TestCase): def _callFUT(self, abspath, package): @@ -74,20 +71,16 @@ class Test_asset_spec_from_abspath(unittest.TestCase): self.assertEqual(result, 'abspath') def test_abspath_startswith_package_path(self): - import os - abspath = os.path.join(os.path.dirname(__file__), 'fixtureapp') + abspath = os.path.join(here, 'fixtureapp') pkg = DummyPackage('pyramid.tests') pkg.__file__ = 'file' result = self._callFUT(abspath, pkg) self.assertEqual(result, 'pyramid:fixtureapp') def test_abspath_doesnt_startwith_package_path(self): - import os - abspath = os.path.dirname(__file__) pkg = DummyPackage('pyramid.tests') - result = self._callFUT(abspath, pkg) - self.assertEqual(result, abspath) - + result = self._callFUT(here, pkg) + self.assertEqual(result, here) class DummyPackage: def __init__(self, name): diff --git a/pyramid/tests/test_path.py b/pyramid/tests/test_path.py index 29b9baf1f..f99436eb8 100644 --- a/pyramid/tests/test_path.py +++ b/pyramid/tests/test_path.py @@ -1,4 +1,8 @@ import unittest +import os +from pyramid.compat import PY3 + +here = os.path.abspath(os.path.dirname(__file__)) class TestCallerPath(unittest.TestCase): def tearDown(self): @@ -16,7 +20,6 @@ class TestCallerPath(unittest.TestCase): def test_pkgrelative(self): import os - here = os.path.abspath(os.path.dirname(__file__)) result = self._callFUT('a/b/c') self.assertEqual(result, os.path.join(here, 'a/b/c')) @@ -29,7 +32,6 @@ class TestCallerPath(unittest.TestCase): def test_memoization_success(self): import os - here = os.path.abspath(os.path.dirname(__file__)) from pyramid.tests import test_path result = self._callFUT('a/b/c') self.assertEqual(result, os.path.join(here, 'a/b/c')) @@ -167,7 +169,343 @@ class TestPackageName(unittest.TestCase): import __main__ result = self._callFUT(__main__) self.assertEqual(result, '__main__') - + +class TestAssetResolver(unittest.TestCase): + def _getTargetClass(self): + from pyramid.path import AssetResolver + return AssetResolver + + def _makeOne(self, package='pyramid.tests'): + return self._getTargetClass()(package) + + def test_ctor_as_package(self): + 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): + from pyramid.path import FSAssetDescriptor + inst = self._makeOne(None) + r = inst.resolve(os.path.join(here, 'test_asset.py')) + self.assertEqual(r.__class__, FSAssetDescriptor) + self.failUnless(r.exists()) + + def test_resolve_absspec(self): + from pyramid.path import PkgResourcesAssetDescriptor + inst = self._makeOne(None) + r = inst.resolve('pyramid.tests:test_asset.py') + self.assertEqual(r.__class__, PkgResourcesAssetDescriptor) + self.failUnless(r.exists()) + + def test_resolve_relspec_with_pkg(self): + from pyramid.path import PkgResourcesAssetDescriptor + inst = self._makeOne('pyramid.tests') + r = inst.resolve('test_asset.py') + self.assertEqual(r.__class__, PkgResourcesAssetDescriptor) + self.failUnless(r.exists()) + + def test_resolve_relspec_no_package(self): + inst = self._makeOne(None) + self.assertRaises(ValueError, inst.resolve, 'test_asset.py') + +class TestPkgResourcesAssetDescriptor(unittest.TestCase): + def _getTargetClass(self): + from pyramid.path import PkgResourcesAssetDescriptor + return PkgResourcesAssetDescriptor + + def _makeOne(self, pkg='pyramid.tests', path='test_asset.py'): + return self._getTargetClass()(pkg, path) + + def test_class_implements(self): + from pyramid.interfaces import IAssetDescriptor + from zope.interface.verify import verifyClass + klass = self._getTargetClass() + verifyClass(IAssetDescriptor, klass) + + def test_instance_implements(self): + from pyramid.interfaces import IAssetDescriptor + from zope.interface.verify import verifyObject + inst = self._makeOne() + verifyObject(IAssetDescriptor, inst) + + def test_absspec(self): + inst = self._makeOne() + self.assertEqual(inst.absspec(), 'pyramid.tests:test_asset.py') + + def test_abspath(self): + inst = self._makeOne() + self.assertEqual(inst.abspath(), os.path.join(here, 'test_asset.py')) + + def test_stream(self): + inst = self._makeOne() + inst.pkg_resources = DummyPkgResource() + inst.pkg_resources.resource_stream = lambda x, y: '%s:%s' % (x, y) + self.assertEqual(inst.stream(), + '%s:%s' % ('pyramid.tests', 'test_asset.py')) + + def test_isdir(self): + inst = self._makeOne() + inst.pkg_resources = DummyPkgResource() + inst.pkg_resources.resource_isdir = lambda x, y: '%s:%s' % (x, y) + self.assertEqual(inst.isdir(), + '%s:%s' % ('pyramid.tests', 'test_asset.py')) + + def test_listdir(self): + inst = self._makeOne() + inst.pkg_resources = DummyPkgResource() + inst.pkg_resources.resource_listdir = lambda x, y: '%s:%s' % (x, y) + self.assertEqual(inst.listdir(), + '%s:%s' % ('pyramid.tests', 'test_asset.py')) + + def test_exists(self): + inst = self._makeOne() + inst.pkg_resources = DummyPkgResource() + inst.pkg_resources.resource_exists = lambda x, y: '%s:%s' % (x, y) + self.assertEqual(inst.exists(), + '%s:%s' % ('pyramid.tests', 'test_asset.py')) + +class TestFSAssetDescriptor(unittest.TestCase): + def _getTargetClass(self): + from pyramid.path import FSAssetDescriptor + return FSAssetDescriptor + + def _makeOne(self, path=os.path.join(here, 'test_asset.py')): + return self._getTargetClass()(path) + + def test_class_implements(self): + from pyramid.interfaces import IAssetDescriptor + from zope.interface.verify import verifyClass + klass = self._getTargetClass() + verifyClass(IAssetDescriptor, klass) + + def test_instance_implements(self): + from pyramid.interfaces import IAssetDescriptor + from zope.interface.verify import verifyObject + inst = self._makeOne() + verifyObject(IAssetDescriptor, inst) + + def test_absspec(self): + inst = self._makeOne() + self.assertRaises(NotImplementedError, inst.absspec) + + def test_abspath(self): + inst = self._makeOne() + self.assertEqual(inst.abspath(), os.path.join(here, 'test_asset.py')) + + def test_stream(self): + inst = self._makeOne() + val = inst.stream().read() + self.assertTrue(b'asset' in val) + + def test_isdir_False(self): + inst = self._makeOne() + self.assertFalse(inst.isdir()) + + def test_isdir_True(self): + inst = self._makeOne(here) + self.assertTrue(inst.isdir()) + + def test_listdir(self): + inst = self._makeOne(here) + self.assertTrue(inst.listdir()) + + def test_exists(self): + inst = self._makeOne() + self.assertTrue(inst.exists()) + +class TestDottedNameResolver(unittest.TestCase): + def _makeOne(self, package=None): + from pyramid.path import DottedNameResolver + return DottedNameResolver(package) + + def config_exc(self, func, *arg, **kw): + try: + func(*arg, **kw) + except ValueError as e: + return e + else: + raise AssertionError('Invalid not raised') # pragma: no cover + + def test_zope_dottedname_style_resolve_builtin(self): + typ = self._makeOne() + if PY3: # pragma: no cover + result = typ._zope_dottedname_style('builtins.str') + else: + result = typ._zope_dottedname_style('__builtin__.str') + 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') + 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') + + def test__zope_dottedname_style_resolve_relative(self): + import pyramid.tests + typ = self._makeOne(package=pyramid.tests) + result = typ._zope_dottedname_style( + '.test_path.TestDottedNameResolver') + 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) + result = typ._zope_dottedname_style( + '..tests.test_path.TestDottedNameResolver') + 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('.') + 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, '.') + 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') + 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) + self.assertRaises(ImportError, typ._zope_dottedname_style, + '.notexisting') + + def test__zope_dottedname_style_resolveable_relative(self): + import pyramid + typ = self._makeOne(package=pyramid) + result = typ._zope_dottedname_style('.tests') + from pyramid import tests + self.assertEqual(result, tests) + + def test__zope_dottedname_style_irresolveable_absolute(self): + typ = self._makeOne() + self.assertRaises( + ImportError, + typ._zope_dottedname_style, 'pyramid.fudge.bar') + + def test__zope_dottedname_style_resolveable_absolute(self): + typ = self._makeOne() + result = typ._zope_dottedname_style( + 'pyramid.tests.test_path.TestDottedNameResolver') + 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') + 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') + + def test__pkg_resources_style_resolve_relative(self): + import pyramid.tests + typ = self._makeOne(package=pyramid.tests) + result = typ._pkg_resources_style( + '.test_path:TestDottedNameResolver') + 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('.') + 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') + + def test__pkg_resources_style_irrresolveable_relative(self): + import pyramid + typ = self._makeOne(package=pyramid) + self.assertRaises(ImportError, typ._pkg_resources_style, + ':notexisting') + + def test_resolve_not_a_string(self): + typ = self._makeOne() + e = self.config_exc(typ.resolve, None) + self.assertEqual(e.args[0], 'None is not a string') + + def test_resolve_using_pkgresources_style(self): + typ = self._makeOne() + result = typ.resolve( + 'pyramid.tests.test_path:TestDottedNameResolver') + self.assertEqual(result, self.__class__) + + def test_resolve_using_zope_dottedname_style(self): + typ = self._makeOne() + result = typ.resolve( + 'pyramid.tests.test_path:TestDottedNameResolver') + self.assertEqual(result, self.__class__) + + def test_resolve_missing_raises(self): + typ = self._makeOne() + self.assertRaises(ImportError, typ.resolve, 'cant.be.found') + + 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') + + def test_ctor_module(self): + import pyramid.tests + 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 + class DummyPackageOrModule: def __init__(self, real_package_or_module, raise_exc=None): self.__dict__['raise_exc'] = raise_exc @@ -181,9 +519,3 @@ class DummyPackageOrModule: if self.raise_exc is not None: raise self.raise_exc self.__dict__[key] = val - - - - - - diff --git a/pyramid/tests/test_util.py b/pyramid/tests/test_util.py index 61e372417..b9a9d1960 100644 --- a/pyramid/tests/test_util.py +++ b/pyramid/tests/test_util.py @@ -1,189 +1,6 @@ import unittest from pyramid.compat import PY3 -class TestDottedNameResolver(unittest.TestCase): - def _makeOne(self, package=None): - from pyramid.util import DottedNameResolver - return DottedNameResolver(package) - - def config_exc(self, func, *arg, **kw): - from pyramid.exceptions import ConfigurationError - try: - func(*arg, **kw) - except ConfigurationError as e: - return e - else: - raise AssertionError('Invalid not raised') # pragma: no cover - - def test_zope_dottedname_style_resolve_builtin(self): - typ = self._makeOne() - if PY3: # pragma: no cover - result = typ._zope_dottedname_style('builtins.str') - else: - result = typ._zope_dottedname_style('__builtin__.str') - self.assertEqual(result, str) - - def test_zope_dottedname_style_resolve_absolute(self): - typ = self._makeOne() - result = typ._zope_dottedname_style( - 'pyramid.tests.test_util.TestDottedNameResolver') - 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_util.nonexisting_name') - - def test__zope_dottedname_style_resolve_relative(self): - import pyramid.tests - typ = self._makeOne(package=pyramid.tests) - result = typ._zope_dottedname_style( - '.test_util.TestDottedNameResolver') - 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) - result = typ._zope_dottedname_style( - '..tests.test_util.TestDottedNameResolver') - 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('.') - 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, '.') - 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') - 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) - self.assertRaises(ImportError, typ._zope_dottedname_style, - '.notexisting') - - def test__zope_dottedname_style_resolveable_relative(self): - import pyramid - typ = self._makeOne(package=pyramid) - result = typ._zope_dottedname_style('.tests') - from pyramid import tests - self.assertEqual(result, tests) - - def test__zope_dottedname_style_irresolveable_absolute(self): - typ = self._makeOne() - self.assertRaises( - ImportError, - typ._zope_dottedname_style, 'pyramid.fudge.bar') - - def test__zope_dottedname_style_resolveable_absolute(self): - typ = self._makeOne() - result = typ._zope_dottedname_style( - 'pyramid.tests.test_util.TestDottedNameResolver') - self.assertEqual(result, self.__class__) - - def test__pkg_resources_style_resolve_absolute(self): - typ = self._makeOne() - result = typ._pkg_resources_style( - 'pyramid.tests.test_util:TestDottedNameResolver') - 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') - - def test__pkg_resources_style_resolve_relative(self): - import pyramid.tests - typ = self._makeOne(package=pyramid.tests) - result = typ._pkg_resources_style( - '.test_util:TestDottedNameResolver') - 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('.') - self.assertEqual(result, pyramid.tests) - - def test__pkg_resources_style_resolve_relative_nocurrentpackage(self): - typ = self._makeOne() - from pyramid.exceptions import ConfigurationError - self.assertRaises(ConfigurationError, typ._pkg_resources_style, - '.whatever') - - def test__pkg_resources_style_irrresolveable_relative(self): - import pyramid - typ = self._makeOne(package=pyramid) - self.assertRaises(ImportError, typ._pkg_resources_style, - ':notexisting') - - def test_resolve_not_a_string(self): - typ = self._makeOne() - e = self.config_exc(typ.resolve, None) - self.assertEqual(e.args[0], 'None is not a string') - - def test_resolve_using_pkgresources_style(self): - typ = self._makeOne() - result = typ.resolve( - 'pyramid.tests.test_util:TestDottedNameResolver') - self.assertEqual(result, self.__class__) - - def test_resolve_using_zope_dottedname_style(self): - typ = self._makeOne() - result = typ.resolve( - 'pyramid.tests.test_util:TestDottedNameResolver') - self.assertEqual(result, self.__class__) - - def test_resolve_missing_raises(self): - typ = self._makeOne() - self.assertRaises(ImportError, typ.resolve, 'cant.be.found') - - def test_ctor_string_module_resolveable(self): - import pyramid.tests - typ = self._makeOne('pyramid.tests.test_util') - 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): - from pyramid.config import ConfigurationError - self.assertRaises(ConfigurationError, self._makeOne, 'cant.be.found') - - def test_ctor_module(self): - import pyramid.tests - import pyramid.tests.test_util - typ = self._makeOne(pyramid.tests.test_util) - 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 Test_WeakOrderedSet(unittest.TestCase): def _makeOne(self): from pyramid.config import WeakOrderedSet diff --git a/pyramid/util.py b/pyramid/util.py index f22f847c4..2018fb73e 100644 --- a/pyramid/util.py +++ b/pyramid/util.py @@ -1,6 +1,4 @@ import inspect -import pkg_resources -import sys import weakref from pyramid.compat import ( @@ -10,147 +8,9 @@ from pyramid.compat import ( PY3, ) -from pyramid.exceptions import ConfigurationError -from pyramid.path import package_of +from pyramid.path import DottedNameResolver # bw compat -class DottedNameResolver(object): - """ This class resolves dotted name references to 'global' Python - objects (objects which can be imported) to those objects. - - Two dotted name styles are supported during deserialization: - - - ``pkg_resources``-style dotted names where non-module attributes - of a package are separated from the rest of the path using a ':' - e.g. ``package.module:attr``. - - - ``zope.dottedname``-style dotted names where non-module - attributes of a package are separated from the rest of the path - using a '.' e.g. ``package.module.attr``. - - These styles can be used interchangeably. If the serialization - contains a ``:`` (colon), the ``pkg_resources`` resolution - mechanism will be chosen, otherwise the ``zope.dottedname`` - resolution mechanism will be chosen. - - The constructor accepts a single argument named ``package`` which - should be a one of: - - - a Python module or package object - - - A fully qualified (not relative) dotted name to a module or package - - - The value ``None`` - - The ``package`` is used when relative dotted names are supplied to - the resolver's ``resolve`` and ``maybe_resolve`` methods. 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 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:`pyramid.config.ConfigurationError` exception. - - 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 to packages, never to modules). For - example, if the ``package`` argument to this type was passed the - string ``xml.dom.expatbuilder``, and ``.mindom`` is supplied to - the ``resolve`` method, the resulting import would be for - ``xml.minidom``, because ``xml.dom.expatbuilder`` is a module - object, not a package object. - - If a *package* or *package name* (as opposed to a module or module - name) is supplied as ``package``, this package will be used to - relative compute dotted names. For example, if the ``package`` - argument to this type was passed the string ``xml.dom``, and - ``.minidom`` is supplied to the ``resolve`` method, the resulting - import would be for ``xml.minidom``. - - When a dotted name cannot be resolved, a - :class:`pyramid.exceptions.ConfigurationError` error is raised. - """ - def __init__(self, package): - if package is None: - self.package_name = None - self.package = None - else: - if isinstance(package, string_types): - try: - __import__(package) - except ImportError: - raise ConfigurationError( - 'The dotted name %r cannot be imported' % (package,)) - package = sys.modules[package] - self.package = package_of(package) - self.package_name = self.package.__name__ - - def _pkg_resources_style(self, value): - """ package.module:attr style """ - if value.startswith('.') or value.startswith(':'): - if not self.package_name: - raise ConfigurationError( - 'relative name %r irresolveable without ' - 'package_name' % (value,)) - if value in ['.', ':']: - value = self.package_name - else: - value = self.package_name + value - return pkg_resources.EntryPoint.parse( - 'x=%s' % value).load(False) - - def _zope_dottedname_style(self, value): - """ package.module.attr style """ - module = self.package_name - if not module: - module = None - if value == '.': - if module is None: - raise ConfigurationError( - 'relative name %r irresolveable without package' % (value,) - ) - name = module.split('.') - else: - name = value.split('.') - if not name[0]: - if module is None: - raise ConfigurationError( - 'relative name %r irresolveable without ' - 'package' % (value,) - ) - module = module.split('.') - name.pop(0) - while not name[0]: - module.pop() - name.pop(0) - name = module + name - - used = name.pop(0) - found = __import__(used) - for n in name: - used += '.' + n - try: - found = getattr(found, n) - except AttributeError: - __import__(used) - found = getattr(found, n) # pragma: no cover - - return found - - def resolve(self, dotted): - if not isinstance(dotted, string_types): - raise ConfigurationError('%r is not a string' % (dotted,)) - return self.maybe_resolve(dotted) - - def maybe_resolve(self, dotted): - if isinstance(dotted, string_types): - if ':' in dotted: - return self._pkg_resources_style(dotted) - else: - return self._zope_dottedname_style(dotted) - return dotted +DottedNameResolver = DottedNameResolver # for pyflakes class WeakOrderedSet(object): """ Maintain a set of items. |
