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

Replace pkg_resources #7943

Merged
merged 18 commits into from Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
98955cf
compat: resolve importlib.metadata vs importlib_metadata
rokm Sep 10, 2023
4e1b33e
compat: generalize setup.py hack for missing run-time dependencies
rokm Sep 11, 2023
bc4d9c5
building: use importlib.metadata for discovery of hook directories
rokm Aug 26, 2023
cae4fc3
tests: add tests for run-time use of metadata collected from .egg
rokm Sep 5, 2023
19d8829
bootstrap: add .egg directories in sys._MEIPASS to sys.path
rokm Sep 5, 2023
de74438
hookutils: port metadata collection to importlib.metadata
rokm Sep 6, 2023
c423706
hookutils: port get_installer to importlib.metadata
rokm Sep 6, 2023
6ff661a
analysis: use importlib.metadata in error handling in _metadata_from
rokm Sep 6, 2023
7889dbe
hookutils: port collect_entry_point to importlib.metadata
rokm Sep 9, 2023
7325e7a
hookutils: port requirements_for_package() to importlib.metadata
rokm Sep 12, 2023
fcb521d
modulegraph: remove the modulegraph.replacePackage function
rokm Sep 16, 2023
43e9639
modulegraph: remove parsing of setuptools' *-nspkg.pth files
rokm Sep 16, 2023
88b9769
modulegraph: rework support for legacy setuptools namespace packages
rokm Sep 16, 2023
44fb411
utils: run_tests: use importlib.metadata to discover tests
rokm Sep 17, 2023
35d303a
hookutils: implement replacement for is_module_satisfies
rokm Sep 17, 2023
c7fe105
hookutils: have collect_all look up the dist from package name
rokm Sep 12, 2023
701fdb3
hookutils: adjust behavior of collect_data_files with include_py_files
rokm Sep 12, 2023
86ae9b2
hookutils: remove requirements_for_package
rokm Sep 21, 2023
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
24 changes: 10 additions & 14 deletions PyInstaller/building/build_main.py
Expand Up @@ -85,20 +85,22 @@
@isolated.decorate
def discover_hook_directories():
"""
Discover hook directories via pkg_resources and pyinstaller40 entry points. Perform the discovery in a subprocess
Discover hook directories via pyinstaller40 entry points. Perform the discovery in an isolated subprocess
to avoid importing the package(s) in the main process.

:return: list of discovered hook directories.
"""

import sys # noqa: F401
from traceback import format_exception_only
import pkg_resources
from PyInstaller.log import logger
from PyInstaller.compat import importlib_metadata

# The “selectable” entry points (via group and name keyword args) were introduced in importlib_metadata 4.6 and
# Python 3.10. The compat module ensures we are using a compatible version.
entry_points = importlib_metadata.entry_points(group='pyinstaller40', name='hook-dirs')

entry_points = pkg_resources.iter_entry_points('pyinstaller40', 'hook-dirs')
# Ensure that pyinstaller_hooks_contrib comes last so that hooks from packages providing their own take priority.
entry_points = sorted(entry_points, key=lambda x: x.module_name == "_pyinstaller_hooks_contrib.hooks")
entry_points = sorted(entry_points, key=lambda x: x.module == "_pyinstaller_hooks_contrib.hooks")

