diff --git a/PyInstaller/__init__.py b/PyInstaller/__init__.py index a488473afda..02cbf7800f6 100644 --- a/PyInstaller/__init__.py +++ b/PyInstaller/__init__.py @@ -55,11 +55,12 @@ # Where to put all the temporary files; .log, .pyz, etc. DEFAULT_WORKPATH = os.path.join(os.getcwd(), 'build') -PLATFORM = compat.system + '-' + compat.architecture +_PLATFORM = compat.system + '-' + compat.architecture +PLATFORM = _PLATFORM # Include machine name in path to bootloader for some machines (e.g., 'arm'). Explicitly avoid doing this on macOS, # where we keep universal2 bootloaders in Darwin-64bit folder regardless of whether we are on x86_64 or arm64. if compat.machine and not compat.is_darwin: - PLATFORM += '-' + compat.machine + PLATFORM = _PLATFORM + '-' + compat.machine # Similarly, disambiguate musl Linux from glibc Linux. if compat.is_musl: - PLATFORM += '-musl' + PLATFORM = _PLATFORM + '-musl' diff --git a/PyInstaller/__main__.py b/PyInstaller/__main__.py index 91bdda33392..879a082699b 100644 --- a/PyInstaller/__main__.py +++ b/PyInstaller/__main__.py @@ -11,17 +11,22 @@ """ Main command-line interface to PyInstaller. """ +from __future__ import annotations import argparse import os import platform from collections import defaultdict +from typing import Dict, Iterable, List, Tuple, Union from PyInstaller import __version__ from PyInstaller import log as logging # Note: do not import anything else until compat.check_requirements function is run! from PyInstaller import compat +_PyIConfig = Union[Dict[str, Union[bool, str, List[str], None]], Iterable[Tuple[str, Union[bool, str, List[str], + None]]]] + logger = logging.getLogger(__name__) # Taken from https://stackoverflow.com/a/22157136 to format args more flexibly: any help text which beings with ``R|`` @@ -146,7 +151,7 @@ def generate_parser() -> _PyiArgumentParser: return parser -def run(pyi_args=None, pyi_config=None): +def run(pyi_args: Iterable[str] | None = None, pyi_config: _PyIConfig | None = None): """ pyi_args allows running PyInstaller programmatically without a subprocess pyi_config allows checking configuration once when running multiple tests diff --git a/PyInstaller/compat.py b/PyInstaller/compat.py index 0e6b59bb0df..f1dd8fab0bf 100644 --- a/PyInstaller/compat.py +++ b/PyInstaller/compat.py @@ -11,6 +11,7 @@ """ Various classes and functions to provide some backwards-compatibility with previous versions of Python onward. """ +from __future__ import annotations import errno @@ -22,12 +23,29 @@ import subprocess import sys import shutil +from typing import Iterable, List, Tuple, Union from PyInstaller._shared_with_waf import _pyi_machine from PyInstaller.exceptions import ExecCommandFailed +if sys.version_info >= (3, 9): + _StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] +else: + _StrOrBytesPath = Union[str, bytes, os.PathLike] +_OpenFile = Union[_StrOrBytesPath, int] + +if sys.version_info >= (3, 8): + from typing import Literal + _Architecture = Literal['64bit', 'n32bit', '32bit'] + _System = Literal['Cygwin', 'Linux', 'Darwin', 'Java', 'Windows'] + _Machine = Literal['sw_64', 'loongarch64', 'arm', 'intel', 'ppc', 'mips', 'riscv', 's390x', 'unknown'] +else: + _Architecture = str + _System = str + _Machine = str + # Copied from https://docs.python.org/3/library/platform.html#cross-platform. -is_64bits = sys.maxsize > 2**32 +is_64bits: bool = sys.maxsize > 2**32 # Distinguish specific code for various Python versions. Variables 'is_pyXY' mean that Python X.Y and up is supported. # Keep even unsupported versions here to keep 3rd-party hooks working. @@ -72,9 +90,9 @@ # disabling the compatibility mode and using python that does not properly support Big Sur still leaves find_library() # broken (which is a scenario that we ignore at the moment). # The same logic applies to macOS 12 (Monterey). -is_macos_11_compat = _macos_ver and _macos_ver[0:2] == (10, 16) # Big Sur or newer in compat mode -is_macos_11_native = _macos_ver and _macos_ver[0:2] >= (11, 0) # Big Sur or newer in native mode -is_macos_11 = is_macos_11_compat or is_macos_11_native # Big Sur or newer +is_macos_11_compat = bool(_macos_ver and _macos_ver[0:2] == (10, 16)) # Big Sur or newer in compat mode +is_macos_11_native = bool(_macos_ver and _macos_ver[0:2] >= (11, 0)) # Big Sur or newer in native mode +is_macos_11 = bool(is_macos_11_compat or is_macos_11_native) # Big Sur or newer # On different platforms is different file for dynamic python library. # TODO: When removing support for is_py37, the "m" variants can be @@ -140,7 +158,7 @@ # The following code creates compat.is_venv and is.virtualenv that are True when running a virtual environment, and also # compat.base_prefix with the path to the base Python installation. -base_prefix = os.path.abspath(getattr(sys, 'real_prefix', getattr(sys, 'base_prefix', sys.prefix))) +base_prefix: str = os.path.abspath(getattr(sys, 'real_prefix', getattr(sys, 'base_prefix', sys.prefix))) # Ensure `base_prefix` is not containing any relative parts. is_venv = is_virtualenv = base_prefix != os.path.abspath(sys.prefix) @@ -202,20 +220,20 @@ # 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 if is_darwin: - architecture = '64bit' if sys.maxsize > 2**32 else '32bit' + architecture: _Architecture = '64bit' if sys.maxsize > 2**32 else '32bit' else: - architecture = platform.architecture()[0] + architecture: _Architecture = platform.architecture()[0] # Cygwin needs special handling, because platform.system() contains identifiers such as MSYS_NT-10.0-19042 and # CYGWIN_NT-10.0-19042 that do not fit PyInstaller's OS naming scheme. Explicitly set `system` to 'Cygwin'. -system = 'Cygwin' if is_cygwin else platform.system() +system: _System = 'Cygwin' if is_cygwin else platform.system() # Machine suffix for bootloader. -machine = _pyi_machine(platform.machine(), platform.system()) +machine: _Machine | None = _pyi_machine(platform.machine(), platform.system()) # Wine detection and support -def is_wine_dll(filename): +def is_wine_dll(filename: _OpenFile): """ Check if the given PE file is a Wine DLL (PE-converted built-in, or fake/placeholder one). @@ -254,21 +272,21 @@ def is_wine_dll(filename): # better to modify os.environ." (Same for unsetenv.) -def getenv(name, default=None) -> str: +def getenv(name: str, default: str | None = None): """ Returns unicode string containing value of environment variable 'name'. """ return os.environ.get(name, default) -def setenv(name, value): +def setenv(name: str, value: str): """ Accepts unicode string and set it as environment variable 'name' containing value 'value'. """ os.environ[name] = value -def unsetenv(name): +def unsetenv(name: str): """ Delete the environment variable 'name'. """ @@ -281,7 +299,12 @@ def unsetenv(name): # Exec commands in subprocesses. -def exec_command(*cmdargs: str, encoding: str = None, raise_enoent: bool = None, **kwargs): +def exec_command( + *cmdargs: str, + encoding: str | None = None, + raise_enoent: bool | None = None, + **kwargs: int | bool | Iterable[int] | None +): """ Run the command specified by the passed positional arguments, optionally configured by the passed keyword arguments. @@ -360,7 +383,7 @@ def exec_command(*cmdargs: str, encoding: str = None, raise_enoent: bool = None, return out -def exec_command_rc(*cmdargs: str, **kwargs) -> int: +def exec_command_rc(*cmdargs: str, **kwargs: float | bool | Iterable[int] | None): """ Return the exit code of the command specified by the passed positional arguments, optionally configured by the passed keyword arguments. @@ -388,7 +411,9 @@ def exec_command_rc(*cmdargs: str, **kwargs) -> int: return subprocess.call(cmdargs, **kwargs) -def exec_command_stdout(*command_args: str, encoding: str = None, **kwargs) -> str: +def exec_command_stdout( + *command_args: str, encoding: str | None = None, **kwargs: float | str | bytes | bool | Iterable[int] | None +): """ Capture and return the standard output of the command specified by the passed positional arguments, optionally configured by the passed keyword arguments. @@ -404,7 +429,7 @@ def exec_command_stdout(*command_args: str, encoding: str = None, **kwargs) -> s Parameters ---------- - command_args : list[str] + command_args : List[str] Variadic list whose: 1. Mandatory first element is the absolute path, relative path, or basename in the current `${PATH}` of the command to run. @@ -434,7 +459,9 @@ def exec_command_stdout(*command_args: str, encoding: str = None, **kwargs) -> s return stdout if encoding is None else stdout.decode(encoding) -def exec_command_all(*cmdargs: str, encoding: str = None, **kwargs): +def exec_command_all(*cmdargs: str, + encoding: str | None = None, + **kwargs: int | bool | Iterable[int] | None) -> Tuple[int, str, str]: """ Run the command specified by the passed positional arguments, optionally configured by the passed keyword arguments. @@ -533,7 +560,7 @@ def __wrap_python(args, kwargs): return cmdargs, kwargs -def exec_python(*args, **kwargs): +def exec_python(*args: str, **kwargs: str | None): """ Wrap running python script in a subprocess. @@ -543,7 +570,7 @@ def exec_python(*args, **kwargs): return exec_command(*cmdargs, **kwargs) -def exec_python_rc(*args, **kwargs): +def exec_python_rc(*args: str, **kwargs: str | None): """ Wrap running python script in a subprocess. @@ -556,7 +583,7 @@ def exec_python_rc(*args, **kwargs): # Path handling. -def expand_path(path): +def expand_path(path: _StrOrBytesPath): """ Replace initial tilde '~' in path with user's home directory, and also expand environment variables (i.e., ${VARNAME} on Unix, %VARNAME% on Windows). @@ -565,7 +592,7 @@ def expand_path(path): # Site-packages functions - use native function if available. -def getsitepackages(prefixes=None): +def getsitepackages(prefixes: Iterable[str] | None = None): """ Returns a list containing all global site-packages directories. @@ -573,7 +600,7 @@ def getsitepackages(prefixes=None): subdirectory depending on the system environment, and returns a list of full paths. """ # This implementation was copied from the ``site`` module, python 3.7.3. - sitepackages = [] + sitepackages: List[str] = [] seen = set() if prefixes is None: @@ -597,7 +624,7 @@ def getsitepackages(prefixes=None): # Wrapper to load a module from a Python source file. This function loads import hooks when processing them. -def importlib_load_source(name, pathname): +def importlib_load_source(name: str, pathname: bytes | str): # Import module from a file. mod_loader = importlib.machinery.SourceFileLoader(name, pathname) return mod_loader.load_module() diff --git a/PyInstaller/fake-modules/pyi_splash.py b/PyInstaller/fake-modules/pyi_splash.py index 03236bb8fd8..a353e2f831c 100644 --- a/PyInstaller/fake-modules/pyi_splash.py +++ b/PyInstaller/fake-modules/pyi_splash.py @@ -172,7 +172,7 @@ def is_alive(): @_check_connection -def update_text(msg): +def update_text(msg: str): """ Updates the text on the splash screen window. diff --git a/PyInstaller/utils/hooks/__init__.py b/PyInstaller/utils/hooks/__init__.py index c455df33f56..681f2bb6219 100644 --- a/PyInstaller/utils/hooks/__init__.py +++ b/PyInstaller/utils/hooks/__init__.py @@ -9,6 +9,8 @@ # SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception) #----------------------------------------------------------------------------- +from __future__ import annotations + import copy import os import sys @@ -16,17 +18,31 @@ import fnmatch from pathlib import Path from collections import deque -from typing import Callable, Tuple +from typing import Callable, Dict, Iterable, List, Tuple, Union import pkg_resources from PyInstaller import HOMEPATH, compat from PyInstaller import log as logging +from PyInstaller.depend.imphookapi import PostGraphAPI from PyInstaller.exceptions import ExecCommandFailed from PyInstaller.utils.hooks.win32 import \ get_pywin32_module_file_attribute # noqa: F401 from PyInstaller import isolated +if sys.version_info >= (3, 9): + _StrOrBytesPath = Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] +else: + _StrOrBytesPath = Union[str, bytes, os.PathLike] + +if sys.version_info >= (3, 8): + from typing import Literal + _OnError = Literal["ignore", "warn once", "warn", "raise"] +else: + _OnError = str + +_Environ = Union[Dict[str, str], Iterable[Tuple[str, str]]] + logger = logging.getLogger(__name__) # These extensions represent Python executables and should therefore be ignored when collecting data files. @@ -39,7 +55,7 @@ # # For example the 'wx' module needs variable 'wxpubsub'. This tells PyInstaller which protocol of the wx module # should be bundled. -hook_variables = {} +hook_variables: Dict[str, str] = {} def __exec_python_cmd(cmd, env=None, capture_stdout=True): @@ -76,7 +92,7 @@ def __exec_statement(statement, capture_stdout=True): return __exec_python_cmd(cmd, capture_stdout=capture_stdout) -def exec_statement(statement): +def exec_statement(statement: str): """ Execute a single Python statement in an externally-spawned interpreter, and return the resulting standard output as a string. @@ -96,7 +112,7 @@ def exec_statement(statement): return __exec_statement(statement, capture_stdout=True) -def exec_statement_rc(statement): +def exec_statement_rc(statement: str): """ Executes a Python statement in an externally spawned interpreter, and returns the exit code. """ @@ -124,7 +140,7 @@ def __exec_script(script_filename, *args, env=None, capture_stdout=True): return __exec_python_cmd(cmd, env=env, capture_stdout=capture_stdout) -def exec_script(script_filename, *args, env=None): +def exec_script(script_filename: _StrOrBytesPath, *args: str, env: _Environ | None = None): """ Executes a Python script in an externally spawned interpreter, and returns anything that was emitted to the standard output as a single string. @@ -135,7 +151,7 @@ def exec_script(script_filename, *args, env=None): return __exec_script(script_filename, *args, env=env, capture_stdout=True) -def exec_script_rc(script_filename, *args, env=None): +def exec_script_rc(script_filename: _StrOrBytesPath, *args: str, env: _Environ | None = None): """ Executes a Python script in an externally spawned interpreter, and returns the exit code. @@ -145,7 +161,7 @@ def exec_script_rc(script_filename, *args, env=None): return __exec_script(script_filename, *args, env=env, capture_stdout=False) -def eval_statement(statement): +def eval_statement(statement: str): """ Execute a single Python statement in an externally-spawned interpreter, and :func:`eval` its output (if any). @@ -170,8 +186,8 @@ def eval_statement(statement): return eval(txt) -def eval_script(scriptfilename, *args, env=None): - txt = exec_script(scriptfilename, *args, env=env).strip() +def eval_script(script_filename: _StrOrBytesPath, *args: str, env: _Environ | None = None): + txt = exec_script(script_filename, *args, env=env).strip() if not txt: # Return an empty string, which is "not true" but is iterable. return '' @@ -179,7 +195,7 @@ def eval_script(scriptfilename, *args, env=None): @isolated.decorate -def get_pyextension_imports(module_name): +def get_pyextension_imports(module_name: str): """ Return list of modules required by binary (C/C++) Python extension. @@ -204,7 +220,7 @@ def get_pyextension_imports(module_name): return list(set(sys.modules.keys()) - original - {module_name}) -def get_homebrew_path(formula=''): +def get_homebrew_path(formula: str = ''): """ Return the homebrew path to the requested formula, or the global prefix when called with no argument. @@ -231,7 +247,7 @@ def get_homebrew_path(formula=''): return None -def remove_prefix(string, prefix): +def remove_prefix(string: str, prefix: str): """ This function removes the given prefix from a string, if the string does indeed begin with the prefix; otherwise, it returns the original string. @@ -242,7 +258,7 @@ def remove_prefix(string, prefix): return string -def remove_suffix(string, suffix): +def remove_suffix(string: str, suffix: str): """ This function removes the given suffix from a string, if the string does indeed end with the suffix; otherwise, it returns the original string. @@ -255,7 +271,7 @@ def remove_suffix(string, suffix): # TODO: Do we really need a helper for this? This is pretty trivially obvious. -def remove_file_extension(filename): +def remove_file_extension(filename: str): """ This function returns filename without its extension. @@ -269,7 +285,7 @@ def remove_file_extension(filename): @isolated.decorate -def can_import_module(module_name): +def can_import_module(module_name: str): """ Check if the specified module can be imported. @@ -294,7 +310,7 @@ def can_import_module(module_name): # TODO: Replace most calls to exec_statement() with calls to this function. -def get_module_attribute(module_name, attr_name): +def get_module_attribute(module_name: str, attr_name: str): """ Get the string value of the passed attribute from the passed module if this attribute is defined by this module _or_ raise `AttributeError` otherwise. @@ -332,7 +348,7 @@ def _get_module_attribute(module_name, attr_name): raise AttributeError(f"Failed to retrieve attribute {attr_name} from module {module_name}") from e -def get_module_file_attribute(package): +def get_module_file_attribute(package: str): """ Get the absolute path to the specified module or package. @@ -353,7 +369,7 @@ def get_module_file_attribute(package): # First, try to use 'pkgutil'. It is the fastest way, but does not work on certain modules in pywin32 that replace # all module attributes with those of the .dll. In addition, we need to avoid it for submodules/subpackages, # because it ends up importing their parent package, which would cause an import leak during the analysis. - filename = None + filename: str | None = None if '.' not in package: try: import pkgutil @@ -395,7 +411,11 @@ def _get_module_file_attribute(package): return filename -def is_module_satisfies(requirements, version=None, version_attr='__version__'): +def is_module_satisfies( + requirements: Iterable[str] | pkg_resources.Requirement, + version: str | pkg_resources.Distribution | None = None, + version_attr: str = "__version__", +): """ Test if a :pep:`0440` requirement is installed. @@ -497,7 +517,7 @@ def is_module_satisfies(requirements, version=None, version_attr='__version__'): return version in requirements_parsed -def is_package(module_name): +def is_package(module_name: str): """ Check if a Python module is really a module or is a package containing other modules, without importing anything in the main process. @@ -505,7 +525,7 @@ def is_package(module_name): :param module_name: Module name to check. :return: True if module is a package else otherwise. """ - def _is_package(module_name): + def _is_package(module_name: str): """ Determines whether the given name represents a package or not. If the name represents a top-level module or a package, it is not imported. If the name represents a sub-module or a sub-package, its parent is imported. @@ -526,13 +546,13 @@ def _is_package(module_name): return isolated.call(_is_package, module_name) -def get_all_package_paths(package): +def get_all_package_paths(package: str): """ Given a package name, return all paths associated with the package. Typically, packages have a single location path, but PEP 420 namespace packages may be split across multiple locations. Returns an empty list if the specified package is not found or is not a package. """ - def _get_package_paths(package): + def _get_package_paths(package: str): """ Retrieve package path(s), as advertised by submodule_search_paths attribute of the spec obtained via importlib.util.find_spec(package). If the name represents a top-level package, the package is not imported. @@ -559,7 +579,7 @@ def _get_package_paths(package): return pkg_paths -def package_base_path(package_path, package): +def package_base_path(package_path: str, package: str): """ Given a package location path and package name, return the package base path, i.e., the directory in which the top-level package is located. For example, given the path ``/abs/path/to/python/libs/pkg/subpkg`` and @@ -568,7 +588,7 @@ def package_base_path(package_path, package): return remove_suffix(package_path, package.replace('.', os.sep)) # Base directory -def get_package_paths(package): +def get_package_paths(package: str): """ Given a package, return the path to packages stored on this machine and also returns the path to this particular package. For example, if pkg.subpkg lives in /abs/path/to/python/libs, then this function returns @@ -594,7 +614,11 @@ def get_package_paths(package): return pkg_base, pkg_dir -def collect_submodules(package: str, filter: Callable[[str], bool] = lambda name: True, on_error="warn once"): +def collect_submodules( + package: str, + filter: Callable[[str], bool] = lambda name: True, + on_error: _OnError = "warn once", +): """ List all submodules of a given package. @@ -648,7 +672,7 @@ def collect_submodules(package: str, filter: Callable[[str], bool] = lambda name return [] # Determine the filesystem path(s) to the specified package. - package_submodules = [] + package_submodules: List[str] = [] todo = deque() todo.append(package) @@ -675,8 +699,8 @@ def collect_submodules(package: str, filter: Callable[[str], bool] = lambda name # This function is called in an isolated sub-process via `isolated.Python.call`. def _collect_submodules(name, on_error): - import sys import pkgutil + import sys from traceback import format_exception_only from PyInstaller.utils.hooks import logger @@ -724,7 +748,7 @@ def _collect_submodules(name, on_error): return modules, subpackages, on_error -def is_module_or_submodule(name, mod_or_submod): +def is_module_or_submodule(name: str, mod_or_submod: str): """ This helper function is designed for use in the ``filter`` argument of :func:`collect_submodules`, by returning ``True`` if the given ``name`` is a module or a submodule of ``mod_or_submod``. @@ -746,7 +770,7 @@ def is_module_or_submodule(name, mod_or_submod): ] -def collect_dynamic_libs(package, destdir=None): +def collect_dynamic_libs(package: str, destdir: object | None = None): """ This function produces a list of (source, dest) of dynamic library files that reside in package. Its output can be directly assigned to ``binaries`` in a hook script. The package parameter must be a string which names the package. @@ -767,7 +791,7 @@ def collect_dynamic_libs(package, destdir=None): return [] pkg_dirs = get_all_package_paths(package) - dylibs = [] + dylibs: List[Tuple[str, str]] = [] for pkg_dir in pkg_dirs: pkg_base = package_base_path(pkg_dir, package) # Recursively glob for all file patterns in the package directory @@ -787,7 +811,13 @@ def collect_dynamic_libs(package, destdir=None): return dylibs -def collect_data_files(package, include_py_files=False, subdir=None, excludes=None, includes=None): +def collect_data_files( + package: str, + include_py_files: bool = False, + subdir: _StrOrBytesPath | None = None, + excludes: Iterable[str] | None = None, + includes: Iterable[str] | None = None, +): r""" This function produces a list of ``(source, dest)`` non-Python (i.e., data) files that reside in ``package``. Its output can be directly assigned to ``datas`` in a hook script; for example, see ``hook-sphinx.py``. @@ -869,7 +899,7 @@ def clude_walker( sources.add(g) if is_include else sources.discard(g) # Obtain all paths for the specified package, and process each path independently. - datas = [] + datas: List[Tuple[str, str]] = [] pkg_dirs = get_all_package_paths(package) for pkg_dir in pkg_dirs: @@ -890,7 +920,7 @@ def clude_walker( return datas -def collect_system_data_files(path, destdir=None, include_py_files=False): +def collect_system_data_files(path: str, destdir: _StrOrBytesPath | None = None, include_py_files: bool = False): """ This function produces a list of (source, dest) non-Python (i.e., data) files that reside somewhere on the system. Its output can be directly assigned to ``datas`` in a hook script. @@ -902,7 +932,7 @@ def collect_system_data_files(path, destdir=None, include_py_files=False): raise TypeError('path must be a str') # Walk through all file in the given package, looking for data files. - datas = [] + datas: List[Tuple[str, str]] = [] for dirpath, dirnames, files in os.walk(path): for f in files: extension = os.path.splitext(f)[1] @@ -917,7 +947,7 @@ def collect_system_data_files(path, destdir=None, include_py_files=False): return datas -def copy_metadata(package_name, recursive=False): +def copy_metadata(package_name: str, recursive: bool = False): """ Collect distribution metadata so that ``pkg_resources.get_distribution()`` can find it. @@ -964,7 +994,7 @@ def copy_metadata(package_name, recursive=False): todo = deque([package_name]) done = set() - out = [] + out: List[Tuple[str, str]] = [] while todo: package_name = todo.pop() @@ -1083,7 +1113,7 @@ def _copy_metadata_dest(egg_path: str, project_name: str) -> str: ) -def get_installer(module): +def get_installer(module: str): """ Try to find which package manager installed a module. @@ -1147,7 +1177,7 @@ def helper(): @_memoize def _map_distribution_to_packages(): logger.info('Determining a mapping of distributions to packages...') - dist_to_packages = {} + dist_to_packages: Dict[str, List[str]] = {} for p in sys.path: # The path entry ``''`` refers to the current directory. if not p: @@ -1172,8 +1202,8 @@ def _map_distribution_to_packages(): # Given a ``package_name`` as a string, this function returns a list of packages needed to satisfy the requirements. # This output can be assigned directly to ``hiddenimports``. -def requirements_for_package(package_name): - hiddenimports = [] +def requirements_for_package(package_name: str): + hiddenimports: List[str] = [] dist_to_packages = _map_distribution_to_packages() for requirement in pkg_resources.get_distribution(package_name).requires(): @@ -1190,13 +1220,13 @@ def requirements_for_package(package_name): def collect_all( - package_name, - include_py_files=True, - filter_submodules=None, - exclude_datas=None, - include_datas=None, - on_error="warn once", -) -> Tuple[list, list, list]: + package_name: str, + include_py_files: bool = True, + filter_submodules: Callable[[str], bool] | None = None, + exclude_datas: Iterable[str] | None = None, + include_datas: Iterable[str] | None = None, + on_error: _OnError = "warn once", +): """ Collect everything for a given package name. @@ -1225,7 +1255,7 @@ def collect_all( datas, binaries, hiddenimports = collect_all('my_module_name') """ - datas = [] + datas: List[Tuple[str, str]] = [] try: datas += copy_metadata(package_name) except Exception as e: @@ -1244,7 +1274,7 @@ def collect_all( return datas, binaries, hiddenimports -def collect_entry_point(name: str) -> Tuple[list, list]: +def collect_entry_point(name: str): """ Collect modules and metadata for all exporters of a given entry point. @@ -1269,15 +1299,16 @@ def collect_entry_point(name: str) -> Tuple[list, list]: .. versionadded:: 4.3 """ import pkg_resources - datas = [] - imports = [] + datas: List[Tuple[str, str]] = [] + imports: List[str] = [] for dist in pkg_resources.iter_entry_points(name): - datas += copy_metadata(dist.dist.project_name) + project_name = '' if dist.dist is None else dist.dist.project_name + datas += copy_metadata(project_name) imports.append(dist.module_name) return datas, imports -def get_hook_config(hook_api, module_name, key): +def get_hook_config(hook_api: PostGraphAPI, module_name: str, key: str): """ Get user settings for hooks. @@ -1312,7 +1343,11 @@ def get_hook_config(hook_api, module_name, key): return value -def include_or_exclude_file(filename, include_list=None, exclude_list=None): +def include_or_exclude_file( + filename: str, + include_list: Iterable[str] | None = None, + exclude_list: Iterable[str] | None = None, +): """ Generic inclusion/exclusion decision function based on filename and list of include and exclude patterns. diff --git a/PyInstaller/utils/hooks/conda.py b/PyInstaller/utils/hooks/conda.py index 437107da1ce..a4d6fe191fd 100644 --- a/PyInstaller/utils/hooks/conda.py +++ b/PyInstaller/utils/hooks/conda.py @@ -33,20 +33,35 @@ Packages are all referenced by the *distribution name* you use to install it, rather than the *package name* you import it with. I.e., use ``distribution("pillow")`` instead of ``distribution("PIL")`` or use ``package_distribution("PIL")``. """ +from __future__ import annotations import fnmatch import json import sys -from pathlib import Path -from typing import Iterable, List +from pathlib import Path, PurePath +from typing import Dict, Iterable, List, Tuple, Union +from os import PathLike from PyInstaller import compat from PyInstaller.log import logger -if compat.is_py38: +if sys.version_info >= (3, 9): + _StrOrBytesPath = Union[str, bytes, PathLike[str], PathLike[bytes]] +else: + _StrOrBytesPath = Union[str, bytes, PathLike] + +if sys.version_info >= (3, 8): from importlib.metadata import PackagePath as _PackagePath + from typing import TypedDict + + class _RawDict(TypedDict): + name: str + version: str + files: List[Union[_StrOrBytesPath, PurePath]] + depends: List[str] else: from importlib_metadata import PackagePath as _PackagePath + _RawDict = Dict[str, Union[str, List[str], List[Union[_StrOrBytesPath, PurePath]]]] # Conda virtual environments each get their own copy of `conda-meta` so the use of `sys.prefix` instead of # `sys.base_prefix`, `sys.real_prefix` or anything from our `compat` module is intentional. @@ -54,7 +69,7 @@ CONDA_META_DIR = CONDA_ROOT / "conda-meta" # Find all paths in `sys.path` that are inside Conda root. -PYTHONPATH_PREFIXES = [] +PYTHONPATH_PREFIXES: List[Path] = [] for _path in sys.path: _path = Path(_path) try: @@ -81,7 +96,7 @@ class Distribution: This class is not intended to be constructed directly by users. Rather use :meth:`distribution` or :meth:`package_distribution` to provide one for you. """ - def __init__(self, json_path): + def __init__(self, json_path: str): try: self._json_path = Path(json_path) assert self._json_path.exists() @@ -92,7 +107,7 @@ def __init__(self, json_path): ) # Everything we need (including this distribution's name) is kept in the metadata json. - self.raw = json.loads(self._json_path.read_text()) + self.raw: _RawDict = json.loads(self._json_path.read_text()) # Unpack the more useful contents of the json. self.name = self.raw["name"] @@ -113,7 +128,7 @@ def _init_dependencies(self): The names in ``self.raw["depends"]`` come with extra version constraint information which must be stripped. """ - dependencies = [] + dependencies: List[str] = [] # For each dependency: for dependency in self.raw["depends"]: # ``dependency`` is a string of the form: "[name] [version constraints]" @@ -130,7 +145,7 @@ def _init_package_names(self): These are names you would ``import`` rather than names you would install. """ - packages = [] + packages: List[str] = [] for file in self.files: package = _get_package_name(file) if package is not None: @@ -138,7 +153,7 @@ def _init_package_names(self): return packages @classmethod - def from_name(cls, name): + def from_name(cls, name: str): """ Get distribution information for a given distribution **name** (i.e., something you would ``conda install``). @@ -151,7 +166,7 @@ def from_name(cls, name): ) @classmethod - def from_package_name(cls, name): + def from_package_name(cls, name: str): """ Get distribution information for a **package** (i.e., something you would import). @@ -187,7 +202,7 @@ def locate(self): return Path(sys.prefix) / self -def walk_dependency_tree(initial: str, excludes: Iterable[str] = None) -> dict: +def walk_dependency_tree(initial: str, excludes: Iterable[str] | None = None): """ Collect a :class:`Distribution` and all direct and indirect dependencies of that distribution. @@ -205,7 +220,7 @@ def walk_dependency_tree(initial: str, excludes: Iterable[str] = None) -> dict: # Rather than use true recursion, mimic it with a to-do queue. from collections import deque - done = {} + done: Dict[str, Distribution] = {} names_to_do = deque([initial]) while names_to_do: @@ -244,7 +259,7 @@ def _iter_distributions(name, dependencies, excludes): return [Distribution.from_name(name)] -def requires(name: str, strip_versions=False) -> List[str]: +def requires(name: str, strip_versions: bool = False): """ List requirements of a distribution. @@ -261,7 +276,7 @@ def requires(name: str, strip_versions=False) -> List[str]: return distribution(name).raw["depends"] -def files(name: str, dependencies=False, excludes=None) -> List[PackagePath]: +def files(name: str, dependencies: bool = False, excludes: Iterable[str] | None = None): """ List all files belonging to a distribution. @@ -288,7 +303,7 @@ def files(name: str, dependencies=False, excludes=None) -> List[PackagePath]: lib_dir = PackagePath("lib") -def collect_dynamic_libs(name: str, dest: str = ".", dependencies: bool = True, excludes: Iterable[str] = None) -> List: +def collect_dynamic_libs(name: str, dest: str = ".", dependencies: bool = True, excludes: Iterable[str] | None = None): """ Collect DLLs for distribution **name**. @@ -307,7 +322,7 @@ def collect_dynamic_libs(name: str, dest: str = ".", dependencies: bool = True, This collects libraries only from Conda's shared ``lib`` (Unix) or ``Library/bin`` (Windows) folders. To collect from inside a distribution's installation use the regular :func:`PyInstaller.utils.hooks.collect_dynamic_libs`. """ - _files = [] + _files: List[Tuple[str, str]] = [] for file in files(name, dependencies, excludes): # A file is classified as a DLL if it lives inside the dedicated ``lib_dir`` DLL folder. if file.parent == lib_dir: @@ -367,7 +382,7 @@ def _get_package_name(file: PackagePath): def _init_distributions(): - distributions = {} + distributions: Dict[str, Distribution] = {} for path in CONDA_META_DIR.glob("*.json"): dist = Distribution(path) distributions[dist.name] = dist @@ -378,7 +393,7 @@ def _init_distributions(): def _init_packages(): - distributions_by_package = {} + distributions_by_package: Dict[str, Distribution] = {} for distribution in distributions.values(): for package in distribution.packages: distributions_by_package[package] = distribution diff --git a/PyInstaller/utils/hooks/win32.py b/PyInstaller/utils/hooks/win32.py index 9f0c5fd77bf..fd282bf8a1c 100644 --- a/PyInstaller/utils/hooks/win32.py +++ b/PyInstaller/utils/hooks/win32.py @@ -10,7 +10,7 @@ # ---------------------------------------------------------------------------- -def get_pywin32_module_file_attribute(module_name): +def get_pywin32_module_file_attribute(module_name: str): """ Get the absolute path of the PyWin32 DLL specific to the PyWin32 module with the passed name. diff --git a/pyi_splash-stubs/__init__.pyi b/pyi_splash-stubs/__init__.pyi new file mode 100644 index 00000000000..35440475a91 --- /dev/null +++ b/pyi_splash-stubs/__init__.pyi @@ -0,0 +1,35 @@ +# ----------------------------------------------------------------------------- +# Copyright (c) 2022-2022, PyInstaller Development Team. +# +# Distributed under the terms of the GNU General Public License (version 2 +# or later) with exception for distributing the bootloader. +# +# The full license is in the file COPYING.txt, distributed with this software. +# +# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception) +# ----------------------------------------------------------------------------- + +# https://pyinstaller.org/en/stable/advanced-topics.html#module-pyi_splash +# https://github.com/pyinstaller/pyinstaller/blob/develop/PyInstaller/fake-modules/pyi_splash.py +try: + from typing_extensions import Literal +except ImportError: + from typing import Literal + +__all__ = ["CLOSE_CONNECTION", "FLUSH_CHARACTER", "is_alive", "close", "update_text"] + + +def is_alive() -> bool: + ... + + +def update_text(msg: str) -> None: + ... + + +def close() -> None: + ... + + +CLOSE_CONNECTION: Literal[b"\u0004"] +FLUSH_CHARACTER: Literal[b"\r"] diff --git a/setup.py b/setup.py index 953fd6b4752..db1734f0c9e 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,6 @@ from setuptools import setup, find_packages #-- plug-in building the bootloader - from distutils.core import Command from distutils.command.build import build @@ -258,7 +257,7 @@ def run(self) -> None: **wheel_commands, 'bdist_wheels': bdist_wheels, }, - packages=find_packages(include=["PyInstaller", "PyInstaller.*"]), + packages=find_packages(include=["PyInstaller", "PyInstaller.*", "pyi_splash-stubs"]), package_data={ "PyInstaller": [ # Include all bootloaders in wheels by default.