[Python-checkins] bpo-44893: Implement EntryPoint as simple class with attributes. (GH-30150)

jaraco webhook-mailer at python.org
Thu Dec 16 15:49:46 EST 2021


https://github.com/python/cpython/commit/04deaee4c8d313717f3ea8f6a4fd70286d510d6e
commit: 04deaee4c8d313717f3ea8f6a4fd70286d510d6e
branch: main
author: Jason R. Coombs <jaraco at jaraco.com>
committer: jaraco <jaraco at jaraco.com>
date: 2021-12-16T15:49:42-05:00
summary:

bpo-44893: Implement EntryPoint as simple class with attributes. (GH-30150)

* bpo-44893: Implement EntryPoint as simple class and deprecate tuple access in favor of attribute access. Syncs with importlib_metadata 4.8.1.

* Apply refactorings found in importlib_metadata 4.8.2.

files:
A Lib/test/test_importlib/data/example2-1.0.0-py3-none-any.whl
A Misc/NEWS.d/next/Library/2021-12-16-13-54-55.bpo-44893.I7aLiW.rst
M Lib/importlib/metadata/__init__.py
M Lib/importlib/metadata/_functools.py
M Lib/importlib/metadata/_itertools.py
M Lib/importlib/metadata/_meta.py
M Lib/importlib/metadata/_text.py
M Lib/test/test_importlib/fixtures.py
M Lib/test/test_importlib/test_main.py
M Lib/test/test_importlib/test_metadata_api.py
M Lib/test/test_importlib/test_zip.py

diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py
index ec41ed39157a9..d44541fcbfbf4 100644
--- a/Lib/importlib/metadata/__init__.py
+++ b/Lib/importlib/metadata/__init__.py
@@ -15,10 +15,9 @@
 import collections
 
 from . import _adapters, _meta
-from ._meta import PackageMetadata
 from ._collections import FreezableDefaultDict, Pair
-from ._functools import method_cache
-from ._itertools import unique_everseen
+from ._functools import method_cache, pass_none
+from ._itertools import always_iterable, unique_everseen
 from ._meta import PackageMetadata, SimplePath
 
 from contextlib import suppress
@@ -121,8 +120,33 @@ def valid(line):
         return line and not line.startswith('#')
 
 
-class EntryPoint(
-        collections.namedtuple('EntryPointBase', 'name value group')):
+class DeprecatedTuple:
+    """
+    Provide subscript item access for backward compatibility.
+
+    >>> recwarn = getfixture('recwarn')
+    >>> ep = EntryPoint(name='name', value='value', group='group')
+    >>> ep[:]
+    ('name', 'value', 'group')
+    >>> ep[0]
+    'name'
+    >>> len(recwarn)
+    1
+    """
+
+    _warn = functools.partial(
+        warnings.warn,
+        "EntryPoint tuple interface is deprecated. Access members by name.",
+        DeprecationWarning,
+        stacklevel=2,
+    )
+
+    def __getitem__(self, item):
+        self._warn()
+        return self._key()[item]
+
+
+class EntryPoint(DeprecatedTuple):
     """An entry point as defined by Python packaging conventions.
 
     See `the packaging docs on entry points
@@ -153,6 +177,9 @@ class EntryPoint(
 
     dist: Optional['Distribution'] = None
 
+    def __init__(self, name, value, group):
+        vars(self).update(name=name, value=value, group=group)
+
     def load(self):
         """Load the entry point from its definition. If only a module
         is indicated by the value, return that module. Otherwise,
@@ -179,7 +206,7 @@ def extras(self):
         return list(re.finditer(r'\w+', match.group('extras') or ''))
 
     def _for(self, dist):
-        self.dist = dist
+        vars(self).update(dist=dist)
         return self
 
     def __iter__(self):
@@ -193,16 +220,31 @@ def __iter__(self):
         warnings.warn(msg, DeprecationWarning)
         return iter((self.name, self))
 
-    def __reduce__(self):
-        return (
-            self.__class__,
-            (self.name, self.value, self.group),
-        )
-
     def matches(self, **params):
         attrs = (getattr(self, param) for param in params)
         return all(map(operator.eq, params.values(), attrs))
 