hook_directories = []
for entry_point in entry_points:
Expand Down Expand Up @@ -405,14 +407,6 @@ def __init__(
logger.info('Extending PYTHONPATH with paths\n' + pprint.pformat(self.pathex))
sys.path.extend(self.pathex)

# If pkg_resources has already been imported, force update of its working set to account for changes made to
# sys.path. Otherwise, distribution data in the added path(s) may not be discovered.
if 'pkg_resources' in sys.modules:
# See https://github.com/pypa/setuptools/issues/373
import pkg_resources
if hasattr(pkg_resources, '_initialize_master_working_set'):
pkg_resources._initialize_master_working_set()

self.hiddenimports = hiddenimports or []
# Include hidden imports passed via CONF['hiddenimports']; these might be populated if user has a wrapper script
# that calls `build_main.main()` with custom `pyi_config` dictionary that contains `hiddenimports`.
Expand Down Expand Up @@ -569,7 +563,9 @@ def assemble(self):
# Expand sys.path of module graph. The attribute is the set of paths to use for imports: sys.path, plus our
# loader, plus other paths from e.g. --path option).
self.graph.path = self.pathex + self.graph.path
self.graph.set_setuptools_nspackages()

# Scan for legacy namespace packages.
self.graph.scan_legacy_namespace_packages()

logger.info("Running Analysis %s", self.tocbasename)

Expand Down
59 changes: 43 additions & 16 deletions PyInstaller/compat.py
Expand Up @@ -27,6 +27,32 @@
from PyInstaller._shared_with_waf import _pyi_machine
from PyInstaller.exceptions import ExecCommandFailed

# setup.py sets this environment variable to avoid errors due to unmet run-time dependencies. The PyInstaller.compat
# module is imported by setup.py to build wheels, and some dependencies that are otherwise required at run-time
# (importlib-metadata on python < 3.10, pywin32-ctypes on Windows) might not be present while building wheels,
# nor are they required during that phase.
_setup_py_mode = os.environ.get('_PYINSTALLER_SETUP_PY', '0') != '0'

# PyInstaller requires importlib.metadata from python >= 3.10 stdlib, or equivalent importlib-metadata >= 4.6.
if _setup_py_mode:
importlib_metadata = None
else:
if sys.version_info >= (3, 10):
import importlib.metadata as importlib_metadata
else:
try:
import importlib_metadata
except ImportError as e:
from PyInstaller.exceptions import ImportlibMetadataError
raise ImportlibMetadataError() from e

import packaging.version # For importlib_metadata version check

# Validate the version
if packaging.version.parse(importlib_metadata.version("importlib-metadata")) < packaging.version.parse("4.6"):
from PyInstaller.exceptions import ImportlibMetadataError
raise ImportlibMetadataError()

# Strict collect mode, which raises error when trying to collect duplicate files into PKG/CArchive or COLLECT.
strict_collect_mode = os.environ.get("PYINSTALLER_STRICT_COLLECT_MODE", "0") != "0"

Expand Down Expand Up @@ -170,26 +196,27 @@
# -> all pyinstaller modules should use win32api from PyInstaller.compat to
# ensure that it can work on MSYS2 (which requires pywin32-ctypes)
if is_win:
try:
from win32ctypes.pywin32 import pywintypes # noqa: F401, E402
from win32ctypes.pywin32 import win32api # noqa: F401, E402
except ImportError:
# This environment variable is set by setup.py
# - It's not an error for pywin32 to not be installed at that point
if not os.environ.get('PYINSTALLER_NO_PYWIN32_FAILURE'):
if _setup_py_mode:
pywintypes = None
win32api = None
else:
try:
from win32ctypes.pywin32 import pywintypes # noqa: F401, E402
from win32ctypes.pywin32 import win32api # noqa: F401, E402
except ImportError as e:
raise SystemExit(
'PyInstaller cannot check for assembly dependencies.\n'
'Please install pywin32-ctypes.\n\n'
'pip install pywin32-ctypes\n'
)
except Exception:
if sys.flags.optimize == 2:
raise SystemExit(
"pycparser, a Windows only indirect dependency of PyInstaller, is incompatible with "
"Python's \"discard docstrings\" (-OO) flag mode. For more information see:\n"
" https://github.com/pyinstaller/pyinstaller/issues/6345"
)
raise
) from e
except Exception as e:
if sys.flags.optimize == 2:
raise SystemExit(
"pycparser, a Windows only indirect dependency of PyInstaller, is incompatible with "
"Python's \"discard docstrings\" (-OO) flag mode. For more information see:\n"
" https://github.com/pyinstaller/pyinstaller/issues/6345"
) from e
raise

