diff --git a/README.md b/README.md
index 0b0e3c1..3a1b01f 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,5 @@
+
+
[porchlight](https://porchlight.readthedocs.io/en/latest/)
==========
@@ -47,9 +49,6 @@ neighborhood.add_function(increase_x)
print(neighborhood)
```
-Although this is the current extent of documentation, there should be some more
-complete documentation within the next couple weeks/months.
-
Documentation
-----------
diff --git a/docs/conf.py b/docs/conf.py
index 7417d13..dd2833f 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -23,7 +23,7 @@
author = "D J Teal"
# The full version, including alpha/beta/rc tags
-release = "0.3.1"
+release = "0.4.0"
# -- General configuration ---------------------------------------------------
@@ -68,3 +68,17 @@
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["source/_static"]
# html_css_files = ["porchlight.css"]
+
+
+# Include initialization and call special methods to be documented. From
+# https://stackoverflow.com/questions/5599254/how-to-use-sphinxs-autodoc
+# -to-document-a-classs-init-self-method
+def skip(app, what, name, obj, would_skip, options):
+ if name in ["__init__", "__call__"]:
+ return False
+
+ return would_skip
+
+
+def setup(app):
+ app.connect("autodoc-skip-member", skip)
diff --git a/docs/index.rst b/docs/index.rst
index f71e05a..f213e97 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -5,7 +5,7 @@ Welcome to |porchlight|'s documentation!
coupling Python functions and managing shared data.
.. toctree::
- :maxdepth: 2
+ :maxdepth: 3
:caption: Contents:
other/about
@@ -13,6 +13,7 @@ coupling Python functions and managing shared data.
source/neighborhood
source/door
source/param
+ source/utils
Indices and tables
==================
diff --git a/docs/source/door.rst b/docs/source/door.rst
index ca631fc..4c7976b 100644
--- a/docs/source/door.rst
+++ b/docs/source/door.rst
@@ -1,5 +1,11 @@
`door` module
=============
+The `door` module contains classes that represent interfaces to python
+functions. They are primarily used on functions that have been defined in pure
+python, but can be extended to include arbitrary callables.
+
.. automodule:: porchlight.door
:members:
+ :show-inheritance:
+ :private-members:
diff --git a/docs/source/neighborhood.rst b/docs/source/neighborhood.rst
index 2a84e01..98ca6e4 100644
--- a/docs/source/neighborhood.rst
+++ b/docs/source/neighborhood.rst
@@ -1,5 +1,13 @@
`neighborhood` module
=====================
+`neighborhood` contains the |Neighborhood| class, which acts as a mediator
+class between different functions that have been re-cast as
+:py:class:`~porchlight.door.Door` objects.
+
.. automodule:: porchlight.neighborhood
:members:
+ :show-inheritance:
+ :private-members:
+
+.. |Neighborhood| replace:: :py:class:`~porchlight.neighborhood.Neighborhood`
diff --git a/docs/source/param.rst b/docs/source/param.rst
index 1974bdb..9a5063e 100644
--- a/docs/source/param.rst
+++ b/docs/source/param.rst
@@ -3,3 +3,5 @@
.. automodule:: porchlight.param
:members:
+ :show-inheritance:
+ :private-members:
diff --git a/docs/source/utils.rst b/docs/source/utils.rst
new file mode 100644
index 0000000..f7483b3
--- /dev/null
+++ b/docs/source/utils.rst
@@ -0,0 +1,29 @@
+`utils` module
+==============
+
+The `utils` module contains utility functions and classes to assist in
+|porchlight|'s internal functioning.
+
+.. automodule:: porchlight.utils
+ :members:
+ :show-inheritance:
+ :private-members:
+
+Inspection utilities
+--------------------
+
+.. automodule:: porchlight.utils.inspect_functions
+ :members:
+ :show-inheritance:
+ :private-members:
+
+Typing utilities
+----------------
+
+.. automodule:: porchlight.utils.typing_functions
+ :members:
+ :show-inheritance:
+ :private-members:
+
+
+.. |porchlight| replace:: **porchlight**
diff --git a/porchlight/door.py b/porchlight/door.py
index da19244..5824efb 100644
--- a/porchlight/door.py
+++ b/porchlight/door.py
@@ -421,9 +421,97 @@ class Door(BaseDoor):
"""Inherits from and extends :class:`~porchlight.door.BaseDoor`"""
def __init__(
- self, function: Callable = None, *, argument_mapping: dict = {}
+ self,
+ function: Callable = None,
+ *,
+ argument_mapping: dict = {},
+ wrapped: bool = False,
+ arguments: dict = {},
+ keyword_args: dict = {},
+ return_vals: List = [],
+ name: str = "",
+ typecheck: bool = False,
):
+ """Initializes the :py:class:`~porchlight.door.Door` object using a
+ callable.
+
+ Arguments
+ ---------
+
+ function : Callable
+ A callable object to be parsed by :py:class:`~BaseDoor`.
+
+ argument_mapping : dict, keyword-only, optional
+ Maps parameters automatically by name. For example, to have a Door
+ accept "a" and "b" ans arguments instead of "x" and "y", one could
+ use
+
+ .. code-block:: python
+
+ def fxn(x):
+ y = 2 * x
+ return y
+
+ my_door = Door(fxn, argument_mapping={'x': 'a', 'y': 'b'})
+
+ to accomplish what would otherwise require wrapping the function
+ yourself.
+
+ wrapped : bool, keyword-only, optional
+ If `True`, will not parse the function using
+ :py:class:`~porchlight.door.BaseDoor`. Instead, it will take user
+ arguments and generate a function wrapper using the following
+ keyword-only arguments:
+
+ - arguments
+ - keyword_args
+ - return_vals
+
+ And this wrapper will be used to initialize the
+ :py:class:`~porchlight.door.BaseDoor` properties.
+
+ arguments : dict, keyword-only, optional
+ Arguments to be passed to the function if it is wrapped. Does not
+ override :py:class:`~porchlight.door.BaseDoor` if ``wrapped`` is
+ ``False``.
+
+ keyword_args : dict, keyword-only, optional
+ Corresponds to keyword arguments that may be passed positionally.
+ Only used when ``wrapped`` is ``True``.
+
+ name : str, keyword-only, optional
+ Overrides the default name for the Door if provided.
+
+ typecheck : :py:obj:`bool`, optional
+ If `True`, the `Door` object will assert that arguments passed
+ to `__call__` (when the `Door` itself is called like a
+ function) have the type expected by type annotations and any user
+ specifications. By default, this is `True`.
+ """
self.argmap = argument_mapping
+ self.name = name
+ self.wrapped = wrapped
+ self.typecheck = typecheck
+
+ if name:
+ self.__name__ = name
+
+ # For wrapped functions, circumvent normal initialization.
+ if self.wrapped:
+ self._initialize_wrapped_function(
+ arguments, keyword_args, return_vals
+ )
+
+ # In these cases, there's no reason to use a decorator.
+ if function is None:
+ msg = "Auto-wrapped functions must be passed directly."
+
+ logger.error(msg)
+ raise DoorError(msg)
+
+ self._base_function = function
+
+ return
self.function_initialized = False
if function is None:
@@ -431,6 +519,23 @@ def __init__(
self.__call__(function)
+ def _initialize_wrapped_function(
+ self, arguments, keyword_args, return_vals
+ ):
+ """Initializes a function that is auto-wrapped by
+ :py:class:`~porchlight.door.Door` instead of being passed to
+ :py:class:`~porchlight.door.BaseDoor`
+ """
+ if not self.name:
+ self.name = "AutoWrappedFunctionDoor"
+ self.__name__ = self.name
+
+ self.arguments = arguments
+ self.keyword_args = keyword_args
+ self.return_vals = return_vals
+
+ self.function_initialized = True
+
def __call__(self, *args, **kwargs):
if not self.function_initialized:
# Need to recieve the function.
@@ -449,7 +554,9 @@ def __call__(self, *args, **kwargs):
raise TypeError(msg)
function = args[0]
- super().__init__(function)
+
+ super().__init__(function, typecheck=self.typecheck)
+
self.function_initialized = True
# Perform any necessary argument mapping.
@@ -457,6 +564,10 @@ def __call__(self, *args, **kwargs):
return self
+ if self.wrapped:
+ result = self._base_function(*args, **kwargs)
+ return result
+
# Check argument mappings.
if not self.argmap:
# Just pass arguments normally
@@ -508,13 +619,16 @@ def map_arguments(self):
logger.error(msg)
raise DoorError(msg)
- if old_name not in self.arguments:
+ if old_name not in self.arguments and not any(
+ old_name in retvals for retvals in self.return_vals
+ ):
msg = f"{old_name} is not a valid argument for {self.name}"
logger.error(msg)
raise DoorError(msg)
- self.arguments[mapped_name] = self.arguments[old_name]
- del self.arguments[old_name]
+ if old_name in self.arguments:
+ self.arguments[mapped_name] = self.arguments[old_name]
+ del self.arguments[old_name]
# Change keyword arguments as well.
if old_name in self.keyword_args:
diff --git a/porchlight/tests/test_door.py b/porchlight/tests/test_door.py
index 6f74c77..99bef27 100644
--- a/porchlight/tests/test_door.py
+++ b/porchlight/tests/test_door.py
@@ -10,6 +10,8 @@
import typing
import logging
import os
+import math
+import random
logging.basicConfig(filename=f"{os.getcwd()}/porchlight_unittest.log")
@@ -59,12 +61,20 @@ def test_fxn(x: int) -> int:
y = 2 * x
return y
- door = Door(test_fxn)
+ door = Door(test_fxn, typecheck=True)
# Call the Door with erroneous types based on annotations.
with self.assertRaises(ParameterError):
door(x="6")
+ # Typechecking off
+ door = Door(test_fxn, typecheck=False)
+ door(x="6")
+
+ # Default functionality should be typechecking off
+ door = Door(test_fxn)
+ door(x=[6])
+
def test_required_arguments(self):
# This property is critical for the functioning of the Neighborhood
# object, since it determines what areguments are passed and *how* they
@@ -315,6 +325,82 @@ def test1(x, y: int, z=1) -> int:
# Changing the argument mapping with the setter.
test1.argument_mapping = {"hello_again": "x", "world_two": "z"}
+ def test_argument_mapping_return_values(self):
+ # Below works as expected. The return value is visible as 'why'.
+ @Door(argument_mapping={"ecks": "x", "why": "y"})
+ def test1(x, y):
+ y = x + 1
+ return y
+
+ # This raises a DoorError
+ @Door(argument_mapping={"ecks": "x", "why": "y"})
+ def test2(x):
+ y = x + 1
+ return y
+
+ # These tests should work exactly the same in terms of input/output.
+ def testval():
+ return random.randint(-10, 10)
+
+ tests = [[testval(), testval()] for _ in range(100)]
+
+ for x, y in tests:
+ self.assertEqual(test1(x, y), test2(x))
+
+ def test_auto_wrapping(self):
+ # Should work for any type of callable.
+ def my_func(x: int) -> int:
+ y = x + 1
+ return y
+
+ tests = [
+ (
+ my_func,
+ {"arguments": {"hello": int}, "return_vals": ["world"]},
+ ),
+ (
+ lambda x: x + 1,
+ {"arguments": {"x": int}, "return_vals": ["y"]},
+ ),
+ (
+ math.cos,
+ {
+ "arguments": {"theta": int},
+ "return_vals": ["cos_theta"],
+ },
+ ),
+ ]
+
+ for fxn, kwargs in tests:
+ my_door = Door(fxn, wrapped=True, **kwargs)
+
+ for arg, value in kwargs.items():
+ self.assertEqual(getattr(my_door, arg), value)
+
+ # Actually run the Door
+ my_door = Door(my_func, wrapped=True, **tests[0][1])
+ expected_output = [my_func(x) for x in range(10)]
+ output = [my_door(x) for x in range(10)]
+
+ self.assertEqual(expected_output, output)
+
+ # Test a slightly more complicated door.
+ def test1(x: int, *, y=0) -> int:
+ z = x ** y
+ return z
+
+ kwargs = {
+ "arguments": {"x": int},
+ "keyword_args": {"y": 0},
+ "return_vals": ["z"],
+ }
+
+ normal_door = Door(test1)
+ wrapped_door = Door(test1, wrapped=True, **kwargs)
+
+ self.assertEqual(normal_door(5), wrapped_door(5))
+ self.assertEqual(normal_door(-500, y=2), wrapped_door(-500, y=2))
+
if __name__ == "__main__":
unittest.main()
diff --git a/porchlight/utils/inspect_functions.py b/porchlight/utils/inspect_functions.py
index d08ce8f..0c789a2 100644
--- a/porchlight/utils/inspect_functions.py
+++ b/porchlight/utils/inspect_functions.py
@@ -1,4 +1,4 @@
-"""Tools for introspection of functions extending what :py:module:`inspect` can
+"""Tools for introspection of functions extending what :py:mod:`inspect` can
do.
"""
import inspect
diff --git a/pyproject.toml b/pyproject.toml
index 57d2c7f..9bdd9c3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "porchlight"
-version = "0.3.1"
+version = "0.4.0"
description = "A function-managing package for models and systems with shared variables."
authors = ["Teal, D "]
license = "GNU General Public License v3.0 or later"