diff --git a/PyInstaller/__main__.py b/PyInstaller/__main__.py index 91bdda3339..b7b66684aa 100644 --- a/PyInstaller/__main__.py +++ b/PyInstaller/__main__.py @@ -11,6 +11,7 @@ """ Main command-line interface to PyInstaller. """ +from __future__ import annotations import argparse import os @@ -146,7 +147,7 @@ def generate_parser() -> _PyiArgumentParser: return parser -def run(pyi_args=None, pyi_config=None): +def run(pyi_args: list | None = None, pyi_config: dict | 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 0e6b59bb0d..3eaad29f7e 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 @@ -27,7 +28,7 @@ from PyInstaller.exceptions import ExecCommandFailed # 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,8 +73,8 @@ # 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_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 = is_macos_11_compat or is_macos_11_native # Big Sur or newer # On different platforms is different file for dynamic python library. @@ -140,7 +141,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) @@ -215,7 +216,7 @@ # Wine detection and support -def is_wine_dll(filename): +def is_wine_dll(filename: str | os.PathLike): """ Check if the given PE file is a Wine DLL (PE-converted built-in, or fake/placeholder one). @@ -254,21 +255,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 +282,9 @@ 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 | list | None +): """ Run the command specified by the passed positional arguments, optionally configured by the passed keyword arguments. @@ -360,7 +363,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 | list | None): """ Return the exit code of the command specified by the passed positional arguments, optionally configured by the passed keyword arguments. @@ -388,7 +391,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 | list | 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 +409,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 +439,7 @@ 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 | list | None): """ Run the command specified by the passed positional arguments, optionally configured by the passed keyword arguments. @@ -533,7 +538,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 +548,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 +561,7 @@ def exec_python_rc(*args, **kwargs): # Path handling. -def expand_path(path): +def expand_path(path: str | os.PathLike): """ 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 +570,7 @@ def expand_path(path): # Site-packages functions - use native function if available. -def getsitepackages(prefixes=None): +def getsitepackages(prefixes: list | None = None): """ Returns a list containing all global site-packages directories. @@ -597,7 +602,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: 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 03236bb8fd..a353e2f831 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 c455df33f5..19b1184bf2 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,12 +18,13 @@ import fnmatch from pathlib import Path from collections import deque -from typing import Callable, Tuple +from typing import Callable 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 @@ -76,7 +79,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 +99,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 +127,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: str | os.PathLike, *args: str, env: dict | 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 +138,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: str | os.PathLike, *args: str, env: dict | None = None): """ Executes a Python script in an externally spawned interpreter, and returns the exit code. @@ -145,7 +148,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 +173,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: str | os.PathLike, *args: str, env: dict | 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 +182,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 +207,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 +234,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 +245,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 +258,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 +272,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 +297,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 +335,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 +356,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 +398,11 @@ def _get_module_file_attribute(package): return filename -def is_module_satisfies(requirements, version=None, version_attr='__version__'): +def is_module_satisfies( + requirements: list | pkg_resources.Requirement, + version: str | pkg_resources.Distribution | None = None, + version_attr: str = "__version__", +): """ Test if a :pep:`0440` requirement is installed. @@ -497,7 +504,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 +512,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 +533,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 +566,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 +575,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 +601,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: str = "warn once", +): """ List all submodules of a given package. @@ -724,7 +735,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 +757,7 @@ def is_module_or_submodule(name, mod_or_submod): ] -def collect_dynamic_libs(package, destdir=None): +def collect_dynamic_libs(package: str, destdir: str | 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. @@ -787,7 +798,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: str | os.PathLike | None = None, + excludes: list | None = None, + includes: list | 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``. @@ -890,7 +907,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: str | os.PathLike | 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. @@ -917,7 +934,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. @@ -1083,7 +1100,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. @@ -1172,7 +1189,7 @@ 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): +def requirements_for_package(package_name: str): hiddenimports = [] dist_to_packages = _map_distribution_to_packages() @@ -1190,13 +1207,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 | None = None, + exclude_datas: list | None = None, + include_datas: list | None = None, + on_error: str = "warn once", +): """ Collect everything for a given package name. @@ -1244,7 +1261,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. @@ -1272,12 +1289,13 @@ def collect_entry_point(name: str) -> Tuple[list, list]: datas = [] imports = [] 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 +1330,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: list | None = None, + exclude_list: list | 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 437107da1c..db44bdd3c5 100644 --- a/PyInstaller/utils/hooks/conda.py +++ b/PyInstaller/utils/hooks/conda.py @@ -33,6 +33,7 @@ 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 @@ -81,7 +82,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,11 +93,11 @@ 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: dict = json.loads(self._json_path.read_text()) # Unpack the more useful contents of the json. - self.name = self.raw["name"] - self.version = self.raw["version"] + self.name: str = self.raw["name"] + self.version: str = self.raw["version"] self.files = [PackagePath(i) for i in self.raw["files"]] self.dependencies = self._init_dependencies() self.packages = self._init_package_names() @@ -138,7 +139,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 +152,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 +188,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. @@ -244,7 +245,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[str]: """ List requirements of a distribution. @@ -261,7 +262,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: list | None = None) -> List[PackagePath]: """ List all files belonging to a distribution. @@ -288,7 +289,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**.