# macOS's platform.architecture() can be buggy, so we do this manually here. Based off the python documentation:
# https://docs.python.org/3/library/platform.html#platform.architecture
Expand Down
5 changes: 2 additions & 3 deletions PyInstaller/depend/analysis.py
Expand Up @@ -804,9 +804,8 @@ def _metadata_from(self, package, methods=(), recursive_methods=()) -> set:
require metadata for some distribution (which may not be its own) at runtime. In the case of a match,
collect the required metadata.
"""
from pkg_resources import DistributionNotFound

from PyInstaller.utils.hooks import copy_metadata
from PyInstaller.compat import importlib_metadata

# Generate sets of possible function names to search for.
need_metadata = set()
Expand All @@ -831,7 +830,7 @@ def _metadata_from(self, package, methods=(), recursive_methods=()) -> set:
elif function_name in need_recursive_metadata:
out.update(copy_metadata(package, recursive=True))

except DistributionNotFound:
except importlib_metadata.PackageNotFoundError:
# Currently, we opt to silently skip over missing metadata.
continue

Expand Down
8 changes: 8 additions & 0 deletions PyInstaller/exceptions.py
Expand Up @@ -72,3 +72,11 @@ def __init__(self):
class InvalidSrcDestTupleError(SystemExit):
def __init__(self, src_dest, message):
super().__init__(f"Invalid (SRC, DEST_DIR) tuple: {src_dest!r}. {message}")


class ImportlibMetadataError(SystemExit):
def __init__(self):
super().__init__(
"PyInstaller requires importlib.metadata from python >= 3.10 stdlib or importlib_metadata from "
"importlib-metadata >= 4.6"
)
4 changes: 2 additions & 2 deletions PyInstaller/hooks/hook-PySide6.py
Expand Up @@ -9,7 +9,7 @@
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller.utils.hooks import is_module_satisfies
from PyInstaller.utils.hooks import check_requirement
from PyInstaller.utils.hooks.qt import pyside6_library_info

# Only proceed if PySide6 can be imported.
Expand All @@ -18,7 +18,7 @@

# Starting with PySide6 6.4.0, we need to collect PySide6.support.deprecated for | and & operators to work with
# Qt key and key modifiers enums. See #7249.
if is_module_satisfies("PySide6 >= 6.4.0"):
if check_requirement("PySide6 >= 6.4.0"):
hiddenimports += ['PySide6.support.deprecated']

# Collect required Qt binaries.
Expand Down
4 changes: 2 additions & 2 deletions PyInstaller/hooks/hook-distutils.py
Expand Up @@ -9,7 +9,7 @@
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller.utils.hooks import is_module_satisfies
from PyInstaller.utils.hooks import check_requirement

hiddenimports = []

Expand All @@ -29,5 +29,5 @@
# anyway), so check if we are using that version. While the distutils override behavior can be controleld via the
# ``SETUPTOOLS_USE_DISTUTILS`` environment variable, the latter may have a different value during the build and at the
# runtime, and so we need to ensure that both stdlib and setuptools variant of distutils are collected.
if is_module_satisfies("setuptools >= 60.0"):
if check_requirement("setuptools >= 60.0"):
hiddenimports += ['setuptools._distutils']
4 changes: 2 additions & 2 deletions PyInstaller/hooks/hook-gi.repository.GObject.py
Expand Up @@ -8,12 +8,12 @@
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
from PyInstaller.utils.hooks import is_module_satisfies
from PyInstaller.utils.hooks import check_requirement
from PyInstaller.utils.hooks.gi import GiModuleInfo

module_info = GiModuleInfo('GObject', '2.0')
if module_info.available:
binaries, datas, hiddenimports = module_info.collect_typelib_data()
# gi._gobject removed from PyGObject in version 3.25.1
if is_module_satisfies('PyGObject < 3.25.1'):
if check_requirement('PyGObject < 3.25.1'):
hiddenimports += ['gi._gobject']
6 changes: 3 additions & 3 deletions PyInstaller/hooks/hook-importlib_resources.py
Expand Up @@ -12,11 +12,11 @@
`importlib_resources` is a backport of the 3.9+ module `importlib.resources`
"""

from PyInstaller.utils.hooks import is_module_satisfies, collect_data_files
from PyInstaller.utils.hooks import check_requirement, collect_data_files

# Prior to v1.2.0, a `version.txt` file is used to set __version__. Later versions use `importlib.metadata`.
if is_module_satisfies("importlib_resources < 1.2.0"):
if check_requirement("importlib_resources < 1.2.0"):
datas = collect_data_files("importlib_resources", includes=["version.txt"])

if is_module_satisfies("importlib_resources >= 1.3.1"):
if check_requirement("importlib_resources >= 1.3.1"):
hiddenimports = ['importlib_resources.trees']
4 changes: 2 additions & 2 deletions PyInstaller/hooks/hook-kivy.py
Expand Up @@ -10,9 +10,9 @@
#-----------------------------------------------------------------------------

from PyInstaller import log as logging
from PyInstaller.utils.hooks import is_module_satisfies
from PyInstaller.utils.hooks import check_requirement

