diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 89368b2..b1ce73b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/psf/black - rev: 21.12b0 + rev: 22.12.0 hooks: - id: black args: @@ -30,5 +30,5 @@ repos: - id: flake8 args: - "--max-line-length=79" - - "--max-complexity=18" + - "--max-complexity=10" - "--ignore=F401,W503" diff --git a/docs/conf.py b/docs/conf.py index dd2833f..b2616fd 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.4.0" +release = "0.5.0" # -- General configuration --------------------------------------------------- diff --git a/docs/other/quickstart.rst b/docs/other/quickstart.rst index 7af4826..bb4a503 100644 --- a/docs/other/quickstart.rst +++ b/docs/other/quickstart.rst @@ -298,21 +298,6 @@ Closing Nuances - |Neighborhood| objects will execute their functions sequentially, in the order they are added. if you'd like to re-order the functions before execution, see :py:meth:`~porchlight.neighborhood.Neighborhood.order_doors`. -- As of v0.4.0, there are a number of `known bugs - `_. - In particular, there is an issue with some special functions being imported. - You can readily circumvent this by writing a basic wrapper: - -.. code-block:: python - - from numpy import cos # ufuncs aren't supported - from porchlight import Door - - @Door - def my_cos(x): - '''Replace x and y with whatever variables you need.''' - y = cos(x) - return y - |porchlight| is under active development. The current development strategy will not include a dedicated stable branch until v1.0.0. That means that you diff --git a/porchlight/__init__.py b/porchlight/__init__.py index 008e7b6..a562524 100644 --- a/porchlight/__init__.py +++ b/porchlight/__init__.py @@ -1,10 +1,11 @@ -# Initialize logging -import logging import os from .door import Door from .neighborhood import Neighborhood from .param import Param -logging.basicConfig(filename=f"{os.getcwd()}/porchlight.log") -loggers = logging.getLogger(__name__) + +# Initialize logging +import logging + +logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/porchlight/door.py b/porchlight/door.py index 5824efb..8daf850 100644 --- a/porchlight/door.py +++ b/porchlight/door.py @@ -1,9 +1,7 @@ """ .. |Basedoor| replace:: :py:class:`~porchlight.door.BaseDoor` """ -import functools import inspect -import itertools import re import types @@ -15,6 +13,7 @@ import typing from typing import Any, Callable, Dict, List, Type +import warnings import logging logger = logging.getLogger(__name__) @@ -24,6 +23,10 @@ class DoorError(Exception): pass +class DoorWarning(Warning): + pass + + class BaseDoor: """Contains the basic information about a function such as expected arguments, type annotations, and named return values. @@ -43,23 +46,28 @@ class BaseDoor: arguments without a default value are assigned a :class:~porchlight.param.Empty` value instead of their default value. - max_n_return : :py:obj:`int` - Maximum number of returned values. - - min_n_return : :py:obj:`int` - Minimum number of returned values. - n_args : :py:obj:`int` Number of arguments accepted by this `BaseDoor` name : :py:obj:`str` The name of the function as visible from the base function's __name__. - return_types : :py:obj:`list` of :py:obj:`list` of `~typing.Type` + return_types : :py:obj:`dict` of :py:obj:`str`, :py:obj:`Type` pairs. Values returned by any return statements in the base function. - return_vals : :py:obj:`list` of :py:obj:`list` of :py:obj:`str` - Values returned by any return statements in the base function. + return_vals : :py:obj:`list` of :py:obj:`str` + Names of parameters returned by the base function. Any return + statements in a Door much haveidentical return parameters. I.e., the + following would fail if imported as a Door. + + .. code-block:: python + + def fxn(x): + if x < 1: + y = x + 1 + return x, y + + return x typecheck : :py:obj:`bool` If True, when arguments are passed to the `BaseDoor`'s base function @@ -74,12 +82,10 @@ class BaseDoor: _base_function: Callable arguments: Dict[str, Type] keyword_args: Dict[str, Any] - max_n_return: int - min_n_return: int n_args: int name: str - return_types: List[List[Type]] - return_vals: List[List[str]] + return_types: List[Type] + return_vals: List[str] typecheck: bool def __init__( @@ -117,6 +123,8 @@ def __init__( self.typecheck = typecheck self._inspect_base_callable() + logging.debug(f"Door {self.name} initialized.") + def __eq__(self, other) -> bool: """Equality is defined as referencing the same base function.""" if isinstance(other, BaseDoor) and self.name is other.name: @@ -149,13 +157,22 @@ def _inspect_base_callable(self): # arguments in the end. function = get_wrapped_function(self._base_function) - self.name = function.__name__ - self.__name__ = function.__name__ + # Name may be otherwise assigned, this is a safe way to ensure that + # does not get overwritten. + if not hasattr(self, "name") or not self.name: + self.name = function.__name__ + self.__name__ = function.__name__ + + else: + logging.debug(f"Ignoring name assignment for {self.name}") + self.arguments = {} self.positional_only = [] self.keyword_args = {} self.keyword_only_args = {} + # Attempting to retrieve type hints for the return value. This *does + # not* fail if they aren't found. try: ret_type_annotation = typing.get_type_hints(function)["return"] self.return_types = decompose_type( @@ -229,12 +246,36 @@ def _inspect_base_callable(self): for name, _type in self.arguments.items(): if _type == inspect._empty: - self.arguments[name] = Empty + self.arguments[name] = Empty() self.n_args = len(self.arguments) # The return values require some more effort. - self.return_vals = self._get_return_vals(function) + return_vals = self._get_return_vals(function) + + # porchlight >=v0.5.0 requires that return_vals be a single, non-nested + # list of return values that are uniform across return statements. + for i, ret_list in enumerate(return_vals): + if any(ret_list != rl for rl in return_vals): + msg = ( + f"Door objects do not allow for multiple return sets " + f"within the same function. That is, a function must " + f"always return the same set of parameters. But, " + f"{function.__name__} has return values:\n" + ) + + for i, rl in enumerate(return_vals): + msg += f" {i}) {', '.join(rl)}" + + logging.error(msg) + + raise DoorError(msg) + + if return_vals: + self.return_vals = return_vals[0] + + else: + self.return_vals = return_vals logger.debug(f"Found {self.n_args} arguments in {self.name}.") @@ -262,9 +303,6 @@ def __call__(self, *args, **kwargs): # Type checking. if self.typecheck: for k, v in input_kwargs.items(): - if self.arguments[k] == Empty: - continue - if not isinstance(v, self.arguments[k]): msg = ( f"Type checking is on, and the type for input " @@ -328,6 +366,7 @@ def _get_return_vals(function: Callable) -> List[str]: # definition. defmatch_str = r"^(\ )+def\s+" retmatch_str = r".*\s+return\s(.*)" + retmatch_str = r"^\s+(?:return|yield)\s(.*)" indentmatch_str = r"^(\s)*" for i, line in enumerate(lines): @@ -384,13 +423,25 @@ def _get_return_vals(function: Callable) -> List[str]: vals = [v.strip() for v in vals] + # Checks to ensure it's not just a bunch of/one empty string, + # which just implies that the line is: + # return + # + # While this could be applied to vals, it could obfuscate the + # error that *must* occur in those cases, which is a + # SyntaxError. Trusting the parser here. + if not [v for v in vals if v != ""]: + # This is empty. + vals = [] + for val in vals: if not re.match(r"\w+$", val): # This is undefined, not an error. So assign return # value 'undefined' for this return statement and issue # a warning. source_file = inspect.getfile(function) - logger.warning( + + msg = ( f"Could not define any set of return variable " f"names for the following return line: \n" f"{source_file}: {start_line+i}) " @@ -400,6 +451,10 @@ def _get_return_vals(function: Callable) -> List[str]: f"callable." ) + logger.warning(msg) + + warnings.warn(msg, DoorWarning) + vals = [] break @@ -642,10 +697,10 @@ def map_arguments(self): del self.keyword_args[old_name] # Also change outputs that contain the same name. - for i, ret_tuple in enumerate(self.return_vals): - for j, ret_val in enumerate(ret_tuple): - if old_name == ret_val: - self.return_vals[i][j] = mapped_name + ret_tuple = self.return_vals + for i, ret_val in enumerate(ret_tuple): + if old_name == ret_val: + self.return_vals[i] = mapped_name # Place back in the original order. rev_argmap = {v: k for k, v in self.argmap.items()} @@ -667,6 +722,8 @@ def _check_argmap(argmap): exception if it is invalid. Will also raise warnings for certain non-fatal actions. """ + builtin_set = set(bi for bi in __builtins__.keys()) + for key, value in argmap.items(): # Argument map should contain valid python variable names. if not re.match(r"^[a-zA-Z_]([a-zA-Z0-9_])*$", key): @@ -674,6 +731,16 @@ def _check_argmap(argmap): logging.error(msg) raise DoorError(msg) + if key in builtin_set: + msg = f"Key {key} matches built-in name." + logger.warning(msg) + warnings.warn(msg, DoorWarning) + + if value in builtin_set: + msg = f"Mapping arg {value} matches global name." + logger.warning(msg) + warnings.warn(msg, DoorWarning) + def __repr__(self): return super().__repr__().replace("BaseDoor", "Door") @@ -710,10 +777,9 @@ def original_return_vals(self): return_vals = copy.copy(self.return_vals) # Also change outputs that contain the same name. - for i, ret_tuple in enumerate(self.return_vals): - for j, ret_val in enumerate(ret_tuple): - if ret_val in self.argmap: - return_vals[i][j] = self.argmap[ret_val] + for i, ret_val in enumerate(return_vals): + if ret_val in self.argmap: + return_vals[i] = self.argmap[ret_val] return return_vals @@ -738,9 +804,8 @@ def variables(self) -> List[str]: all_vars.append(arg) for ret in self.return_vals: - for r in ret: - if r not in all_vars: - all_vars.append(r) + if ret not in all_vars: + all_vars.append(ret) return all_vars @@ -750,7 +815,7 @@ def required_arguments(self) -> List[str]: required = [] for x in self.arguments: - if isinstance(self.keyword_args[x].value, Empty): + if self.keyword_args[x].value == Empty(): required.append(x) return required diff --git a/porchlight/neighborhood.py b/porchlight/neighborhood.py index 5ab922b..31cfec8 100644 --- a/porchlight/neighborhood.py +++ b/porchlight/neighborhood.py @@ -36,12 +36,10 @@ class Neighborhood: """ _doors: Dict[str, door.Door] - _dynamic_doors: set[str] _params: Dict[str, param.Param] _call_order: List[str] def __init__(self, initial_doors: List[Callable] = []): - """Initializes the Neighborhood object.""" self._doors = {} self._params = {} self._call_order = [] @@ -53,13 +51,18 @@ def __init__(self, initial_doors: List[Callable] = []): else: self.add_function(d) + # Logging + msg = ( + f"Neighborhood initialized with {len(self._doors)} " + f"doors/functions." + ) + logger.debug(msg) def __repr__(self): - """Must communicate teh following: - + A unique identifier. - + List of doors - + list of tracked parameters and their values. - """ + # Must communicate the following: + # + A unique identifier. + # + List of doors + # + list of tracked parameters and their values. info = { "doors": self.doors, "params": self.params, @@ -98,6 +101,7 @@ def add_function( object. """ new_door = door.Door(function) + logging.debug(f"Adding new function to neighborhood: {new_door.name}") self.add_door(new_door, overwrite_defaults, dynamic_door) @@ -115,9 +119,9 @@ def add_door( :class:`~porchlight.door.DynamicDoor`, or :py:obj:`list` of :class:`~porchlight.door.Door` objects. - Either a single initialized - `door.Door` object or a list of them. If a list is provided, this - function is called for each item in the list. + Either a single initialized `door.Door` object or a list of them. + If a list is provided, this function is called for each item in the + list. overwrite_defaults : bool, optional If `True`, will overwrite any parameters shared between the @@ -164,17 +168,16 @@ def add_door( # Add all return values as parameters. if not dynamic_door: - for pname in [p for rvs in new_door.return_vals for p in rvs]: + for pname in new_door.return_vals: if pname not in self._params: self._params[pname] = param.Param(pname, param.Empty()) return # Dynamic doors must be specified separately. They get initialized when - # first modified. + # first called/explicitly generated. # - # Dynamic doors must also be type-annotated. If they are not raise an - # error. + # Dynamic doors must also be type-annotated. if ( "return_types" not in new_door.__dict__ or not new_door.return_types @@ -191,7 +194,7 @@ def add_door( for i, rt in enumerate(return_types): if isinstance(rt, door.Door) or rt is door.Door: - ret_val = new_door.return_vals[0][i] + ret_val = new_door.return_vals[i] if ret_val not in self.doors: # Can define a function that just pulls out the attr # independent of its current reference. @@ -206,7 +209,7 @@ def template_ddoor(param_name): template_ddoor.generator_kwargs = {"param_name": ret_val} self.add_door(template_ddoor) - self.add_param(ret_val, param.Empty) + self.add_param(ret_val, param.Empty()) def remove_door(self, name: str): """Removes a :class:`~porchlight.door.Door` from :attr:`_doors`. @@ -386,26 +389,25 @@ def call_all_doors(self): input_params[pname] = self._params[pname].value # Run the cur_door object and catch its output. + logging.debug(f"Calling door {cur_door.name}.") output = cur_door(**input_params) # Check if the cur_door has a known return value. if not cur_door.return_vals: + logging.debug("No return value found.") continue - elif len(cur_door.return_vals[0]) > 1: - # This only works for functions with one possible output. This, - # frankly, should probably be the case nearly all of the time. - # Still need to make a call on if there's support in a subset - # of cases. See issue #19 for updates. + elif len(cur_door.return_vals) > 1: update_params = { - v: x for v, x in zip(cur_door.return_vals[0], output) + v: x for v, x in zip(cur_door.return_vals, output) } else: - assertmsg = "Mismatched output/return." - assert len(cur_door.return_vals) == 1, assertmsg - update_params = {cur_door.return_vals[0][0]: output} + update_params = {cur_door.return_vals[0]: output} + logging.debug(f"Updating parameters: {list(update_params.keys())}") + + # Update all parameters to reflect the next values. for pname, new_value in update_params.items(): # If the parameter is currently empty, just reassign and # continue. This refreshes the type value of the parameter @@ -422,9 +424,7 @@ def call_all_doors(self): logger.error(msg) raise param.ParameterError(msg) - # Editing the _value directly here... but I'm not sure if - # that's the best idea. - self._params[pname]._value = new_value + self._params[pname].value = new_value def run_step(self): """Runs a single step forward for all functions, in specified order, @@ -460,6 +460,9 @@ def order_doors(self, order: List[str]): The order for doors to be called in. Each `str` must correspond to a key in `Neighborhood._doors`. """ + # The order list must: + # + Contain all doors once. + # + All doors must already exist and be known. if not order: msg = f"Empty or invalid input: {order}." logger.error(msg) @@ -480,6 +483,8 @@ def order_doors(self, order: List[str]): logger.error(msg) raise KeyError(msg) + logging.debug(f"Adjusting call order: {self._call_order} -> {order}") + self._call_order = order @property diff --git a/porchlight/param.py b/porchlight/param.py index 7908c1c..10195d2 100644 --- a/porchlight/param.py +++ b/porchlight/param.py @@ -12,16 +12,21 @@ class ParameterError(Exception): class Empty: - """An empty class representing missing parameters values.""" + """An empty class representing missing parameters values. - def __init__(self): - pass + At initializtion, if an instance does not already exist it is created. + """ + + def __new__(cls): + # Return the singleton instance if it exists already, otherwise become + # the singleton instance. + if not hasattr(cls, "_singleton_instance"): + cls._singleton_instance = super(Empty, cls).__new__(cls) + + return cls._singleton_instance def __eq__(self, other): - """Force Equality of this special value regardless of whether it is - initialized or not. - """ - if isinstance(other, Empty) or other == Empty: + if other is self: return True else: @@ -100,7 +105,7 @@ def __repr__(self): "type": self.type, } - infostrings = [f"{key}={value}" for key, value in info.items()] + infostrings = [f"{key}={repr(value)}" for key, value in info.items()] outstr = f"Param({', '.join(infostrings)})" diff --git a/porchlight/tests/test_basedoor.py b/porchlight/tests/test_basedoor.py index 06f26ec..782c6f9 100644 --- a/porchlight/tests/test_basedoor.py +++ b/porchlight/tests/test_basedoor.py @@ -24,7 +24,7 @@ def test_fxn(x: int) -> int: # Must contain both input and output parameter. arguments = ["x"] keyword_args = ["x"] - return_vals = [["y"]] + return_vals = ["y"] # Not comparing any values during this test. for arg in arguments: @@ -51,7 +51,7 @@ def fxn_use_decorator(x): self.assertEqual(fxn_use_decorator.name, "fxn_use_decorator") - self.assertEqual(fxn_use_decorator.arguments, {"x": Empty}) + self.assertEqual(fxn_use_decorator.arguments, {"x": Empty()}) # Test on a decorated function. def test_decorator(fxn): @@ -70,7 +70,7 @@ def test_fxn(x: int) -> int: # Must contain both input and output parameter. arguments = ["x"] keyword_args = ["x"] - return_vals = [["y"]] + return_vals = ["y"] # Not comparing any values during this test. for arg in arguments: @@ -79,8 +79,7 @@ def test_fxn(x: int) -> int: for kwarg in keyword_args: self.assertIn(kwarg, door.keyword_args) - for retval in return_vals: - self.assertIn(retval, door.return_vals) + self.assertEqual(return_vals, door.return_vals) # Call the BaseDoor result = door(x=5) @@ -93,7 +92,7 @@ def test_numpy_ufunc(self): except ModuleNotFoundError as e: # Printing a message and returning - print( + logging.error( f"NOTICE: Could not run test {self.id()}, got " f"ModuleNotFoundError: {e}." ) @@ -190,7 +189,8 @@ def test_oneline(x): def test_oneline_bad(x): return x * 2 - result = BaseDoor._get_return_vals(test_oneline_bad) + with self.assertWarns(door.DoorWarning): + result = BaseDoor._get_return_vals(test_oneline_bad) self.assertEqual(result, []) @@ -311,7 +311,7 @@ def test(x: int) -> int: expected_repr = ( f"BaseDoor(name=test, base_function={str(test)}, " f"arguments={{'x': }}, " - f"return_vals=[['y']])" + f"return_vals=['y'])" ) test_door = BaseDoor(test) @@ -339,7 +339,7 @@ def test1( "pos2": Empty(), "kwpos": Empty(), "kwposdef": Empty(), - "kwonly": Empty, + "kwonly": Empty(), } self.assertEqual(new_door.arguments, expected_arguments) @@ -392,6 +392,47 @@ def test_prop(arg1, arg2, kwarg1=None, kwarg2=["hi"]) -> int: self.assertEqual(test_prop.kwargs, expected_val) + def test_generator(self): + # Tests generator functions with porchlight. + @BaseDoor + def testgen1(x): + y = 0 + + while y <= x: + yield y + y = y + 1 + + return y + + expected = {"arguments": {"x": Empty()}, "return_vals": ["y"]} + + for attr, val in expected.items(): + self.assertEqual(getattr(testgen1, attr), val) + + # Should be identical to something with return instead of yield (for + # porchlight specifically). + @BaseDoor + def test1(x): + y = 0 + + while y <= x: + return y + y = y + 1 + + return y + + test_attrs = [ + "arguments", + "positional_only", + "keyword_args", + "n_args", + "return_types", + "return_vals", + ] + + for attr in test_attrs: + self.assertEqual(getattr(test1, attr), getattr(testgen1, attr)) + if __name__ == "__main__": import unittest diff --git a/porchlight/tests/test_door.py b/porchlight/tests/test_door.py index 99bef27..9428062 100644 --- a/porchlight/tests/test_door.py +++ b/porchlight/tests/test_door.py @@ -4,7 +4,7 @@ from unittest import TestCase import unittest -from porchlight.door import Door, DoorError +from porchlight.door import Door, DoorError, DoorWarning from porchlight.param import Empty, Param, ParameterError import typing @@ -27,7 +27,7 @@ def test_fxn(x: int) -> int: # Must contain both input and output parameter. arguments = ["x"] keyword_args = ["x"] - return_vals = [["y"]] + return_vals = ["y"] # Not comparing any values during this test. for arg in arguments: @@ -36,8 +36,7 @@ def test_fxn(x: int) -> int: for kwarg in keyword_args: self.assertIn(kwarg, door.keyword_args) - for retval in return_vals: - self.assertIn(retval, door.return_vals) + self.assertEqual(return_vals, door.return_vals) # Call the Door result = door(x=5) @@ -54,7 +53,15 @@ def fxn_use_decorator(x): self.assertEqual(fxn_use_decorator.name, "fxn_use_decorator") - self.assertEqual(fxn_use_decorator.arguments, {"x": Empty}) + self.assertEqual(fxn_use_decorator.arguments, {"x": Empty()}) + + # Try manually naming the door. + @Door(name="testname") + def test1(): + pass + + self.assertEqual(test1.__name__, "testname") + self.assertEqual(test1.name, "testname") def test___call__(self): def test_fxn(x: int) -> int: @@ -158,7 +165,7 @@ def test(x: int) -> int: expected_repr = ( f"Door(name=test, base_function={str(test)}, " f"arguments={{'x': }}, " - f"return_vals=[['y']])" + f"return_vals=['y'])" ) test_door = Door(test) @@ -178,7 +185,7 @@ def orig_func(x, y: int = 1): return z, x self.assertEqual(my_func.variables, ["hello", "world", "z"]) - self.assertEqual(my_func.return_vals, [["z", "hello"]]) + self.assertEqual(my_func.return_vals, ["z", "hello"]) self.assertEqual(my_func.required_arguments, ["hello"]) self.assertEqual(my_func.original_arguments, orig_func.arguments) self.assertEqual(my_func.original_return_vals, orig_func.return_vals) @@ -206,7 +213,7 @@ def test2_unmapped( test2_mapped.variables, ["temperature", "pressure", "k_B", "density"], ) - self.assertEqual(test2_mapped.return_vals, [["density"]]) + self.assertEqual(test2_mapped.return_vals, ["density"]) self.assertEqual(test2_mapped.required_arguments, []) self.assertEqual( test2_mapped.original_arguments, test2_unmapped.arguments @@ -231,30 +238,17 @@ def test4(x): self.assertEqual(list(test4.arguments.keys()), ["x"]) def test_mapping_multiple_returns(self): - @Door(argument_mapping={"hello": "x", "bing": "z"}) - def test1(x, y: int, z=1) -> typing.Tuple[int]: - result = True if x < y else False + with self.assertRaises(DoorError): - if result: - return result, x, z + @Door(argument_mapping={"hello": "x", "bing": "z"}) + def test1(x, y: int, z=1) -> typing.Tuple[int]: + result = True if x < y else False - else: - return result, y, z + if result: + return result, x, z - self.maxDiff = None - self.assertEqual( - test1.return_vals, - [["result", "hello", "bing"], ["result", "y", "bing"]], - ) - self.assertEqual(list(test1.arguments.keys()), ["hello", "y", "bing"]) - self.assertEqual( - test1.keyword_arguments, - { - "hello": Param("hello"), - "y": Param("y"), - "bing": Param("bing", 1), - }, - ) + else: + return result, y, z def test_bad_mapping_variable_names(self): bad_names = ("0fign_", " bonk", "this is a sentence...", "there.dot") @@ -281,7 +275,9 @@ def test1(x, y: int, z=1) -> int: test2 = Door(test1) - self.assertEqual(test2.arguments, {"x": Empty, "y": int, "z": Empty}) + self.assertEqual( + test2.arguments, {"x": Empty(), "y": int, "z": Empty()} + ) def test_bad_mapping_bad_functions(self): # A bad argument @@ -314,6 +310,19 @@ def test_bad_mapping_name_err_and_warning(self): def test1(x, y): pass + def test_builtin_mapping_name_warning(self): + with self.assertWarns(DoorWarning): + + @Door(argument_mapping={"type": "x"}) + def test1(x): + pass + + with self.assertWarns(DoorWarning): + + @Door(argument_mapping={"hola": "type"}) + def test2(type): + return + def test_argument_mapping_property(self): @Door(argument_mapping={"hello": "x", "world": "z"}) def test1(x, y: int, z=1) -> int: @@ -401,6 +410,14 @@ def test1(x: int, *, y=0) -> int: self.assertEqual(normal_door(5), wrapped_door(5)) self.assertEqual(normal_door(-500, y=2), wrapped_door(-500, y=2)) + # Auto-wrapping cannot be used with decorators. + with self.assertRaises(DoorError): + + @Door(wrapped=True, **kwargs) + def test2(x: int, *, y=0) -> int: + z = x ** y + return z + if __name__ == "__main__": unittest.main() diff --git a/porchlight/tests/test_dynamicdoor.py b/porchlight/tests/test_dynamicdoor.py index d046852..c6c2a14 100644 --- a/porchlight/tests/test_dynamicdoor.py +++ b/porchlight/tests/test_dynamicdoor.py @@ -95,7 +95,7 @@ def my_door(x: int, y: int = 1) -> int: self.assertEqual(doorgen1(2, 5), 2 ** 5) self.assertEqual(doorgen1.arguments, {"hello": int, "y": int}) - self.assertEqual(doorgen1.return_vals, [["z"]]) + self.assertEqual(doorgen1.return_vals, ["z"]) @door.Door(argument_mapping={"hello": "x"}) def doorgen2(x: int, y: int = 1) -> door.Door: diff --git a/porchlight/tests/test_neighborhood.py b/porchlight/tests/test_neighborhood.py index 612a77c..018e012 100644 --- a/porchlight/tests/test_neighborhood.py +++ b/porchlight/tests/test_neighborhood.py @@ -31,7 +31,7 @@ def test2(y: int) -> int: neighborhood = Neighborhood([test1, test2]) - self.assertEqual(list(neighborhood.params.keys()), ['x', 'y', 'z']) + self.assertEqual(list(neighborhood.params.keys()), ["x", "y", "z"]) def test___repr__(self): neighborhood = Neighborhood() @@ -48,15 +48,15 @@ def test1(x: int) -> int: # Keeping the below as an example of what the string looks like. Update # this to match the actual implementation as changes occur below. # expected = ( - # "Neighborhood(doors={'test1': Door(name=test1, " - # "base_function=, " - # "arguments={'x': }, return_vals=[['y']])}, " - # "params={'x': Param(name=x, value=, constant=False, type=), 'y': Param(name=" - # "y, value=, " - # "constant=False, type=)}, call_order=['test1'])" + # Neighborhood(doors={'test1': Door(name=test1, + # base_function=.test1 at 0x7fdcf1333640>, arguments={'x': }, + # return_vals=[['y']])}, params={'x': Para m(name=x, + # value=, + # constant=False, type=), 'y': + # Param(name=y, value=, constant= False, type=)}, call_order=['test1']) # ) params = neighborhood.params @@ -66,7 +66,6 @@ def test1(x: int) -> int: f"params={params}, call_order=['test1'])" ) - print(repr(neighborhood)) self.assertEqual(repr(neighborhood), expected) def test_add_function(self): @@ -468,7 +467,9 @@ def fxn_two(y): neighborhood = Neighborhood() neighborhood.add_function(fxn_one) - neighborhood.add_function(fxn_two) + + with self.assertWarns(door.DoorWarning): + neighborhood.add_function(fxn_two) # Provide the required first arg, x neighborhood.set_param("x", 0) diff --git a/porchlight/tests/test_param.py b/porchlight/tests/test_param.py index a1dcc97..f5e4fc3 100644 --- a/porchlight/tests/test_param.py +++ b/porchlight/tests/test_param.py @@ -55,7 +55,7 @@ def test___eq__(self): def test___repr__(self): param1 = param.Param("x", 1) expected_string = ( - "Param(name=x, value=1, constant=False, " "type=)" + "Param(name='x', value=1, constant=False, " "type=)" ) self.assertEqual(repr(param1), expected_string) @@ -103,8 +103,7 @@ class TestEmpty(TestCase): def test___eq__(self): empty = param.Empty() - self.assertTrue(empty, param.Empty) - self.assertTrue(empty, param.Empty()) + self.assertEqual(empty, param.Empty()) self.assertNotEqual(empty, 1) diff --git a/porchlight/tests/test_utils_inspect_functions.py b/porchlight/tests/test_utils_inspect_functions.py index 6ac3de4..e92bb9d 100644 --- a/porchlight/tests/test_utils_inspect_functions.py +++ b/porchlight/tests/test_utils_inspect_functions.py @@ -154,6 +154,16 @@ def test4_2(): self.assertEqual(result[0], expected_result) + def test_non_callable(self): + tests = [1, "2", {3: 4}] + + for test in tests: + with self.assertRaises(TypeError): + inspect_functions.get_all_source(test) + + with self.assertRaises(TypeError): + inspect_functions.get_wrapped_function(test) + if __name__ == "__main__": unittest.main() diff --git a/porchlight/utils/typing_functions.py b/porchlight/utils/typing_functions.py index 0a6dd6d..2ce380e 100644 --- a/porchlight/utils/typing_functions.py +++ b/porchlight/utils/typing_functions.py @@ -28,7 +28,6 @@ def decompose_type( are ignored. These are only for *resolvable* types at return, such as Tuples, Lists, and Iterables. Callables are excluded by default. """ - all_types = [] # Check that typevar is not one of interest to any of the types in diff --git a/pyproject.toml b/pyproject.toml index 9bdd9c3..a38fbc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "porchlight" -version = "0.4.0" +version = "0.5.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"