diff --git a/changelogs/fragments/67093-site-packages-pythonpath-collections-loader.yml b/changelogs/fragments/67093-site-packages-pythonpath-collections-loader.yml new file mode 100644 index 00000000000..d1a669a855e --- /dev/null +++ b/changelogs/fragments/67093-site-packages-pythonpath-collections-loader.yml @@ -0,0 +1,5 @@ +minor_changes: +- >- + Enable Ansible Collections loader to discover and + import collections from ``site-packages`` dir and + ``PYTHONPATH``-added locations. diff --git a/lib/ansible/utils/collection_loader.py b/lib/ansible/utils/collection_loader.py index 2a70595aa2a..bf4d7589480 100644 --- a/lib/ansible/utils/collection_loader.py +++ b/lib/ansible/utils/collection_loader.py @@ -33,17 +33,33 @@ _SYNTHETIC_PACKAGES = { class AnsibleCollectionLoader(with_metaclass(Singleton, object)): def __init__(self, config=None): if config: - self._n_configured_paths = config.get_config_value('COLLECTIONS_PATHS') + paths = config.get_config_value('COLLECTIONS_PATHS') else: - self._n_configured_paths = os.environ.get('ANSIBLE_COLLECTIONS_PATHS', '').split(os.pathsep) + paths = os.environ.get('ANSIBLE_COLLECTIONS_PATHS', '').split(os.pathsep) - if isinstance(self._n_configured_paths, string_types): - self._n_configured_paths = [self._n_configured_paths] - elif self._n_configured_paths is None: - self._n_configured_paths = [] + if isinstance(paths, string_types): + paths = [paths] + elif paths is None: + paths = [] # expand any placeholders in configured paths - self._n_configured_paths = [to_native(os.path.expanduser(p), errors='surrogate_or_strict') for p in self._n_configured_paths] + paths = [ + to_native(os.path.expanduser(p), errors='surrogate_or_strict') + for p in paths + ] + + # Append all ``ansible_collections`` dirs from sys.path to the end + for path in sys.path: + if ( + path not in paths and + os.path.isdir(to_bytes( + os.path.join(path, 'ansible_collections'), + errors='surrogate_or_strict', + )) + ): + paths.append(path) + + self._n_configured_paths = paths self._n_playbook_paths = [] self._default_collection = None diff --git a/test/integration/targets/collections_runtime_pythonpath/aliases b/test/integration/targets/collections_runtime_pythonpath/aliases new file mode 100644 index 00000000000..0a772ad706c --- /dev/null +++ b/test/integration/targets/collections_runtime_pythonpath/aliases @@ -0,0 +1,3 @@ +shippable/posix/group4 +skip/python2.6 +skip/aix diff --git a/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/ansible_collections/python/dist/plugins/modules/boo.py b/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/ansible_collections/python/dist/plugins/modules/boo.py new file mode 100644 index 00000000000..a2313b127d2 --- /dev/null +++ b/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/ansible_collections/python/dist/plugins/modules/boo.py @@ -0,0 +1,28 @@ +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +"""Say hello.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + module = AnsibleModule( + argument_spec={ + 'name': {'default': 'world'}, + }, + ) + name = module.params['name'] + + module.exit_json( + msg='Greeting {name} completed.'. + format(name=name.title()), + greeting='Hello, {name}!'.format(name=name), + ) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/pyproject.toml b/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/pyproject.toml new file mode 100644 index 00000000000..feec734a6b0 --- /dev/null +++ b/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools >= 44", + "wheel", +] +build-backend = "setuptools.build_meta" diff --git a/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/setup.cfg b/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/setup.cfg new file mode 100644 index 00000000000..d25ebb0fc37 --- /dev/null +++ b/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/setup.cfg @@ -0,0 +1,15 @@ +[metadata] +name = ansible-collections.python.dist +version = 1.0.0rc2.post3.dev4 + +[options] +package_dir = + = . +packages = + ansible_collections + ansible_collections.python + ansible_collections.python.dist + ansible_collections.python.dist.plugins + ansible_collections.python.dist.plugins.modules +zip_safe = True +include_package_data = True diff --git a/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-foo/ansible_collections/python/dist/plugins/modules/boo.py b/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-foo/ansible_collections/python/dist/plugins/modules/boo.py new file mode 100644 index 00000000000..1ef033303b0 --- /dev/null +++ b/test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-foo/ansible_collections/python/dist/plugins/modules/boo.py @@ -0,0 +1,28 @@ +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +"""Say hello in Ukrainian.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +from ansible.module_utils.basic import AnsibleModule + + +def main(): + module = AnsibleModule( + argument_spec={ + 'name': {'default': 'світ'}, + }, + ) + name = module.params['name'] + + module.exit_json( + msg='Greeting {name} completed.'. + format(name=name.title()), + greeting='Привіт, {name}!'.format(name=name), + ) + + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/collections_runtime_pythonpath/runme.sh b/test/integration/targets/collections_runtime_pythonpath/runme.sh new file mode 100755 index 00000000000..e1f2574815b --- /dev/null +++ b/test/integration/targets/collections_runtime_pythonpath/runme.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +set -eux -o pipefail + + +export PIP_DISABLE_PIP_VERSION_CHECK=1 + + +source virtualenv.sh + + +>&2 echo \ + === Test that the module \ + gets picked up if discoverable \ + via PYTHONPATH env var === +PYTHONPATH="${PWD}/ansible-collection-python-dist-boo:$PYTHONPATH" \ +ansible \ + -m python.dist.boo \ + -a 'name=Bob' \ + -c local localhost \ + "$@" | grep -E '"greeting": "Hello, Bob!",' + + +>&2 echo \ + === Test that the module \ + gets picked up if installed \ + into site-packages === +python -m pip.__main__ install pep517 +( # Build a binary Python dist (a wheel) using PEP517: + cp -r ansible-collection-python-dist-boo "${OUTPUT_DIR}/" + cd "${OUTPUT_DIR}/ansible-collection-python-dist-boo" + python -m pep517.build --binary --out-dir dist . +) +# Install a pre-built dist with pip: +python -m pip.__main__ install \ + --no-index \ + -f "${OUTPUT_DIR}/ansible-collection-python-dist-boo/dist/" \ + --only-binary=ansible-collections.python.dist \ + ansible-collections.python.dist +python -m pip.__main__ show ansible-collections.python.dist +ansible \ + -m python.dist.boo \ + -a 'name=Frodo' \ + -c local localhost \ + "$@" | grep -E '"greeting": "Hello, Frodo!",' + + +>&2 echo \ + === Test that ansible_collections \ + root takes precedence over \ + PYTHONPATH/site-packages === +# This is done by injecting a module with the same FQCN +# into another collection root. +ANSIBLE_COLLECTIONS_PATHS="${PWD}/ansible-collection-python-dist-foo" \ +PYTHONPATH="${PWD}/ansible-collection-python-dist-boo:$PYTHONPATH" \ +ansible \ + -m python.dist.boo \ + -a 'name=Степан' \ + -c local localhost \ + "$@" | grep -E '"greeting": "Привіт, Степан!",'