8000 bpo-12915: Add pkgutil.resolve_name (GH-18310) · python/cpython@1ed6161 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1ed6161

Browse files
authored
bpo-12915: Add pkgutil.resolve_name (GH-18310)
1 parent 9aeb0ef commit 1ed6161

File tree

4 files changed

+169
-0
lines changed

4 files changed

+169
-0
lines changed

Doc/library/pkgutil.rst

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,44 @@ support.
227227
then ``None`` is returned. In particular, the :term:`loader` for
228228
:term:`namespace packages <namespace package>` does not support
229229
:meth:`get_data <importlib.abc.ResourceLoader.get_data>`.
230+
231+
232+
.. function:: resolve_name(name)
233+
234+
Resolve a name to an object.
235+
236+
This functionality is used in numerous places in the standard library (see
237+
:issue:`12915`) - and equivalent functionality is also in widely used
238+
third-party packages such as setuptools, Django and Pyramid.
239+
240+
It is expected that *name* will be a string in one of the following
241+
formats, where W is shorthand for a valid Python identifier and dot stands
242+
for a literal period in these pseudo-regexes:
243+
244+
* ``W(.W)*``
245+
* ``W(.W)*:(W(.W)*)?``
246+
247+
The first form is intended for backward compatibility only. It assumes that
248+
some part of the dotted name is a package, and the rest is an object
249+
somewhere within that package, possibly nested inside other objects.
250+
Because the place where the package stops and the object hierarchy starts
251+
can't be inferred by inspection, repeated attempts to import must be done
252+
with this form.
253+
254+
In the second form, the caller makes the division point clear through the
255+
provision of a single colon: the dotted name to the left of the colon is a
256+
package to be imported, and the dotted name to the right is the object
257+
hierarchy within that package. Only one import is needed in this form. If
258+
it ends with the colon, then a module object is returned.
259+
260+
The function will return an object (which might be a module), or raise one
261+
of the following exceptions:
262+
263+
:exc:`ValueError` -- if *name* isn't in a recognised format.
264+
265+
:exc:`ImportError` -- if an import failed when it shouldn't have.
266+
267+
:exc:`AttributeError` -- If a failure occurred when traversing the object
268+
hierarchy within the imported package to get to the desired object.
269+
270+
.. versionadded:: 3.9

Lib/pkgutil.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import importlib.machinery
88
import os
99
import os.path
10+
import re
1011
import sys
1112
from types import ModuleType
1213
import warnings
@@ -635,3 +636,71 @@ def get_data(package, resource):
635636
parts.insert(0, os.path.dirname(mod.__file__))
636637
resource_name = os.path.join(*parts)
637638
return loader.get_data(resource_name)
639+
640+
641+
_DOTTED_WORDS = r'[a-z_]\w*(\.[a-z_]\w*)*'
642+
_NAME_PATTERN = re.compile(f'^({_DOTTED_WORDS})(:({_DOTTED_WORDS})?)?$', re.I)
643+
del _DOTTED_WORDS
644+
645+
def resolve_name(name):
646+
"""
647+
Resolve a name to an object.
648+
649+
It is expected that `name` will be a string in one of the following
650+
formats, where W is shorthand for a valid Python identifier and dot stands
651+
for a literal period in these pseudo-regexes:
652+
653+
W(.W)*
654+
W(.W)*:(W(.W)*)?
655+
656+
The first form is intended for backward compatibility only. It assumes that
657+
some part of the dotted name is a package, and the rest is an object
658+
somewhere within that package, possibly nested inside other objects.
659+
Because the place where the package stops and the object hierarchy starts
660+
can't be inferred by inspection, repeated attempts to import must be done
661+
with this form.
662+
663+
In the second form, the caller makes the division point clear through the
664+
provision of a single colon: the dotted name to the left of the colon is a
665+
package to be imported, and the dotted name to the right is the object
666+
hierarchy within that package. Only one import is needed in this form. If
667+
it ends with the colon, then a module object is returned.
668+
669+
The function will return an object (which might be a module), or raise one
670+
of the following exceptions:
671+
672+
ValueError - if `name` isn't in a recognised format
673+
ImportError - if an import failed when it shouldn't have
674+
AttributeError - if a failure occurred when traversing the object hierarchy
675+
within the imported package to get to the desired object)
676+
"""
677+
m = _NAME_PATTERN.match(name)
678+
if not m:
679+
raise ValueError(f'invalid format: {name!r}')
680+
groups = m.groups()
681+
if groups[2]:
682+
# there is a colon - a one-step import is all that's needed
683+
mod = importlib.import_module(groups[0])
684+
parts = groups[3].split('.') if groups[3] else []
685+
else:
686+
# no colon - have to iterate to find the package boundary
687+
parts = name.split('.')
688+
modname = parts.pop(0)
689+
# first part *must* be a module/package.
690+
mod = importlib.import_module(modname)
691+
while parts:
692+
p = parts[0]
693+
s = f'{modname}.{p}'
694+
try:
695+
mod = importlib.import_module(s)
696+
parts.pop(0)
697+
modname = s
698+
except ImportError:
699+
break
700+
# if we reach this point, mod is the module, already imported, and
701+
# parts is the list of parts in the object hierarchy to be traversed, or
702+
# an empty list if just the module is wanted.
703+
result = mod
704+
for p in parts:
705+
result = getattr(result, p)
706+
return result

