summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorVinay Sajip <vinay_sajip@yahoo.co.uk>2020-02-14 22:02:13 (GMT)
committerGitHub <noreply@github.com>2020-02-14 22:02:13 (GMT)
commit1ed61617a4a6632905ad6a0b440cd2cafb8b6414 (patch)
treebfe15c8f526f05a0c214d409bb94e64e750442a4 /Lib
parent9aeb0ef9309384099e2f23bcee2240fbc096568e (diff)
downloadcpython-1ed61617a4a6632905ad6a0b440cd2cafb8b6414.zip
cpython-1ed61617a4a6632905ad6a0b440cd2cafb8b6414.tar.gz
cpython-1ed61617a4a6632905ad6a0b440cd2cafb8b6414.tar.bz2
bpo-12915: Add pkgutil.resolve_name (GH-18310)
Diffstat (limited to 'Lib')
-rw-r--r--Lib/pkgutil.py69
-rw-r--r--Lib/test/test_pkgutil.py55
2 files changed, 124 insertions, 0 deletions
diff --git a/Lib/pkgutil.py b/Lib/pkgutil.py
index 8474a77..4bc3083 100644
--- a/Lib/pkgutil.py
+++ b/Lib/pkgutil.py
@@ -7,6 +7,7 @@ import importlib.util
import importlib.machinery
import os
import os.path
+import re
import sys
from types import ModuleType
import warnings
@@ -635,3 +636,71 @@ def get_data(package, resource):
parts.insert(0, os.path.dirname(mod.__file__))
resource_name = os.path.join(*parts)
return loader.get_data(resource_name)
+
+
+_DOTTED_WORDS = r'[a-z_]\w*(\.[a-z_]\w*)*'
+_NAME_PATTERN = re.compile(f'^({_DOTTED_WORDS})(:({_DOTTED_WORDS})?)?$', re.I)
+del _DOTTED_WORDS
+
+def resolve_name(name):
+ """
+ Resolve a name to an object.
+
+ It is expected that `name` will be a string in one of the following
+ formats, where W is shorthand for a valid Python identifier and dot stands
+ for a literal period in these pseudo-regexes:
+
+ W(.W)*
+ W(.W)*:(W(.W)*)?
+
+ The first form is intended for backward compatibility only. It assumes that
+ some part of the dotted name is a package, and the rest is an object
+ somewhere within that package, possibly nested inside other objects.
+ Because the place where the package stops and the object hierarchy starts
+ can't be inferred by inspection, repeated attempts to import must be done
+ with this form.
+
+ In the second form, the caller makes the division point clear through the
+ provision of a single colon: the dotted name to the left of the colon is a
+ package to be imported, and the dotted name to the right is the object
+ hierarchy within that package. Only one import is needed in this form. If
+ it ends with the colon, then a module object is returned.
+
+ The function will return an object (which might be a module), or raise one
+ of the following exceptions:
+
+ ValueError - if `name` isn't in a recognised format
+ ImportError - if an import failed when it shouldn't have
+ AttributeError - if a failure occurred when traversing the object hierarchy
+ within the imported package to get to the desired object)
+ """
+ m = _NAME_PATTERN.match(name)
+ if not m:
+ raise ValueError(f'invalid format: {name!r}')
+ groups = m.groups()
+ if groups[2]:
+ # there is a colon - a one-step import is all that's needed
+ mod = importlib.import_module(groups[0])
+ parts = groups[3].split('.') if groups[3] else []
+ else:
+ # no colon - have to iterate to find the package boundary
+ parts = name.split('.')
+ modname = parts.pop(0)
+ # first part *must* be a module/package.
+ mod = importlib.import_module(modname)
+ while parts:
+ p = parts[0]
+ s = f'{modname}.{p}'
+ try:
+ mod = importlib.import_module(s)
+ parts.pop(0)
+ modname = s
+ except ImportError:
+ break
+ # if we reach this point, mod is the module, already imported, and
+ # parts is the list of parts in the object hierarchy to be traversed, or
+ # an empty list if just the module is wanted.
+ result = mod
+ for p in parts:
+ result = getattr(result, p)
+ return result
diff --git a/Lib/test/test_pkgutil.py b/Lib/test/test_pkgutil.py
index 2887ce6..906150b 100644
--- a/Lib/test/test_pkgutil.py
+++ b/Lib/test/test_pkgutil.py
@@ -186,6 +186,61 @@ class PkgutilTests(unittest.TestCase):
with self.assertRaises((TypeError, ValueError)):
list(pkgutil.walk_packages(bytes_input))
+ def test_name_resolution(self):
+ import logging
+ import logging.handlers
+
+ success_cases = (
+ ('os', os),
+ ('os.path', os.path),
+ ('os.path:pathsep', os.path.pathsep),
+ ('logging', logging),
+ ('logging:', logging),
+ ('logging.handlers', logging.handlers),
+ ('logging.handlers:', logging.handlers),
+ ('logging.handlers:SysLogHandler', logging.handlers.SysLogHandler),
+ ('logging.handlers.SysLogHandler', logging.handlers.SysLogHandler),
+ ('logging.handlers:SysLogHandler.LOG_ALERT',
+ logging.handlers.SysLogHandler.LOG_ALERT),
+ ('logging.handlers.SysLogHandler.LOG_ALERT',
+ logging.handlers.SysLogHandler.LOG_ALERT),
+ ('builtins.int', int),
+ ('builtins:int', int),
+ ('builtins.int.from_bytes', int.from_bytes),
+ ('builtins:int.from_bytes', int.from_bytes),
+ ('builtins.ZeroDivisionError', ZeroDivisionError),
+ ('builtins:ZeroDivisionError', ZeroDivisionError),
+ ('os:path', os.path),
+ )
+
+ failure_cases = (
+ (None, TypeError),
+ (1, TypeError),
+ (2.0, TypeError),
+ (True, TypeError),
+ ('', ValueError),
+ ('?abc', ValueError),
+ ('abc/foo', ValueError),
+ ('foo', ImportError),
+ ('os.foo', AttributeError),
+ ('os.foo:', ImportError),
+ ('os.pth:pathsep', ImportError),
+ ('logging.handlers:NoSuchHandler', AttributeError),
+ ('logging.handlers:SysLogHandler.NO_SUCH_VALUE', AttributeError),
+ ('logging.handlers.SysLogHandler.NO_SUCH_VALUE', AttributeError),
+ ('ZeroDivisionError', ImportError),
+ )
+
+ for s, expected in success_cases:
+ with self.subTest(s=s):
+ o = pkgutil.resolve_name(s)
+ self.assertEqual(o, expected)
+
+ for s, exc in failure_cases:
+ with self.subTest(s=s):
+ with self.assertRaises(exc):
+ pkgutil.resolve_name(s)
+
class PkgutilPEP302Tests(unittest.TestCase):