Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

transparent downstream vendoring #69850

Merged
merged 4 commits into from Jun 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelogs/fragments/downstream_vendoring.yml
@@ -0,0 +1,2 @@
minor_changes:
- downstream packagers may install packages under ansible._vendor, which will be added to head of sys.path at ansible package load
3 changes: 3 additions & 0 deletions lib/ansible/__init__.py
Expand Up @@ -19,6 +19,9 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

# make vendored top-level modules accessible EARLY
import ansible._vendor

# Note: Do not add any code to this file. The ansible module may be
# a namespace package when using Ansible-2.1+ Anything in this file may not be
# available if one of the other packages in the namespace is loaded first.
Expand Down
46 changes: 46 additions & 0 deletions lib/ansible/_vendor/__init__.py
@@ -0,0 +1,46 @@
# (c) 2020 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import os
import pkgutil
import sys
import warnings

# This package exists to host vendored top-level Python packages for downstream packaging. Any Python packages
# installed beneath this one will be masked from the Ansible loader, and available from the front of sys.path.
# It is expected that the vendored packages will be loaded very early, so a warning will be fired on import of
# the top-level ansible package if any packages beneath this are already loaded at that point.
#
# Python packages may be installed here during downstream packaging using something like:
# pip install --upgrade -t (path to this dir) cryptography pyyaml packaging jinja2

# mask vendored content below this package from being accessed as an ansible subpackage
__path__ = []


def _ensure_vendored_path_entry():
"""
Ensure that any downstream-bundled content beneath this package is available at the top of sys.path
"""
# patch our vendored dir onto sys.path
vendored_path_entry = os.path.dirname(__file__)
vendored_module_names = set(m[1] for m in pkgutil.iter_modules([vendored_path_entry], '')) # m[1] == m.name

if vendored_module_names:
# patch us early to load vendored deps transparently
if vendored_path_entry in sys.path:
# handle reload case by removing the existing entry, wherever it might be
sys.path.remove(vendored_path_entry)
sys.path.insert(0, vendored_path_entry)

already_loaded_vendored_modules = set(sys.modules.keys()).intersection(vendored_module_names)

if already_loaded_vendored_modules:
warnings.warn('One or more Python packages bundled by this ansible-base distribution were already '
'loaded ({0}). This may result in undefined behavior.'.format(', '.join(sorted(already_loaded_vendored_modules))))


_ensure_vendored_path_entry()
65 changes: 65 additions & 0 deletions test/units/_vendor/test_vendor.py
@@ -0,0 +1,65 @@
# (c) 2020 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import os
import pkgutil
import pytest
import sys

from units.compat.mock import MagicMock, NonCallableMagicMock, patch


def reset_internal_vendor_package():
import ansible
ansible_vendor_path = os.path.join(os.path.dirname(ansible.__file__), '_vendor')

if ansible_vendor_path in sys.path:
sys.path.remove(ansible_vendor_path)

for pkg in ['ansible._vendor', 'ansible']:
if pkg in sys.modules:
del sys.modules[pkg]


def test_package_path_masking():
from ansible import _vendor

assert hasattr(_vendor, '__path__') and _vendor.__path__ == []


def test_no_vendored():
reset_internal_vendor_package()
with patch.object(pkgutil, 'iter_modules', return_value=[]):
previous_path = list(sys.path)
import ansible
ansible_vendor_path = os.path.join(os.path.dirname(ansible.__file__), '_vendor')

assert ansible_vendor_path not in sys.path
assert sys.path == previous_path


def test_vendored(vendored_pkg_names=None):
if not vendored_pkg_names:
vendored_pkg_names = ['boguspkg']
reset_internal_vendor_package()
with patch.object(pkgutil, 'iter_modules', return_value=list((None, p, None) for p in vendored_pkg_names)):
previous_path = list(sys.path)
import ansible
ansible_vendor_path = os.path.join(os.path.dirname(ansible.__file__), '_vendor')
assert sys.path[0] == ansible_vendor_path

if ansible_vendor_path in previous_path:
previous_path.remove(ansible_vendor_path)

assert sys.path[1:] == previous_path


def test_vendored_conflict():
with pytest.warns(UserWarning) as w:
import pkgutil
import sys
test_vendored(vendored_pkg_names=['sys', 'pkgutil']) # pass a real package we know is already loaded
assert 'pkgutil, sys' in str(w[0].message) # ensure both conflicting modules are listed and sorted