Skip to content

Commit

Permalink
Classes created with class_from_function now have a valid import path (
Browse files Browse the repository at this point in the history
  • Loading branch information
mauvilsa committed Jul 25, 2023
1 parent 8e23b51 commit 14d9733
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 1 deletion.
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ paths are considered internals and can change in minor and patch releases.
v4.23.0 (2023-07-??)
--------------------

Added
^^^^^
- Classes created with ``class_from_function`` now have a valid import path
(`#309 <https://github.com/omni-us/jsonargparse/issues/309>`__).

Fixed
^^^^^
- Invalid environment variable names when ``env_prefix`` is derived from
Expand Down
11 changes: 11 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1502,6 +1502,17 @@ this function, the example above would change to:
:func:`.class_from_function` requires the input function to have a return
type annotation that must be the class type it returns.

Classes created with :func:`.class_from_function` can be selected using
``class_path`` for :ref:`sub-classes`. For example, if
:func:`.class_from_function` is run in a module ``my_module`` as:

.. testcode:: class_from_function

class_from_function(instantiate_myclass, name="MyClass")

Then the ``class_path`` for the created class would be ``my_module.MyClass``.


Parameter resolvers
-------------------

Expand Down
15 changes: 14 additions & 1 deletion jsonargparse/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,12 +366,14 @@ class ClassFromFunctionBase:
def class_from_function(
func: Callable[..., ClassType],
func_return: Optional[Type[ClassType]] = None,
name: Optional[str] = None,
) -> Type[ClassType]:
"""Creates a dynamic class which if instantiated is equivalent to calling func.
Args:
func: A function that returns an instance of a class.
func_return: The return type of the function. Required if func does not have a return type annotation.
name: The name of the class. Defaults to function name suffixed with "_class".
"""
if func_return is None:
func_return = inspect.signature(func).return_annotation
Expand All @@ -386,17 +388,28 @@ def class_from_function(
func_return = inspect.signature(func).return_annotation
raise ValueError(f"Unable to dereference {func_return}, the return type of {func}: {ex}") from ex

if not name:
name = func.__qualname__.replace(".", "__") + "_class"

caller_module = inspect.getmodule(inspect.stack()[1][0]) or inspect.getmodule(class_from_function)
assert caller_module
if hasattr(caller_module, name):
raise ValueError(f"{caller_module.__name__} already defines {name!r}, please use a different name")

@wraps(func)
def __new__(cls, *args, **kwargs):
return func(*args, **kwargs)

class ClassFromFunction(func_return, ClassFromFunctionBase): # type: ignore
pass

setattr(caller_module, name, ClassFromFunction)
ClassFromFunction.wrapped_function = func
ClassFromFunction.__new__ = __new__ # type: ignore
ClassFromFunction.__doc__ = func.__doc__
ClassFromFunction.__name__ = func.__name__
ClassFromFunction.__module__ = caller_module.__name__
ClassFromFunction.__name__ = name
ClassFromFunction.__qualname__ = name
return ClassFromFunction


Expand Down
9 changes: 9 additions & 0 deletions jsonargparse_tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,15 @@ def test_class_from_function(function, class_type):
cls = class_from_function(function)
assert issubclass(cls, class_type)
assert isinstance(cls(), class_type)
module_path, name = get_import_path(cls).rsplit(".", 1)
assert module_path == __name__
assert cls is globals()[name]


def test_class_from_function_name_clash():
with pytest.raises(ValueError) as ctx:
class_from_function(get_random, name="get_random")
ctx.match("already defines 'get_random', please use a different name")


def get_unknown() -> "Unknown": # type: ignore # noqa: F821
Expand Down

0 comments on commit 14d9733

Please sign in to comment.