Skip to content

Commit

Permalink
Remove unused decorator
Browse files Browse the repository at this point in the history
And reformat tests in the vein of usage in Function and Macro
  • Loading branch information
liamhuber committed Apr 29, 2024
1 parent 6d064db commit d620c7b
Show file tree
Hide file tree
Showing 2 changed files with 44 additions and 129 deletions.
113 changes: 1 addition & 112 deletions pyiron_workflow/io_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ class ScrapesIO(HasIOPreview, ABC):
@classmethod
@abstractmethod
def _io_defining_function(cls) -> callable:
"""Must return a static class method."""
"""Must return a static method."""

_output_labels: ClassVar[tuple[str] | None] = None # None: scrape them
_validate_output_labels: ClassVar[bool] = True # True: validate against source code
Expand Down Expand Up @@ -356,114 +356,3 @@ def inputs(self) -> Inputs:
@property
def outputs(self) -> OutputsWithInjection:
return self._outputs


class DecoratedNode(StaticNode, ScrapesIO, ABC):
"""
A static node whose IO is defined by a function's information (and maybe output
labels).
"""


def decorated_node_decorator_factory(
parent_class: type[DecoratedNode],
io_static_method: callable,
decorator_docstring_additions: str = "",
**parent_class_attr_overrides,
):
"""
A decorator factory for building decorators to dynamically create new subclasses
of some subclass of :class:`DecoratedNode` using the function they decorate.
New classes get their class name and module set using the decorated function's
name and module.
Args:
parent_class (type[DecoratedNode]): The base class for the new node class.
io_static_method: The static method on the :param:`parent_class` which will
store the io-defining function the resulting decorator will decorate.
:param:`parent_class` must override :meth:`_io_defining_function` inherited
from :class:`DecoratedNode` to return this method. This allows
:param:`parent_class` classes to have unique names for their io-defining
functions.
decorator_docstring_additions (str): Any extra text to add between the main
body of the docstring and the arguments.
**parent_class_attr_overrides: Any additional attributes to pass to the new,
dynamically created class created by the resulting decorator.
Returns:
(callable): A decorator that takes creates a new subclass of
:param:`parent_class` that uses the wrapped function as the return value of
:meth:`_io_defining_function` for the :class:`DecoratedNode` mixin.
"""
if getattr(parent_class, io_static_method.__name__) is not io_static_method:
raise ValueError(
f"{io_static_method.__name__} is not a method on {parent_class}"
)
if not isinstance(io_static_method, FunctionType):
raise TypeError(f"{io_static_method.__name__} should be a static method")

def as_decorated_node_decorator(
*output_labels: str,
validate_output_labels: bool = True,
):
output_labels = None if len(output_labels) == 0 else output_labels

@builds_class_io
def as_decorated_node(io_defining_function: callable):
if not callable(io_defining_function):
raise AttributeError(
f"Tried to create a new child class of {parent_class.__name__}, "
f"but got {io_defining_function} instead of a callable."
)

return type(
io_defining_function.__name__,
(parent_class,), # Define parentage
{
io_static_method.__name__: staticmethod(io_defining_function),
"__module__": io_defining_function.__module__,
"_output_labels": output_labels,
"_validate_output_labels": validate_output_labels,
**parent_class_attr_overrides,
},
)

return as_decorated_node

as_decorated_node_decorator.__doc__ = dedent(
f"""
A decorator for dynamically creating `{parent_class.__name__}` sub-classes by
wrapping a function as the `{io_static_method.__name__}`.
The returned subclass uses the wrapped function (and optionally any provided
:param:`output_labels`) to specify its IO.
{decorator_docstring_additions}
Args:
*output_labels (str): A name for each return value of the graph creating
function. When empty, scrapes output labels automatically from the
source code of the wrapped function. This can be useful when returned
values are not well named, e.g. to make the output channel
dot-accessible if it would otherwise have a label that requires
item-string-based access. Additionally, specifying a _single_ label for
a wrapped function that returns a tuple of values ensures that a
_single_ output channel (holding the tuple) is created, instead of one
channel for each return value. The default approach of extracting
labels from the function source code also requires that the function
body contain _at most_ one `return` expression, so providing explicit
labels can be used to circumvent this (at your own risk). (Default is
empty, try to scrape labels from the source code of the wrapped
function.)
validate_output_labels (bool): Whether to compare the provided output labels
(if any) against the source code (if available). (Default is True.)
Returns:
(callable[[callable], type[{parent_class.__name__}]]): A decorator that
transforms a function into a child class of `{parent_class.__name__}`
using the decorated function as
`{parent_class.__name__}.{io_static_method.__name__}`.
"""
)
return as_decorated_node_decorator
60 changes: 43 additions & 17 deletions tests/unit/test_io_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,52 @@
import unittest

from pyiron_workflow.channels import NOT_DATA
from pyiron_workflow.io_preview import (
ScrapesIO, decorated_node_decorator_factory, OutputLabelsNotValidated
)
from pyiron_workflow.io_preview import ScrapesIO, OutputLabelsNotValidated
from pyiron_workflow.snippets.factory import classfactory


class ScraperParent(ScrapesIO, ABC):

@staticmethod
@abstractmethod
def io_function(*args, **kwargs):
pass

class ScrapesFromDecorated(ScrapesIO):
@classmethod
def _io_defining_function(cls):
return cls.io_function


as_scraper = decorated_node_decorator_factory(
ScraperParent, ScraperParent.io_function
)
def _io_defining_function(cls) -> callable:
return cls._decorated_function


@classfactory
def scraper_factory(
io_defining_function,
validate_output_labels,
io_defining_function_uses_self,
/,
*output_labels,
):
return (
io_defining_function.__name__,
(ScrapesFromDecorated,), # Define parentage
{
"_decorated_function": staticmethod(io_defining_function),
"__module__": io_defining_function.__module__,
"_output_labels": None if len(output_labels) == 0 else output_labels,
"_validate_output_labels": validate_output_labels,
"_io_defining_function_uses_self": io_defining_function_uses_self
},
{},
)


def as_scraper(
*output_labels,
validate_output_labels=True,
io_defining_function_uses_self=False,
):
def scraper_decorator(fnc):
scraper_factory.clear(fnc.__name__) # Force a fresh class
factory_made = scraper_factory(
fnc, validate_output_labels, io_defining_function_uses_self, *output_labels
)
factory_made._class_returns_from_decorated_function = fnc
factory_made.preview_io()
return factory_made
return scraper_decorator


class TestIOPreview(unittest.TestCase):
Expand Down

0 comments on commit d620c7b

Please sign in to comment.