|
|
|
@ -9,17 +9,14 @@ from __future__ import annotations
|
|
|
|
|
import itertools
|
|
|
|
|
import os
|
|
|
|
|
import os.path
|
|
|
|
|
import pkgutil
|
|
|
|
|
import re
|
|
|
|
|
import sys
|
|
|
|
|
from keyword import iskeyword
|
|
|
|
|
from tokenize import Name as _VALID_IDENTIFIER_REGEX
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# DO NOT add new non-stdlib import deps here, this loader is used by external tools (eg ansible-test import sanity)
|
|
|
|
|
# that only allow stdlib and module_utils
|
|
|
|
|
from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
|
|
|
|
|
from ansible.module_utils.six import string_types, PY3
|
|
|
|
|
from ._collection_config import AnsibleCollectionConfig
|
|
|
|
|
|
|
|
|
|
from contextlib import contextmanager
|
|
|
|
@ -33,7 +30,22 @@ except ImportError:
|
|
|
|
|
return sys.modules[name]
|
|
|
|
|
|
|
|
|
|
from importlib import reload as reload_module
|
|
|
|
|
from importlib.resources.abc import TraversableResources
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
try:
|
|
|
|
|
# Available on Python >= 3.11
|
|
|
|
|
# We ignore the import error that will trigger when running mypy with
|
|
|
|
|
# older Python versions.
|
|
|
|
|
from importlib.resources.abc import TraversableResources # type: ignore[import]
|
|
|
|
|
except ImportError:
|
|
|
|
|
# Used with Python 3.9 and 3.10 only
|
|
|
|
|
# This member is still available as an alias up until Python 3.14 but
|
|
|
|
|
# is deprecated as of Python 3.12.
|
|
|
|
|
from importlib.abc import TraversableResources # deprecated: description='TraversableResources move' python_version='3.10'
|
|
|
|
|
except ImportError:
|
|
|
|
|
# Python < 3.9
|
|
|
|
|
# deprecated: description='TraversableResources fallback' python_version='3.8'
|
|
|
|
|
TraversableResources = object # type: ignore[assignment,misc]
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from importlib.util import find_spec, spec_from_loader
|
|
|
|
@ -181,7 +193,7 @@ class _AnsibleTraversableResources(TraversableResources):
|
|
|
|
|
parts = package.split('.')
|
|
|
|
|
is_ns = parts[0] == 'ansible_collections' and len(parts) < 3
|
|
|
|
|
|
|
|
|
|
if isinstance(package, string_types):
|
|
|
|
|
if isinstance(package, str):
|
|
|
|
|
if is_ns:
|
|
|
|
|
# Don't use ``spec_from_loader`` here, because that will point
|
|
|
|
|
# to exactly 1 location for a namespace. Use ``find_spec``
|
|
|
|
@ -203,7 +215,7 @@ class _AnsibleCollectionFinder:
|
|
|
|
|
# TODO: accept metadata loader override
|
|
|
|
|
self._ansible_pkg_path = to_native(os.path.dirname(to_bytes(sys.modules['ansible'].__file__)))
|
|
|
|
|
|
|
|
|
|
if isinstance(paths, string_types):
|
|
|
|
|
if isinstance(paths, str):
|
|
|
|
|
paths = [paths]
|
|
|
|
|
elif paths is None:
|
|
|
|
|
paths = []
|
|
|
|
@ -288,7 +300,7 @@ class _AnsibleCollectionFinder:
|
|
|
|
|
return paths
|
|
|
|
|
|
|
|
|
|
def set_playbook_paths(self, playbook_paths):
|
|
|
|
|
if isinstance(playbook_paths, string_types):
|
|
|
|
|
if isinstance(playbook_paths, str):
|
|
|
|
|
playbook_paths = [playbook_paths]
|
|
|
|
|
|
|
|
|
|
# track visited paths; we have to preserve the dir order as-passed in case there are duplicate collections (first one wins)
|
|
|
|
@ -374,19 +386,17 @@ class _AnsiblePathHookFinder:
|
|
|
|
|
# when called from a path_hook, find_module doesn't usually get the path arg, so this provides our context
|
|
|
|
|
self._pathctx = to_native(pathctx)
|
|
|
|
|
self._collection_finder = collection_finder
|
|
|
|
|
if PY3:
|
|
|
|
|
# cache the native FileFinder (take advantage of its filesystem cache for future find/load requests)
|
|
|
|
|
self._file_finder = None
|
|
|
|
|
# cache the native FileFinder (take advantage of its filesystem cache for future find/load requests)
|
|
|
|
|
self._file_finder = None
|
|
|
|
|
|
|
|
|
|
# class init is fun- this method has a self arg that won't get used
|
|
|
|
|
def _get_filefinder_path_hook(self=None):
|
|
|
|
|
_file_finder_hook = None
|
|
|
|
|
if PY3:
|
|
|
|
|
# try to find the FileFinder hook to call for fallback path-based imports in Py3
|
|
|
|
|
_file_finder_hook = [ph for ph in sys.path_hooks if 'FileFinder' in repr(ph)]
|
|
|
|
|
if len(_file_finder_hook) != 1:
|
|
|
|
|
raise Exception('need exactly one FileFinder import hook (found {0})'.format(len(_file_finder_hook)))
|
|
|
|
|
_file_finder_hook = _file_finder_hook[0]
|
|
|
|
|
# try to find the FileFinder hook to call for fallback path-based imports in Py3
|
|
|
|
|
_file_finder_hook = [ph for ph in sys.path_hooks if 'FileFinder' in repr(ph)]
|
|
|
|
|
if len(_file_finder_hook) != 1:
|
|
|
|
|
raise Exception('need exactly one FileFinder import hook (found {0})'.format(len(_file_finder_hook)))
|
|
|
|
|
_file_finder_hook = _file_finder_hook[0]
|
|
|
|
|
|
|
|
|
|
return _file_finder_hook
|
|
|
|
|
|
|
|
|
@ -407,20 +417,16 @@ class _AnsiblePathHookFinder:
|
|
|
|
|
# out what we *shouldn't* be loading with the limited info it has. So we'll just delegate to the
|
|
|
|
|
# normal path-based loader as best we can to service it. This also allows us to take advantage of Python's
|
|
|
|
|
# built-in FS caching and byte-compilation for most things.
|
|
|
|
|
if PY3:
|
|
|
|
|
# create or consult our cached file finder for this path
|
|
|
|
|
if not self._file_finder:
|
|
|
|
|
try:
|
|
|
|
|
self._file_finder = _AnsiblePathHookFinder._filefinder_path_hook(self._pathctx)
|
|
|
|
|
except ImportError:
|
|
|
|
|
# FUTURE: log at a high logging level? This is normal for things like python36.zip on the path, but
|
|
|
|
|
# might not be in some other situation...
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
return self._file_finder
|
|
|
|
|
# create or consult our cached file finder for this path
|
|
|
|
|
if not self._file_finder:
|
|
|
|
|
try:
|
|
|
|
|
self._file_finder = _AnsiblePathHookFinder._filefinder_path_hook(self._pathctx)
|
|
|
|
|
except ImportError:
|
|
|
|
|
# FUTURE: log at a high logging level? This is normal for things like python36.zip on the path, but
|
|
|
|
|
# might not be in some other situation...
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
# call py2's internal loader
|
|
|
|
|
return pkgutil.ImpImporter(self._pathctx)
|
|
|
|
|
return self._file_finder
|
|
|
|
|
|
|
|
|
|
def find_module(self, fullname, path=None):
|
|
|
|
|
# we ignore the passed in path here- use what we got from the path hook init
|
|
|
|
@ -1086,7 +1092,7 @@ class AnsibleCollectionRef:
|
|
|
|
|
|
|
|
|
|
def _get_collection_path(collection_name):
|
|
|
|
|
collection_name = to_native(collection_name)
|
|
|
|
|
if not collection_name or not isinstance(collection_name, string_types) or len(collection_name.split('.')) != 2:
|
|
|
|
|
if not collection_name or not isinstance(collection_name, str) or len(collection_name.split('.')) != 2:
|
|
|
|
|
raise ValueError('collection_name must be a non-empty string of the form namespace.collection')
|
|
|
|
|
try:
|
|
|
|
|
collection_pkg = import_module('ansible_collections.' + collection_name)
|
|
|
|
@ -1269,7 +1275,7 @@ def _iter_modules_impl(paths, prefix=''):
|
|
|
|
|
|
|
|
|
|
def _get_collection_metadata(collection_name):
|
|
|
|
|
collection_name = to_native(collection_name)
|
|
|
|
|
if not collection_name or not isinstance(collection_name, string_types) or len(collection_name.split('.')) != 2:
|
|
|
|
|
if not collection_name or not isinstance(collection_name, str) or len(collection_name.split('.')) != 2:
|
|
|
|
|
raise ValueError('collection_name must be a non-empty string of the form namespace.collection')
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|