Lib/test/test_pkgutil.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,61 @@ def test_walk_packages_raises_on_string_or_bytes_input(self):
186186
with self.assertRaises((TypeError, ValueError)):
187187
list(pkgutil.walk_packages(bytes_input))
188188

189+
def test_name_resolution(self):
190+
import logging
191+
import logging.handlers
192+
193+
success_cases = (
194+
('os', os),
195+
('os.path', os.path),
196+
('os.path:pathsep', os.path.pathsep),
197+
('logging', logging),
198+
('logging:', logging),
199+
('logging.handlers', logging.handlers),
200+
('logging.handlers:', logging.handlers),
201+
('logging.handlers:SysLogHandler', logging.handlers.SysLogHandler),
202+
('logging.handlers.SysLogHandler', logging.handlers.SysLogHandler),
203+
('logging.handlers:SysLogHandler.LOG_ALERT',
204+
logging.handlers.SysLogHandler.LOG_ALERT),
205+
('logging.handlers.SysLogHandler.LOG_ALERT',
206+
logging.handlers.SysLogHandler.LOG_ALERT),
207+
('builtins.int', int),
208+
('builtins:int', int),
209+
('builtins.int.from_bytes', int.from_bytes),
210+
('builtins:int.from_bytes', int.from_bytes),
211+
('builtins.ZeroDivisionError', ZeroDivisionError),
212+
('builtins:ZeroDivisionError', ZeroDivisionError),
213+
('os:path', os.path),
214+
)
215+
216+
failure_cases = (
217+
(None, TypeError),
218+
(1, TypeError),
219+
(2.0, TypeError),
220+
(True, TypeError),
221+
('', ValueError),
222+
('?abc', ValueError),
223+
('abc/foo', ValueError),
224+
('foo', ImportError),
225+
('os.foo', AttributeError),
226+
('os.foo:', ImportError),
227+
('os.pth:pathsep', ImportError),
228+
('logging.handlers:NoSuchHandler', AttributeError),
229+
('logging.handlers:SysLogHandler.NO_SUCH_VALUE', AttributeError),
230+
('logging.handlers.SysLogHandler.NO_SUCH_VALUE', AttributeError),
231+
('ZeroDivisionError', ImportError),
232+
)
233+
234+
for s, expected in success_cases:
235+
with self.subTest(s=s):
236+
o = pkgutil.resolve_name(s)
237+
self.assertEqual(o, expected)
238+
239+
for s, exc in failure_cases:
240+
with self.subTest(s=s):
241+
with self.assertRaises(exc):
242+
pkgutil.resolve_name(s)
243+
189244

190245
class PkgutilPEP302Tests(unittest.TestCase):
191246

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
A new function ``resolve_name`` has been added to the ``pkgutil`` module.
2+
This resolves a string of the form ``'a.b.c.d'`` or ``'a.b:c.d'`` to an
3+
object. In the example, ``a.b`` is a package/module and ``c.d`` is an object
4+
within that package/module reached via recursive attribute access.

0 commit comments

Comments
 (0)
0