+    def _key(self):
+        return self.name, self.value, self.group
+
+    def __lt__(self, other):
+        return self._key() < other._key()
+
+    def __eq__(self, other):
+        return self._key() == other._key()
+
+    def __setattr__(self, name, value):
+        raise AttributeError("EntryPoint objects are immutable.")
+
+    def __repr__(self):
+        return (
+            f'EntryPoint(name={self.name!r}, value={self.value!r}, '
+            f'group={self.group!r})'
+        )
+
+    def __hash__(self):
+        return hash(self._key())
+
 
 class DeprecatedList(list):
     """
@@ -243,37 +285,26 @@ class DeprecatedList(list):
         stacklevel=2,
     )
 
-    def __setitem__(self, *args, **kwargs):
-        self._warn()
-        return super().__setitem__(*args, **kwargs)
-
-    def __delitem__(self, *args, **kwargs):
-        self._warn()
-        return super().__delitem__(*args, **kwargs)
-
-    def append(self, *args, **kwargs):
-        self._warn()
-        return super().append(*args, **kwargs)
-
-    def reverse(self, *args, **kwargs):
-        self._warn()
-        return super().reverse(*args, **kwargs)
-
-    def extend(self, *args, **kwargs):
-        self._warn()
-        return super().extend(*args, **kwargs)
-
-    def pop(self, *args, **kwargs):
-        self._warn()
-        return super().pop(*args, **kwargs)
-
-    def remove(self, *args, **kwargs):
-        self._warn()
-        return super().remove(*args, **kwargs)
-
-    def __iadd__(self, *args, **kwargs):
-        self._warn()
-        return super().__iadd__(*args, **kwargs)
+    def _wrap_deprecated_method(method_name: str):  # type: ignore
+        def wrapped(self, *args, **kwargs):
+            self._warn()
+            return getattr(super(), method_name)(*args, **kwargs)
+
+        return wrapped
+
+    for method_name in [
+        '__setitem__',
+        '__delitem__',
+        'append',
+        'reverse',
+        'extend',
+        'pop',
+        'remove',
+        '__iadd__',
+        'insert',
+        'sort',
+    ]:
+        locals()[method_name] = _wrap_deprecated_method(method_name)
 
     def __add__(self, other):
         if not isinstance(other, tuple):
@@ -281,14 +312,6 @@ def __add__(self, other):
             other = tuple(other)
         return self.__class__(tuple(self) + other)
 
-    def insert(self, *args, **kwargs):
-        self._warn()
-        return super().insert(*args, **kwargs)
-
-    def sort(self, *args, **kwargs):
-        self._warn()
-        return super().sort(*args, **kwargs)
-
     def __eq__(self, other):
         if not isinstance(other, tuple):
             self._warn()
@@ -333,7 +356,7 @@ def names(self):
         """
         Return the set of all names of all entry points.
         """
-        return set(ep.name for ep in self)
+        return {ep.name for ep in self}
 
     @property
     def groups(self):
@@ -344,21 +367,17 @@ def groups(self):
         >>> EntryPoints().groups
         set()
         """
-        return set(ep.group for ep in self)
+        return {ep.group for ep in self}
 
     @classmethod
     def _from_text_for(cls, text, dist):
         return cls(ep._for(dist) for ep in cls._from_text(text))
 
-    @classmethod
-    def _from_text(cls, text):
-        return itertools.starmap(EntryPoint, cls._parse_groups(text or ''))
-
     @staticmethod
-    def _parse_groups(text):
+    def _from_text(text):
         return (
-            (item.value.name, item.value.value, item.name)
-            for item in Sectioned.section_pairs(text)
+            EntryPoint(name=item.value.name, value=item.value.value, group=item.name)
+            for item in Sectioned.section_pairs(text or '')
         )
 
 
@@ -611,7 +630,6 @@ def files(self):
         missing.
         Result may be empty if the metadata exists but is empty.
         """
-        file_lines = self._read_files_distinfo() or self._read_files_egginfo()
 
         def make_file(name, hash=None, size_str=None):
             result = PackagePath(name)
@@ -620,7 +638,11 @@ def make_file(name, hash=None, size_str=None):
             result.dist = self
             return result
 
-        return file_lines and list(starmap(make_file, csv.reader(file_lines)))
+        @pass_none
+        def make_files(lines):
+            return list(starmap(make_file, csv.reader(lines)))
+
+        return make_files(self._read_files_distinfo() or self._read_files_egginfo())
 
     def _read_files_distinfo(self):
         """
