Source code for torchrl.render.import_utils
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.
from __future__ import annotations
import hashlib
import importlib
import importlib.util
import inspect
import sys
from collections.abc import Callable, Mapping
from pathlib import Path
from typing import Any
__all__ = ["call_with_supported_kwargs", "import_from_string"]
[docs]
def import_from_string(spec: str) -> Any:
"""Imports an object from a ``"module:attribute"`` string.
Args:
spec: Import specification.
Returns:
The imported Python object.
"""
if not isinstance(spec, str) or ":" not in spec:
raise ValueError(
"Import specs must have the form 'module.submodule:object', "
f"got {spec!r}."
)
module_name, attr_path = spec.rsplit(":", 1)
if not module_name or not attr_path:
raise ValueError(
"Import specs must have the form 'module.submodule:object', "
f"got {spec!r}."
)
module = _import_module_or_file(module_name, spec)
obj: Any = module
current = module_name
for attr in attr_path.split("."):
current = f"{current}.{attr}"
if not hasattr(obj, attr):
raise ImportError(
f"Could not import {spec!r}. Checked '{current}', but the "
f"attribute '{attr}' was missing."
)
obj = getattr(obj, attr)
return obj
def _import_module_or_file(module_name: str, spec: str) -> Any:
path = Path(module_name).expanduser()
if path.suffix == ".py" or "/" in module_name or "\\" in module_name:
if not path.exists():
raise ImportError(
f"Could not import file '{module_name}' from import spec {spec!r}."
)
digest = hashlib.sha1(str(path.resolve()).encode()).hexdigest()
import_name = f"_torchrl_render_{path.stem}_{digest}"
module_spec = importlib.util.spec_from_file_location(import_name, path)
if module_spec is None or module_spec.loader is None:
raise ImportError(
f"Could not load Python file '{module_name}' from import spec {spec!r}."
)
module = importlib.util.module_from_spec(module_spec)
sys.modules[import_name] = module
try:
module_spec.loader.exec_module(module)
except Exception as err:
raise ImportError(
f"Could not execute Python file '{module_name}' from import spec {spec!r}."
) from err
return module
try:
return importlib.import_module(module_name)
except Exception as err:
raise ImportError(
f"Could not import module '{module_name}' from import spec {spec!r}."
) from err
[docs]
def call_with_supported_kwargs(
factory: Callable[..., Any],
preferred_arg: Any,
kwargs: Mapping[str, Any],
) -> Any:
"""Calls a user factory with a spec object or supported keyword arguments.
Args:
factory: User callable.
preferred_arg: Spec object used for the documented one-argument protocol.
kwargs: Keyword candidates for convenience protocols.
Returns:
The factory return value.
"""
signature = inspect.signature(factory)
parameters = list(signature.parameters.values())
if any(param.kind == inspect.Parameter.VAR_POSITIONAL for param in parameters):
return factory(preferred_arg)
if any(param.kind == inspect.Parameter.VAR_KEYWORD for param in parameters):
return factory(**dict(kwargs))
positional = [
param
for param in parameters
if param.kind
in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
]
required = [
param
for param in parameters
if param.default is inspect.Parameter.empty
and param.kind
in (
inspect.Parameter.POSITIONAL_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
inspect.Parameter.KEYWORD_ONLY,
)
]
supported = {
name: value
for name, value in kwargs.items()
if name in signature.parameters
and signature.parameters[name].kind
in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY)
}
missing = [param.name for param in required if param.name not in supported]
if missing:
if len(positional) == 1 and len(missing) == 1:
return factory(preferred_arg)
raise TypeError(
f"Could not call factory {factory!r}; missing required argument(s) "
f"{missing}. Use a single spec argument or one of {sorted(kwargs)}."
)
return factory(**supported)