if is_module_satisfies('kivy >= 1.9.1'):
if check_requirement('kivy >= 1.9.1'):
from kivy.tools.packaging.pyinstaller_hooks import (add_dep_paths, get_deps_all, get_factory_modules, kivy_modules)
from kivy.tools.packaging.pyinstaller_hooks import excludedimports, datas # noqa: F401

Expand Down
2 changes: 1 addition & 1 deletion PyInstaller/hooks/hook-matplotlib.py
Expand Up @@ -31,7 +31,7 @@ def mpl_data_dir():
# in contemporary PyInstaller versions, we also need to collect the load-order file. This used to be required for
# python <= 3.7 (that lacked `os.add_dll_directory`), but is also needed for Anaconda python 3.8 and 3.9, where
# `delvewheel` falls back to load-order file codepath due to Anaconda breaking `os.add_dll_directory` implementation.
if compat.is_win and hookutils.is_module_satisfies('matplotlib >= 3.7.0'):
if compat.is_win and hookutils.check_requirement('matplotlib >= 3.7.0'):
delvewheel_datas, delvewheel_binaries = hookutils.collect_delvewheel_libs_directory('matplotlib')

datas += delvewheel_datas
Expand Down
4 changes: 2 additions & 2 deletions PyInstaller/hooks/hook-numpy.py
Expand Up @@ -22,7 +22,7 @@
"""

from PyInstaller.compat import is_conda, is_pure_conda
from PyInstaller.utils.hooks import collect_dynamic_libs, is_module_satisfies
from PyInstaller.utils.hooks import collect_dynamic_libs, check_requirement

# Collect all DLLs inside numpy's installation folder, dump them into built app's root.
binaries = collect_dynamic_libs("numpy", ".")
Expand Down Expand Up @@ -52,7 +52,7 @@

# As of version 1.22, numpy.testing (imported for example by some scipy modules) requires numpy.distutils and distutils.
# So exclude them only for earlier versions.
if is_module_satisfies("numpy < 1.22"):
if check_requirement("numpy < 1.22"):
excludedimports += [
"distutils",
"numpy.distutils",
Expand Down
4 changes: 2 additions & 2 deletions PyInstaller/hooks/hook-pandas.plotting.py
Expand Up @@ -9,10 +9,10 @@
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller.utils.hooks import is_module_satisfies
from PyInstaller.utils.hooks import check_requirement

# Starting with pandas 1.3.0, pandas.plotting._matplotlib is imported via importlib.import_module() and needs to be
# added to hidden imports. But do this only if matplotlib is available in the first place (as it is soft dependency
# of pandas).
if is_module_satisfies('pandas >= 1.3.0') and is_module_satisfies('matplotlib'):
if check_requirement('pandas >= 1.3.0') and check_requirement('matplotlib'):
hiddenimports = ['pandas.plotting._matplotlib']
4 changes: 2 additions & 2 deletions PyInstaller/hooks/hook-pandas.py
Expand Up @@ -9,12 +9,12 @@
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller.utils.hooks import collect_submodules, is_module_satisfies
from PyInstaller.utils.hooks import collect_submodules, check_requirement

# Pandas keeps Python extensions loaded with dynamic imports here.
hiddenimports = collect_submodules('pandas._libs')

# Pandas 1.2.0 and later require cmath hidden import on linux and macOS. On Windows, this is not strictly required, but
# we add it anyway to keep things simple (and future-proof).
if is_module_satisfies('pandas >= 1.2.0'):
if check_requirement('pandas >= 1.2.0'):
hiddenimports += ['cmath']
8 changes: 4 additions & 4 deletions PyInstaller/hooks/hook-pkg_resources.py
Expand Up @@ -9,7 +9,7 @@
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller.utils.hooks import collect_submodules, is_module_satisfies, can_import_module
from PyInstaller.utils.hooks import collect_submodules, check_requirement, can_import_module

# pkg_resources keeps vendored modules in its _vendor subpackage, and does sys.meta_path based import magic to expose
# them as pkg_resources.extern.*
Expand All @@ -25,7 +25,7 @@

# pkg_resources v45.0 dropped support for Python 2 and added this module printing a warning. We could save some bytes if
# we would replace this by a fake module.
if is_module_satisfies('setuptools >= 45.0.0, < 49.1.1'):
if check_requirement('setuptools >= 45.0.0, < 49.1.1'):
hiddenimports.append('pkg_resources.py2_warn')

excludedimports = ['__main__']
Expand All @@ -42,14 +42,14 @@
# In setuptools 60.7.0, the vendored jaraco.text package included "Lorem Ipsum.txt" data file, which also has to be
# collected. However, the presence of the data file (and the resulting directory hierarchy) confuses the importer's
# redirection logic; instead of trying to work-around that, tell user to upgrade or downgrade their setuptools.
if is_module_satisfies("setuptools == 60.7.0"):
if check_requirement("setuptools == 60.7.0"):
raise SystemExit(
"ERROR: Setuptools 60.7.0 is incompatible with PyInstaller. "
"Downgrade to an earlier version or upgrade to a later version."
)
# In setuptools 60.7.1, the "Lorem Ipsum.txt" data file was dropped from the vendored jaraco.text package, so we can
# accommodate it with couple of hidden imports.
elif is_module_satisfies("setuptools >= 60.7.1"):
elif check_requirement("setuptools >= 60.7.1"):
hiddenimports += [
'pkg_resources._vendor.jaraco.functools',
'pkg_resources._vendor.jaraco.context',
Expand Down
4 changes: 2 additions & 2 deletions PyInstaller/hooks/hook-scipy.py
Expand Up @@ -13,7 +13,7 @@
import os

from PyInstaller.compat import is_win
from PyInstaller.utils.hooks import get_module_file_attribute, is_module_satisfies, collect_delvewheel_libs_directory
from PyInstaller.utils.hooks import get_module_file_attribute, check_requirement, collect_delvewheel_libs_directory

binaries = []
datas = []
Expand All @@ -28,7 +28,7 @@
binaries.append((dll_glob, "."))

# Handle delvewheel-enabled win32 wheels, which have external scipy.libs directory (scipy >= 0.9.2)
if is_module_satisfies("scipy >= 1.9.2") and is_win:
if check_requirement("scipy >= 1.9.2") and is_win:
datas, binaries = collect_delvewheel_libs_directory('scipy', datas=datas, binaries=binaries)

# collect library-wide utility extension modules
Expand Down
4 changes: 2 additions & 2 deletions PyInstaller/hooks/hook-scipy.spatial.transform.rotation.py
Expand Up @@ -9,9 +9,9 @@
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller.utils.hooks import is_module_satisfies
from PyInstaller.utils.hooks import check_requirement

# As of scipy 1.6.0, scipy.spatial.transform.rotation is cython-compiled, so we fail to automatically pick up its
# imports.
if is_module_satisfies("scipy >= 1.6.0"):
if check_requirement("scipy >= 1.6.0"):
hiddenimports = ['scipy.spatial.transform._rotation_groups']
4 changes: 2 additions & 2 deletions PyInstaller/hooks/hook-scipy.stats._stats.py
Expand Up @@ -9,7 +9,7 @@
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------

from PyInstaller.utils.hooks import is_module_satisfies
from PyInstaller.utils.hooks import check_requirement

if is_module_satisfies("scipy >= 1.5.0"):
if check_requirement("scipy >= 1.5.0"):
hiddenimports = ['scipy.special.cython_special']
4 changes: 2 additions & 2 deletions PyInstaller/hooks/hook-setuptools.py
Expand Up @@ -10,7 +10,7 @@
#-----------------------------------------------------------------------------

from PyInstaller.compat import is_darwin, is_unix
from PyInstaller.utils.hooks import collect_submodules, is_module_satisfies
from PyInstaller.utils.hooks import collect_submodules, check_requirement

hiddenimports = [
# Test case import/test_zipimport2 fails during importing pkg_resources or setuptools when module not present.
Expand All @@ -36,6 +36,6 @@
# As of setuptools >= 60.0, we need to collect the vendored version of distutils via hiddenimports. The corresponding
# pyi_rth_setuptools runtime hook ensures that the _distutils_hack is installed at the program startup, which allows
# setuptools to override the stdlib distutils with its vendored version, if necessary.
if is_module_satisfies("setuptools >= 60.0"):
if check_requirement("setuptools >= 60.0"):
hiddenimports += ["_distutils_hack"]
hiddenimports += collect_submodules("setuptools._distutils")