summaryrefslogtreecommitdiff
path: root/lib/ansible/utils
diff options
context:
space:
mode:
authorSloane Hertel <19572925+s-hertel@users.noreply.github.com>2021-12-01 12:47:10 -0500
committerGitHub <noreply@github.com>2021-12-01 09:47:10 -0800
commitb50f16db9159335f9ad97a4c33d28265e3003858 (patch)
treedfa9b9c0a1ddaf07d334cd72e78b0b040477992d /lib/ansible/utils
parent1d1597ffd9d238c223f3eb8c139e15e079b5ed08 (diff)
downloadansible-b50f16db9159335f9ad97a4c33d28265e3003858.tar.gz
Update collection loader for Python 3.10 (#76225)
* Implement find_spec and exec_module to remove reliance on deprecated methods in the collection loader ci_complete * Move module execution to exec_module Remove extra sys.modules handling Use default module initialization by returning None from loader.create_module Refactor ci_complete * Remove ansible-test's copy of the collection loader ci_complete * Fix metaclass for Python 2.x ci_complete * Fix Py2/Py3 syntax compatibility * Refactor ci_complete * update collection_loader comments ci_complete * simplify find_module ci_complete * Fix Py2 compatibility - don't get loader from nonexistent spec Remove unnecessary PY3 checking * Refactor common code in load_module and exec_module ci_complete * tidy diff ci_complete * Include collection_loader in target paths for 'compile' sanity test * add changelog * Add "return None" instead of doing it implicitly Remove get_filename short-circuit exec_module if it's a redirect ci_complete
Diffstat (limited to 'lib/ansible/utils')
-rw-r--r--lib/ansible/utils/collection_loader/__init__.py9
-rw-r--r--lib/ansible/utils/collection_loader/_collection_config.py13
-rw-r--r--lib/ansible/utils/collection_loader/_collection_finder.py135
-rw-r--r--lib/ansible/utils/collection_loader/_collection_meta.py9
4 files changed, 111 insertions, 55 deletions
diff --git a/lib/ansible/utils/collection_loader/__init__.py b/lib/ansible/utils/collection_loader/__init__.py
index 21c49c4730..83cc2462eb 100644
--- a/lib/ansible/utils/collection_loader/__init__.py
+++ b/lib/ansible/utils/collection_loader/__init__.py
@@ -1,13 +1,8 @@
# (c) 2019 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-# CAUTION: There are two implementations of the collection loader.
-# They must be kept functionally identical, although their implementations may differ.
-#
-# 1) The controller implementation resides in the "lib/ansible/utils/collection_loader/" directory.
-# It must function on all Python versions supported on the controller.
-# 2) The ansible-test implementation resides in the "test/lib/ansible_test/_util/target/legacy_collection_loader/" directory.
-# It must function on all Python versions supported on managed hosts which are not supported by the controller.
+# CAUTION: This implementation of the collection loader is used by ansible-test.
+# Because of this, it must be compatible with all Python versions supported on the controller or remote.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
diff --git a/lib/ansible/utils/collection_loader/_collection_config.py b/lib/ansible/utils/collection_loader/_collection_config.py
index 49de68b135..4f73a1a731 100644
--- a/lib/ansible/utils/collection_loader/_collection_config.py
+++ b/lib/ansible/utils/collection_loader/_collection_config.py
@@ -1,18 +1,14 @@
# (c) 2019 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-# CAUTION: There are two implementations of the collection loader.
-# They must be kept functionally identical, although their implementations may differ.
-#
-# 1) The controller implementation resides in the "lib/ansible/utils/collection_loader/" directory.
-# It must function on all Python versions supported on the controller.
-# 2) The ansible-test implementation resides in the "test/lib/ansible_test/_util/target/legacy_collection_loader/" directory.
-# It must function on all Python versions supported on managed hosts which are not supported by the controller.
+# CAUTION: This implementation of the collection loader is used by ansible-test.
+# Because of this, it must be compatible with all Python versions supported on the controller or remote.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from ansible.module_utils.common.text.converters import to_text
+from ansible.module_utils.six import add_metaclass
class _EventSource:
@@ -102,5 +98,6 @@ class _AnsibleCollectionConfig(type):
# concrete class of our metaclass type that defines the class properties we want
-class AnsibleCollectionConfig(metaclass=_AnsibleCollectionConfig):
+@add_metaclass(_AnsibleCollectionConfig)
+class AnsibleCollectionConfig(object):
pass
diff --git a/lib/ansible/utils/collection_loader/_collection_finder.py b/lib/ansible/utils/collection_loader/_collection_finder.py
index 13fab8922d..70e2d1038e 100644
--- a/lib/ansible/utils/collection_loader/_collection_finder.py
+++ b/lib/ansible/utils/collection_loader/_collection_finder.py
@@ -1,13 +1,8 @@
# (c) 2019 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-# CAUTION: There are two implementations of the collection loader.
-# They must be kept functionally identical, although their implementations may differ.
-#
-# 1) The controller implementation resides in the "lib/ansible/utils/collection_loader/" directory.
-# It must function on all Python versions supported on the controller.
-# 2) The ansible-test implementation resides in the "test/lib/ansible_test/_util/target/legacy_collection_loader/" directory.
-# It must function on all Python versions supported on managed hosts which are not supported by the controller.
+# CAUTION: This implementation of the collection loader is used by ansible-test.
+# Because of this, it must be compatible with all Python versions supported on the controller or remote.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
@@ -43,6 +38,11 @@ except ImportError:
# 2.7 has a global reload function instead...
reload_module = reload # pylint:disable=undefined-variable
+try:
+ from importlib.util import spec_from_loader
+except ImportError:
+ pass
+
# NB: this supports import sanity test providing a different impl
try:
from ._collection_meta import _meta_yml_to_dict
@@ -184,9 +184,7 @@ class _AnsibleCollectionFinder:
return
reload_module(m)
- def find_module(self, fullname, path=None):
- # Figure out what's being asked for, and delegate to a special-purpose loader
-
+ def _get_loader(self, fullname, path=None):
split_name = fullname.split('.')
toplevel_pkg = split_name[0]
module_to_find = split_name[-1]
@@ -207,23 +205,40 @@ class _AnsibleCollectionFinder:
if part_count > 1 and path is None:
raise ValueError('path must be specified for subpackages (trying to find {0})'.format(fullname))
+ if toplevel_pkg == 'ansible':
+ # something under the ansible package, delegate to our internal loader in case of redirections
+ initialize_loader = _AnsibleInternalRedirectLoader
+ elif part_count == 1:
+ initialize_loader = _AnsibleCollectionRootPkgLoader
+ elif part_count == 2: # ns pkg eg, ansible_collections, ansible_collections.somens
+ initialize_loader = _AnsibleCollectionNSPkgLoader
+ elif part_count == 3: # collection pkg eg, ansible_collections.somens.somecoll
+ initialize_loader = _AnsibleCollectionPkgLoader
+ else:
+ # anything below the collection
+ initialize_loader = _AnsibleCollectionLoader
+
# NB: actual "find"ing is delegated to the constructors on the various loaders; they'll ImportError if not found
try:
- if toplevel_pkg == 'ansible':
- # something under the ansible package, delegate to our internal loader in case of redirections
- return _AnsibleInternalRedirectLoader(fullname=fullname, path_list=path)
- if part_count == 1:
- return _AnsibleCollectionRootPkgLoader(fullname=fullname, path_list=path)
- if part_count == 2: # ns pkg eg, ansible_collections, ansible_collections.somens
- return _AnsibleCollectionNSPkgLoader(fullname=fullname, path_list=path)
- elif part_count == 3: # collection pkg eg, ansible_collections.somens.somecoll
- return _AnsibleCollectionPkgLoader(fullname=fullname, path_list=path)
- # anything below the collection
- return _AnsibleCollectionLoader(fullname=fullname, path_list=path)
+ return initialize_loader(fullname=fullname, path_list=path)
except ImportError:
# TODO: log attempt to load context
return None
+ def find_module(self, fullname, path=None):
+ # Figure out what's being asked for, and delegate to a special-purpose loader
+ return self._get_loader(fullname, path)
+
+ def find_spec(self, fullname, path, target=None):
+ loader = self._get_loader(fullname, path)
+ if loader:
+ spec = spec_from_loader(fullname, loader)
+ if spec is not None and hasattr(loader, '_subpackage_search_paths'):
+ spec.submodule_search_locations = loader._subpackage_search_paths
+ return spec
+ else:
+ return None
+
# Implements a path_hook finder for iter_modules (since it's only path based). This finder does not need to actually
# function as a finder in most cases, since our meta_path finder is consulted first for *almost* everything, except
@@ -251,14 +266,13 @@ class _AnsiblePathHookFinder:
_filefinder_path_hook = _get_filefinder_path_hook()
- def find_module(self, fullname, path=None):
- # we ignore the passed in path here- use what we got from the path hook init
+ def _get_finder(self, fullname):
split_name = fullname.split('.')
toplevel_pkg = split_name[0]
if toplevel_pkg == 'ansible_collections':
# collections content? delegate to the collection finder
- return self._collection_finder.find_module(fullname, path=[self._pathctx])
+ return self._collection_finder
else:
# Something else; we'd normally restrict this to `ansible` descendent modules so that any weird loader
# behavior that arbitrary Python modules have can be serviced by those loaders. In some dev/test
@@ -277,13 +291,31 @@ class _AnsiblePathHookFinder:
# might not be in some other situation...
return None
- spec = self._file_finder.find_spec(fullname)
- if not spec:
- return None
- return spec.loader
+ return self._file_finder
+
+ # call py2's internal loader
+ return pkgutil.ImpImporter(self._pathctx)
+
+ def find_module(self, fullname, path=None):
+ # we ignore the passed in path here- use what we got from the path hook init
+ finder = self._get_finder(fullname)
+ if finder is not None:
+ return finder.find_module(fullname, path=[self._pathctx])
+ else:
+ return None
+
+ def find_spec(self, fullname, target=None):
+ split_name = fullname.split('.')
+ toplevel_pkg = split_name[0]
+
+ finder = self._get_finder(fullname)
+ if finder is not None:
+ if toplevel_pkg == 'ansible_collections':
+ return finder.find_spec(fullname, path=[self._pathctx])
else:
- # call py2's internal loader
- return pkgutil.ImpImporter(self._pathctx).find_module(fullname)
+ return finder.find_spec(fullname)
+ else:
+ return None
def iter_modules(self, prefix):
# NB: this currently represents only what's on disk, and does not handle package redirection
@@ -377,6 +409,23 @@ class _AnsibleCollectionPkgLoaderBase:
return module_path, has_code, package_path
+ def exec_module(self, module):
+ # short-circuit redirect; avoid reinitializing existing modules
+ if self._redirect_module:
+ return
+
+ # execute the module's code in its namespace
+ code_obj = self.get_code(self._fullname)
+ if code_obj is not None: # things like NS packages that can't have code on disk will return None
+ exec(code_obj, module.__dict__)
+
+ def create_module(self, spec):
+ # short-circuit redirect; we've already imported the redirected module, so just alias it and return it
+ if self._redirect_module:
+ return self._redirect_module
+ else:
+ return None
+
def load_module(self, fullname):
# short-circuit redirect; we've already imported the redirected module, so just alias it and return it
if self._redirect_module:
@@ -531,12 +580,10 @@ class _AnsibleCollectionPkgLoader(_AnsibleCollectionPkgLoaderBase):
# only search within the first collection we found
self._subpackage_search_paths = [self._subpackage_search_paths[0]]
- def load_module(self, fullname):
+ def _load_module(self, module):
if not _meta_yml_to_dict:
raise ValueError('ansible.utils.collection_loader._meta_yml_to_dict is not set')
- module = super(_AnsibleCollectionPkgLoader, self).load_module(fullname)
-
module._collection_meta = {}
# TODO: load collection metadata, cache in __loader__ state
@@ -566,6 +613,17 @@ class _AnsibleCollectionPkgLoader(_AnsibleCollectionPkgLoaderBase):
return module
+ def exec_module(self, module):
+ super(_AnsibleCollectionPkgLoader, self).exec_module(module)
+ self._load_module(module)
+
+ def create_module(self, spec):
+ return None
+
+ def load_module(self, fullname):
+ module = super(_AnsibleCollectionPkgLoader, self).load_module(fullname)
+ return self._load_module(module)
+
def _canonicalize_meta(self, meta_dict):
# TODO: rewrite import keys and all redirect targets that start with .. (current namespace) and . (current collection)
# OR we could do it all on the fly?
@@ -676,6 +734,17 @@ class _AnsibleInternalRedirectLoader:
if not self._redirect:
raise ImportError('not redirected, go ask path_hook')
+ def exec_module(self, module):
+ # should never see this
+ if not self._redirect:
+ raise ValueError('no redirect found for {0}'.format(module.__spec__.name))
+
+ # Replace the module with the redirect
+ sys.modules[module.__spec__.name] = import_module(self._redirect)
+
+ def create_module(self, spec):
+ return None
+
def load_module(self, fullname):
# since we're delegating to other loaders, this should only be called for internal redirects where we answered
# find_module with this loader, in which case we'll just directly import the redirection target, insert it into
diff --git a/lib/ansible/utils/collection_loader/_collection_meta.py b/lib/ansible/utils/collection_loader/_collection_meta.py
index 6baa9dca05..9dcb702cf7 100644
--- a/lib/ansible/utils/collection_loader/_collection_meta.py
+++ b/lib/ansible/utils/collection_loader/_collection_meta.py
@@ -1,13 +1,8 @@
# (c) 2019 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
-# CAUTION: There are two implementations of the collection loader.
-# They must be kept functionally identical, although their implementations may differ.
-#
-# 1) The controller implementation resides in the "lib/ansible/utils/collection_loader/" directory.
-# It must function on all Python versions supported on the controller.
-# 2) The ansible-test implementation resides in the "test/lib/ansible_test/_util/target/legacy_collection_loader/" directory.
-# It must function on all Python versions supported on managed hosts which are not supported by the controller.
+# CAUTION: This implementation of the collection loader is used by ansible-test.
+# Because of this, it must be compatible with all Python versions supported on the controller or remote.
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type