378 lines
13 KiB
Python
378 lines
13 KiB
Python
#
|
|
# SPDX-FileCopyrightText: Copyright (c) 1993-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
|
|
import contextlib
|
|
import importlib
|
|
import importlib.util
|
|
import os
|
|
import subprocess as sp
|
|
import sys
|
|
from typing import List
|
|
|
|
try:
|
|
# Available in Python 3.8+
|
|
import importlib.metadata
|
|
except ModuleNotFoundError:
|
|
pass
|
|
|
|
from polygraphy import constants
|
|
from polygraphy.mod import util as mod_util
|
|
|
|
# Tracks all of Polygraphy's lazy imports, excluding internal ones.
|
|
_all_external_lazy_imports = set()
|
|
|
|
# Sometimes the Python package name differs from the module name.
|
|
_PKG_NAME_FROM_MODULE = {}
|
|
|
|
# Some packages need additional flags to install correctly.
|
|
_EXTRA_FLAGS_FOR_MODULE = {
|
|
"onnx_graphsurgeon": ["--extra-index-url=https://pypi.ngc.nvidia.com"],
|
|
}
|
|
|
|
|
|
LATEST_VERSION = "==latest"
|
|
"""Indicates that the latest version of the package is preferred in lazy_import"""
|
|
|
|
|
|
def _version_ok(ver, preferred):
|
|
if preferred == LATEST_VERSION:
|
|
return False
|
|
|
|
pref_ver = preferred.lstrip("<=>").strip()
|
|
cond = preferred.rstrip(pref_ver).strip()
|
|
check = {
|
|
"==": lambda x, y: x == y,
|
|
">=": lambda x, y: x >= y,
|
|
">": lambda x, y: x > y,
|
|
"<=": lambda x, y: x <= y,
|
|
"<": lambda x, y: x < y,
|
|
}[cond]
|
|
return check(mod_util.version(ver), mod_util.version(pref_ver))
|
|
|
|
|
|
def lazy_import(
|
|
name: str,
|
|
log: bool = None,
|
|
pkg_name: str = None,
|
|
install_flags: List[str] = None,
|
|
requires: List[str] = None,
|
|
):
|
|
"""
|
|
Lazily import a module.
|
|
|
|
If config.AUTOINSTALL_DEPS is set to 1,
|
|
missing modules are automatically installed, and existing modules may be
|
|
upgraded if newer versions are required.
|
|
|
|
Args:
|
|
name (str):
|
|
The name of the module and optionally the preferred version of the package,
|
|
formatted as a version string. For example, ``'example_module>=0.5.0'`` or ``'example_module==1.8.0'``.
|
|
log (bool):
|
|
Whether to log information about the module.
|
|
Defaults to True.
|
|
pkg_name (str):
|
|
The name of the package that provides this module, if it is different from the module name.
|
|
Used only if automatic installation of dependencies is enabled.
|
|
install_flags (List[str]):
|
|
Additional flags to provide to the installation command.
|
|
Used only if automatic installation of dependencies is enabled.
|
|
requires (List[str]):
|
|
Additional dependencies required by the module which are *not* specified as dependencies.
|
|
This parameter should only be required when a module does not correctly specify dependencies.
|
|
Defaults to [].
|
|
|
|
Returns:
|
|
LazyModule:
|
|
A lazily loaded module. When an attribute is first accessed,
|
|
the module will be imported.
|
|
"""
|
|
|
|
def issue_wrong_version_error(installed_version, version):
|
|
from polygraphy.logger import G_LOGGER, LogMode
|
|
|
|
G_LOGGER.error(
|
|
f"Module: '{name}' version '{installed_version}' is installed, but version '{version}' is required.\n"
|
|
f"Please install the required version or set POLYGRAPHY_AUTOINSTALL_DEPS=1 in your environment variables "
|
|
f"to allow Polygraphy to do so automatically.\n"
|
|
f"Attempting to continue with the currently installed version of this module, but note that this may cause errors!",
|
|
mode=LogMode.ONCE,
|
|
)
|
|
|
|
VERSION_CHARS = ["=", ">", "<"]
|
|
|
|
log = True if log is None else log
|
|
requires = [] if requires is None else requires
|
|
|
|
def split_name_version(inp):
|
|
version_char_indices = [
|
|
inp.index(char) for char in VERSION_CHARS if char in inp
|
|
]
|
|
if not version_char_indices:
|
|
return inp, None
|
|
|
|
min_index = min(version_char_indices)
|
|
return inp[:min_index], inp[min_index:]
|
|
|
|
name, version = split_name_version(name)
|
|
all_required_mods = list(map(split_name_version, requires)) + [(name, version)]
|
|
|
|
if "polygraphy" not in name:
|
|
_all_external_lazy_imports.add(name)
|
|
|
|
def import_mod():
|
|
from polygraphy import config
|
|
from polygraphy.logger import G_LOGGER, LogMode
|
|
|
|
def install_mod(install_name, install_version, raise_error=True):
|
|
modname = install_name.split(".")[0]
|
|
pkg = (
|
|
pkg_name
|
|
if pkg_name is not None
|
|
else _PKG_NAME_FROM_MODULE.get(modname, modname)
|
|
)
|
|
extra_flags = (
|
|
install_flags
|
|
if install_flags is not None
|
|
else _EXTRA_FLAGS_FOR_MODULE.get(modname, [])
|
|
)
|
|
|
|
def fail():
|
|
log_func = G_LOGGER.critical if raise_error else G_LOGGER.warning
|
|
log_func(
|
|
f"Could not automatically install required module: {pkg}. Please install it manually."
|
|
)
|
|
|
|
if config.ASK_BEFORE_INSTALL:
|
|
res = None
|
|
while res not in ["y", "n"]:
|
|
res = input(
|
|
f"Automatically install '{pkg}' (version: {install_version or 'any'}) ([Y]/n)? "
|
|
)
|
|
res = res.strip()[:1].lower() or "y"
|
|
|
|
if res == "n":
|
|
fail()
|
|
|
|
if install_version == LATEST_VERSION:
|
|
extra_flags.append("--upgrade")
|
|
elif install_version is not None:
|
|
pkg += install_version
|
|
|
|
cmd = config.INSTALL_CMD + [pkg] + extra_flags
|
|
G_LOGGER.info(f"Running installation command: {' '.join(cmd)}")
|
|
status = sp.run(cmd, stdout=sp.PIPE, stderr=sp.PIPE)
|
|
if status.returncode != 0:
|
|
G_LOGGER.error(
|
|
f"Error during installation:\n{constants.TAB}{status.stderr.decode()}"
|
|
)
|
|
fail()
|
|
|
|
mod = importlib.import_module(install_name)
|
|
return mod
|
|
|
|
mod = None
|
|
try:
|
|
mod = importlib.import_module(name)
|
|
except ImportError as err:
|
|
if config.AUTOINSTALL_DEPS:
|
|
for install_name, install_version in all_required_mods:
|
|
G_LOGGER.info(
|
|
f"Module: '{install_name}' is required, but not installed. Attempting to install now."
|
|
)
|
|
mod = install_mod(install_name, install_version)
|
|
else:
|
|
G_LOGGER.critical(
|
|
f"Module: '{name}' is required but could not be imported.\nNote: Error was: {err}\n"
|
|
f"You can set POLYGRAPHY_AUTOINSTALL_DEPS=1 in your environment variables to allow "
|
|
f"Polygraphy to automatically install missing modules.\n"
|
|
)
|
|
|
|
# Auto-upgrade if necessary
|
|
for install_name, install_version in all_required_mods:
|
|
installed_mod = importlib.import_module(install_name)
|
|
if (
|
|
install_version is not None
|
|
and hasattr(installed_mod, "__version__")
|
|
and not _version_ok(installed_mod.__version__, install_version)
|
|
):
|
|
if config.AUTOINSTALL_DEPS:
|
|
G_LOGGER.info(
|
|
f"Note: Module: '{install_name}' version '{installed_mod.__version__}' is installed, but version '{install_version}' is required.\n"
|
|
f"Attempting to upgrade now."
|
|
)
|
|
# We can try to use the other version if install fails, so this is non-fatal.
|
|
installed_mod = install_mod(
|
|
install_name, install_version, raise_error=False
|
|
)
|
|
if install_name == name:
|
|
mod = installed_mod
|
|
|
|
elif install_version != LATEST_VERSION:
|
|
issue_wrong_version_error(
|
|
installed_mod.__version__, install_version
|
|
)
|
|
|
|
if log:
|
|
G_LOGGER.module_info(mod)
|
|
|
|
return mod
|
|
|
|
MODULE_VAR_NAME = "module"
|
|
|
|
class LazyModule:
|
|
def __init__(self):
|
|
super().__setattr__(MODULE_VAR_NAME, None)
|
|
|
|
def __polygraphy_import_mod(self):
|
|
if self.module is None:
|
|
super().__setattr__(MODULE_VAR_NAME, import_mod())
|
|
return self.module
|
|
|
|
def __getattr__(self, name):
|
|
module = self.__polygraphy_import_mod()
|
|
return getattr(module, name)
|
|
|
|
def __setattr__(self, name, value):
|
|
module = self.__polygraphy_import_mod()
|
|
return setattr(module, name, value)
|
|
|
|
def is_installed(self):
|
|
"""
|
|
Checks whether any version of this module is installed.
|
|
The module will not be imported by this method.
|
|
|
|
Returns:
|
|
bool: Whether the module is installed.
|
|
"""
|
|
global importlib
|
|
|
|
try:
|
|
return name in sys.modules or (
|
|
importlib.util.find_spec(name) is not None
|
|
)
|
|
except:
|
|
return False
|
|
|
|
def is_importable(self):
|
|
"""
|
|
Checks whether this module is importable. Note that a module may be installed but not importable.
|
|
|
|
Returns:
|
|
bool: Whether the module is importable.
|
|
"""
|
|
try:
|
|
importlib.import_module(name)
|
|
return True
|
|
except:
|
|
return False
|
|
|
|
return LazyModule()
|
|
|
|
|
|
def has_mod(modname):
|
|
"""
|
|
Checks whether a module is installed without importing the module.
|
|
|
|
Args:
|
|
modname (str): The name of the module to check.
|
|
|
|
Returns:
|
|
bool: Whether the module is installed.
|
|
"""
|
|
import warnings
|
|
|
|
import polygraphy
|
|
from polygraphy.logger import G_LOGGER
|
|
|
|
remove_in = "0.50.0"
|
|
if mod_util.version(polygraphy.__version__) >= mod_util.version(remove_in):
|
|
G_LOGGER.internal_error(
|
|
f"has_mod should have been removed in version: {remove_in}"
|
|
)
|
|
warnings.warn(
|
|
f"has_mod is deprecated and will be removed in Polygraphy {remove_in}",
|
|
DeprecationWarning,
|
|
stacklevel=3,
|
|
)
|
|
|
|
try:
|
|
return modname in sys.modules or (importlib.util.find_spec(modname) is not None)
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
def autoinstall(lazy_mod):
|
|
"""
|
|
If the config.AUTOINSTALL_DEPS is set to 1, automatically install or upgrade a module.
|
|
Does nothing if autoinstallation is disabled.
|
|
|
|
Args:
|
|
lazy_mod (LazyModule):
|
|
A lazy module, like that returned by ``lazy_import``.
|
|
"""
|
|
from polygraphy import config
|
|
|
|
if not config.AUTOINSTALL_DEPS:
|
|
return
|
|
|
|
try:
|
|
# It doesn't matter which attribute we try to get as any call to `__getattr__` will
|
|
# trigger the automatic installation.
|
|
getattr(lazy_mod, "__fake_polygraphy_autoinstall_attr")
|
|
except:
|
|
pass
|
|
|
|
|
|
def import_from_script(path, name):
|
|
"""
|
|
Imports a specified symbol from a Python script.
|
|
|
|
Args:
|
|
path (str): A path to the Python script. The path must include a '.py' extension.
|
|
name (str): The name of the symbol to import from the script.
|
|
|
|
Returns:
|
|
object: The loaded symbol.
|
|
"""
|
|
from polygraphy.logger import G_LOGGER
|
|
|
|
dir = os.path.dirname(path)
|
|
modname = os.path.splitext(os.path.basename(path))[0]
|
|
|
|
sys.path.insert(0, dir)
|
|
|
|
with contextlib.ExitStack() as stack:
|
|
|
|
def reset_sys_path():
|
|
del sys.path[0]
|
|
del sys.modules[modname]
|
|
|
|
stack.callback(reset_sys_path)
|
|
|
|
try:
|
|
importlib.invalidate_caches()
|
|
mod = importlib.import_module(modname)
|
|
return getattr(mod, name)
|
|
except Exception as err:
|
|
ext = os.path.splitext(path)[1]
|
|
err_msg = f"Could not import symbol: {name} from script: {path}"
|
|
if ext != ".py":
|
|
err_msg += f"\nThis could be because the extension of the file is not '.py'. Note: The extension is: {ext}"
|
|
err_msg += f"\nNote: Error was: {err}"
|
|
G_LOGGER.critical(err_msg)
|