@@ -742,6 +764,9 @@ class FastPath:
     """
     Micro-optimized class for searching a path for
     children.
+
+    >>> FastPath('').children()
+    ['...']
     """
 
     @functools.lru_cache()  # type: ignore
@@ -1011,6 +1036,18 @@ def packages_distributions() -> Mapping[str, List[str]]:
     """
     pkg_to_dist = collections.defaultdict(list)
     for dist in distributions():
-        for pkg in (dist.read_text('top_level.txt') or '').split():
+        for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
             pkg_to_dist[pkg].append(dist.metadata['Name'])
     return dict(pkg_to_dist)
+
+
+def _top_level_declared(dist):
+    return (dist.read_text('top_level.txt') or '').split()
+
+
+def _top_level_inferred(dist):
+    return {
+        f.parts[0] if len(f.parts) > 1 else f.with_suffix('').name
+        for f in always_iterable(dist.files)
+        if f.suffix == ".py"
+    }
diff --git a/Lib/importlib/metadata/_functools.py b/Lib/importlib/metadata/_functools.py
index 73f50d00bc04c..71f66bd03cb71 100644
--- a/Lib/importlib/metadata/_functools.py
+++ b/Lib/importlib/metadata/_functools.py
@@ -83,3 +83,22 @@ def wrapper(self, *args, **kwargs):
     wrapper.cache_clear = lambda: None
 
     return wrapper
+
+
+# From jaraco.functools 3.3
+def pass_none(func):
+    """
+    Wrap func so it's not called if its first param is None
+
+    >>> print_text = pass_none(print)
+    >>> print_text('text')
+    text
+    >>> print_text(None)
+    """
+
+    @functools.wraps(func)
+    def wrapper(param, *args, **kwargs):
+        if param is not None:
+            return func(param, *args, **kwargs)
+
+    return wrapper
diff --git a/Lib/importlib/metadata/_itertools.py b/Lib/importlib/metadata/_itertools.py
index dd45f2f096630..d4ca9b9140e3f 100644
--- a/Lib/importlib/metadata/_itertools.py
+++ b/Lib/importlib/metadata/_itertools.py
@@ -17,3 +17,57 @@ def unique_everseen(iterable, key=None):
             if k not in seen:
                 seen_add(k)
                 yield element
+
+
+# copied from more_itertools 8.8
+def always_iterable(obj, base_type=(str, bytes)):
+    """If *obj* is iterable, return an iterator over its items::
+
+        >>> obj = (1, 2, 3)
+        >>> list(always_iterable(obj))
+        [1, 2, 3]
+
+    If *obj* is not iterable, return a one-item iterable containing *obj*::
+
+        >>> obj = 1
+        >>> list(always_iterable(obj))
+        [1]
+
+    If *obj* is ``None``, return an empty iterable:
+
+        >>> obj = None
+        >>> list(always_iterable(None))
+        []
+
+    By default, binary and text strings are not considered iterable::
+
+        >>> obj = 'foo'
+        >>> list(always_iterable(obj))
+        ['foo']
+
+    If *base_type* is set, objects for which ``isinstance(obj, base_type)``
+    returns ``True`` won't be considered iterable.
+
+        >>> obj = {'a': 1}
+        >>> list(always_iterable(obj))  # Iterate over the dict's keys
+        ['a']
+        >>> list(always_iterable(obj, base_type=dict))  # Treat dicts as a unit
+        [{'a': 1}]
+
+    Set *base_type* to ``None`` to avoid any special handling and treat objects
+    Python considers iterable as iterable:
+
+        >>> obj = 'foo'
+        >>> list(always_iterable(obj, base_type=None))
+        ['f', 'o', 'o']
+    """
+    if obj is None:
+        return iter(())
+
+    if (base_type is not None) and isinstance(obj, base_type):
+        return iter((obj,))
+
+    try:
+        return iter(obj)
+    except TypeError:
+        return iter((obj,))
diff --git a/Lib/importlib/metadata/_meta.py b/Lib/importlib/metadata/_meta.py
index 1a6edbf957d5a..d5c0576194ece 100644
--- a/Lib/importlib/metadata/_meta.py
+++ b/Lib/importlib/metadata/_meta.py
@@ -37,7 +37,7 @@ class SimplePath(Protocol):
     def joinpath(self) -> 'SimplePath':
         ...  # pragma: no cover
 
-    def __div__(self) -> 'SimplePath':
+    def __truediv__(self) -> 'SimplePath':
         ...  # pragma: no cover
 
     def parent(self) -> 'SimplePath':
diff --git a/Lib/importlib/metadata/_text.py b/Lib/importlib/metadata/_text.py
index 766979d93c169..c88cfbb2349c6 100644
--- a/Lib/importlib/metadata/_text.py
+++ b/Lib/importlib/metadata/_text.py
@@ -80,7 +80,7 @@ def __hash__(self):
         return hash(self.lower())
 
     def __contains__(self, other):
-        return super(FoldedCase, self).lower().__contains__(other.lower())
+        return super().lower().__contains__(other.lower())
 
     def in_(self, other):
         "Does self appear in other?"
@@ -89,7 +89,7 @@ def in_(self, other):
     # cache lower since it's likely to be called frequently.
     @method_cache
     def lower(self):
-        return super(FoldedCase, self).lower()
+        return super().lower()
 
     def index(self, sub):
         return self.lower().index(sub.lower())
diff --git a/Lib/test/test_importlib/data/example2-1.0.0-py3-none-any.whl b/Lib/test/test_importlib/data/example2-1.0.0-py3-none-any.whl
new file mode 100644
index 0000000000000..5ca93657f8196
Binary files /dev/null and b/Lib/test/test_importlib/data/example2-1.0.0-py3-none-any.whl differ
diff --git a/Lib/test/test_importlib/fixtures.py b/Lib/test/test_importlib/fixtures.py
index 12ed07d33744a..d7ed4e9d56ff5 100644
--- a/Lib/test/test_importlib/fixtures.py
+++ b/Lib/test/test_importlib/fixtures.py
@@ -8,8 +8,17 @@
 import contextlib
 
 from test.support.os_helper import FS_NONASCII
+from test.support import requires_zlib
 from typing import Dict, Union
 
+try:
+    from importlib import resources
+
+    getattr(resources, 'files')
+    getattr(resources, 'as_file')
+except (ImportError, AttributeError):
+    import importlib_resources as resources  # type: ignore
+
 
 @contextlib.contextmanager
 def tempdir():
@@ -54,7 +63,7 @@ def setUp(self):
 
 class SiteDir(Fixtures):
     def setUp(self):
-        super(SiteDir, self).setUp()
+        super().setUp()
         self.site_dir = self.fixtures.enter_context(tempdir())
 
 
@@ -69,7 +78,7 @@ def add_sys_path(dir):
             sys.path.remove(str(dir))
 
     def setUp(self):
-        super(OnSysPath, self).setUp()
+        super().setUp()
         self.fixtures.enter_context(self.add_sys_path(self.site_dir))
 
 
@@ -106,7 +115,7 @@ def main():
     }
 
     def setUp(self):
-        super(DistInfoPkg, self).setUp()
+        super().setUp()
         build_files(DistInfoPkg.files, self.site_dir)
 
     def make_uppercase(self):
@@ -131,7 +140,7 @@ class DistInfoPkgWithDot(OnSysPath, SiteDir):
     }
 
     def setUp(self):
-        super(DistInfoPkgWithDot, self).setUp()
+        super().setUp()
         build_files(DistInfoPkgWithDot.files, self.site_dir)
 
 
@@ -152,13 +161,13 @@ class DistInfoPkgWithDotLegacy(OnSysPath, SiteDir):
     }
 
     def setUp(self):
-        super(DistInfoPkgWithDotLegacy, self).setUp()
+        super().setUp()
         build_files(DistInfoPkgWithDotLegacy.files, self.site_dir)
 
 
 class DistInfoPkgOffPath(SiteDir):
     def setUp(self):
-        super(DistInfoPkgOffPath, self).setUp()
+        super().setUp()
         build_files(DistInfoPkg.files, self.site_dir)
 
 
@@ -198,7 +207,7 @@ def main():
     }
 
     def setUp(self):
-        super(EggInfoPkg, self).setUp()
+        super().setUp()
         build_files(EggInfoPkg.files, prefix=self.site_dir)
 
 
@@ -219,7 +228,7 @@ class EggInfoFile(OnSysPath, SiteDir):
     }
 
     def setUp(self):
-        super(EggInfoFile, self).setUp()
+        super().setUp()
         build_files(EggInfoFile.files, prefix=self.site_dir)
 
 
@@ -285,3 +294,20 @@ def DALS(str):
 class NullFinder:
     def find_module(self, name):
         pass
+
+
+ at requires_zlib()
+class ZipFixtures:
+    root = 'test.test_importlib.data'
+
+    def _fixture_on_path(self, filename):
+        pkg_file = resources.files(self.root).joinpath(filename)
+        file = self.resources.enter_context(resources.as_file(pkg_file))
+        assert file.name.startswith('example'), file.name
+        sys.path.insert(0, str(file))
+        self.resources.callback(sys.path.pop, 0)
+
+    def setUp(self):
+        # Add self.zip_name to the front of sys.path.
+        self.resources = contextlib.ExitStack()
+        self.addCleanup(self.resources.close)
diff --git a/Lib/test/test_importlib/test_main.py b/Lib/test/test_importlib/test_main.py
index 52cb63712a5cb..2e120f7ac50ac 100644
--- a/Lib/test/test_importlib/test_main.py
+++ b/Lib/test/test_importlib/test_main.py
@@ -19,6 +19,7 @@
     distributions,
     entry_points,
     metadata,
+    packages_distributions,
     version,
 )
 
@@ -203,7 +204,7 @@ class InaccessibleSysPath(fixtures.OnSysPath, ffs.TestCase):
     site_dir = '/access-denied'
 
     def setUp(self):
-        super(InaccessibleSysPath, self).setUp()
+        super().setUp()
         self.setUpPyfakefs()
         self.fs.create_dir(self.site_dir, perm_bits=000)
 
@@ -217,13 +218,21 @@ def test_discovery(self):
 
 class TestEntryPoints(unittest.TestCase):
     def __init__(self, *args):
-        super(TestEntryPoints, self).__init__(*args)
-        self.ep = importlib.metadata.EntryPoint('name', 'value', 'group')
+        super().__init__(*args)
+        self.ep = importlib.metadata.EntryPoint(
+            name='name', value='value', group='group'
+        )
 
     def test_entry_point_pickleable(self):
         revived = pickle.loads(pickle.dumps(self.ep))
         assert revived == self.ep
 
+    def test_positional_args(self):
+        """
+        Capture legacy (namedtuple) construction, discouraged.
+        """
+        EntryPoint('name', 'value', 'group')
+
     def test_immutable(self):
         """EntryPoints should be immutable"""
         with self.assertRaises(AttributeError):
@@ -254,8 +263,8 @@ def test_sortable(self):
         # EntryPoint objects are sortable, but result is undefined.
         sorted(
             [
-                EntryPoint('b', 'val', 'group'),
-                EntryPoint('a', 'val', 'group'),
+                EntryPoint(name='b', value='val', group='group'),
+                EntryPoint(name='a', value='val', group='group'),
             ]
         )
 
@@ -271,3 +280,38 @@ def test_unicode_dir_on_sys_path(self):
             prefix=self.site_dir,
         )
         list(distributions())
+
+
+class PackagesDistributionsPrebuiltTest(fixtures.ZipFixtures, unittest.TestCase):
+    def test_packages_distributions_example(self):
+        self._fixture_on_path('example-21.12-py3-none-any.whl')
+        assert packages_distributions()['example'] == ['example']
+
+    def test_packages_distributions_example2(self):
+        """
+        Test packages_distributions on a wheel built
+        by trampolim.
+        """
+        self._fixture_on_path('example2-1.0.0-py3-none-any.whl')
+        assert packages_distributions()['example2'] == ['example2']
+
+
+class PackagesDistributionsTest(
+    fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase
+):
+    def test_packages_distributions_neither_toplevel_nor_files(self):
+        """
+        Test a package built without 'top-level.txt' or a file list.
+        """
+        fixtures.build_files(
+            {
+                'trim_example-1.0.0.dist-info': {
+                    'METADATA': """
+                Name: trim_example
+                Version: 1.0.0
+                """,
+                }
+            },
+            prefix=self.site_dir,
+        )
+        packages_distributions()
diff --git a/Lib/test/test_importlib/test_metadata_api.py b/Lib/test/test_importlib/test_metadata_api.py
index 4a45312e31cd5..e16773a7e87ef 100644
--- a/Lib/test/test_importlib/test_metadata_api.py
+++ b/Lib/test/test_importlib/test_metadata_api.py
@@ -21,7 +21,7 @@
 @contextlib.contextmanager
 def suppress_known_deprecation():
     with warnings.catch_warnings(record=True) as ctx:
-        warnings.simplefilter('default')
+        warnings.simplefilter('default', category=DeprecationWarning)
         yield ctx
 
 
@@ -113,7 +113,7 @@ def test_entry_points_unique_packages(self):
             for ep in entries
         )
         # ns:sub doesn't exist in alt_pkg
-        assert 'ns:sub' not in entries
+        assert 'ns:sub' not in entries.names
 
     def test_entry_points_missing_name(self):
         with self.assertRaises(KeyError):
@@ -194,10 +194,8 @@ def _test_files(files):
                 file.read_text()
 
     def test_file_hash_repr(self):
-        assertRegex = self.assertRegex
-
         util = [p for p in files('distinfo-pkg') if p.name == 'mod.py'][0]
-        assertRegex(repr(util.hash), '<FileHash mode: sha256 value: .*>')
+        self.assertRegex(repr(util.hash), '<FileHash mode: sha256 value: .*>')
 
     def test_files_dist_info(self):
         self._test_files(files('distinfo-pkg'))
diff --git a/Lib/test/test_importlib/test_zip.py b/Lib/test/test_importlib/test_zip.py
index bf16a3b95e18c..276f6288c9159 100644
--- a/Lib/test/test_importlib/test_zip.py
+++ b/Lib/test/test_importlib/test_zip.py
@@ -1,7 +1,7 @@
 import sys
 import unittest
 
-from contextlib import ExitStack
+from . import fixtures
 from importlib.metadata import (
     PackageNotFoundError,
     distribution,
@@ -10,27 +10,11 @@
     files,
     version,
 )
-from importlib import resources
 
-from test.support import requires_zlib
-
-
- at requires_zlib()
-class TestZip(unittest.TestCase):
-    root = 'test.test_importlib.data'
-
-    def _fixture_on_path(self, filename):
-        pkg_file = resources.files(self.root).joinpath(filename)
-        file = self.resources.enter_context(resources.as_file(pkg_file))
-        assert file.name.startswith('example-'), file.name
-        sys.path.insert(0, str(file))
-        self.resources.callback(sys.path.pop, 0)
 
+class TestZip(fixtures.ZipFixtures, unittest.TestCase):
     def setUp(self):
-        # Find the path to the example-*.whl so we can add it to the front of
-        # sys.path, where we'll then try to find the metadata thereof.
-        self.resources = ExitStack()
-        self.addCleanup(self.resources.close)
+        super().setUp()
         self._fixture_on_path('example-21.12-py3-none-any.whl')
 
     def test_zip_version(self):
@@ -63,13 +47,9 @@ def test_one_distribution(self):
         assert len(dists) == 1
 
 
- at requires_zlib()
 class TestEgg(TestZip):
     def setUp(self):
-        # Find the path to the example-*.egg so we can add it to the front of
-        # sys.path, where we'll then try to find the metadata thereof.
-        self.resources = ExitStack()
-        self.addCleanup(self.resources.close)
+        super().setUp()
         self._fixture_on_path('example-21.12-py3.6.egg')
 
     def test_files(self):
diff --git a/Misc/NEWS.d/next/Library/2021-12-16-13-54-55.bpo-44893.I7aLiW.rst b/Misc/NEWS.d/next/Library/2021-12-16-13-54-55.bpo-44893.I7aLiW.rst
new file mode 100644
index 0000000000000..e77c6ad2a483e
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2021-12-16-13-54-55.bpo-44893.I7aLiW.rst
@@ -0,0 +1,3 @@
+EntryPoint objects are no longer tuples. Recommended means to access is by
+attribute ('.name', '.group') or accessor ('.load()'). Access by index is
+deprecated and will raise deprecation warning.



More information about the Python-checkins mailing list