From fb14029b4883fbe95c625008ea25cd2158e04a2d Mon Sep 17 00:00:00 2001 From: "Teal, D" Date: Mon, 3 Apr 2023 12:29:16 -0700 Subject: [PATCH 01/15] Adding warning assertion in neighborhood testing. --- porchlight/tests/test_neighborhood.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/porchlight/tests/test_neighborhood.py b/porchlight/tests/test_neighborhood.py index c08aa4a..17fd7c7 100644 --- a/porchlight/tests/test_neighborhood.py +++ b/porchlight/tests/test_neighborhood.py @@ -756,18 +756,21 @@ def inittest4(arg, kwarg=12): retval_needed = f"{kwarg}: {arg}" return retval_needed + # This warns the user about the invalid return argument that will be + # ignored. neighborhood_4 = Neighborhood( initialization=[inittest1, inittest2, inittest3, inittest4] ) neighborhood_4.add_param("retval_needed", None) - with self.assertRaises(KeyError): + with self.assertWarns(door.DoorWarning), self.assertRaises(KeyError): neighborhood_4.run_step() neighborhood_4.add_param("arg", "Hello world") - neighborhood_4.run_step() + with self.assertWarns(door.DoorWarning): + neighborhood_4.run_step() def test_plain_finalization(self): # Testing specifically a None-returning function. From 1fa65ff0cebf59d6f17b5f7d16caf337f55da9ab Mon Sep 17 00:00:00 2001 From: Teal Date: Tue, 11 Apr 2023 19:00:12 -0700 Subject: [PATCH 02/15] Update README.md --- porchlight/tests/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/porchlight/tests/README.md b/porchlight/tests/README.md index 093b72f..acb86b7 100644 --- a/porchlight/tests/README.md +++ b/porchlight/tests/README.md @@ -1,6 +1,6 @@ Testing `porchlight` -------------------- -To run the following tests, ensure you are in the root porchlight directory -above this one (`../..` from the dir containing this file) and run the command: +To run all tests, ensure you are in the root porchlight directory +above this one and run the command: `python -m unittest`. From fe5912658d6b762315261d22923ee1fbfdfaf6a3 Mon Sep 17 00:00:00 2001 From: "Teal, D" Date: Wed, 12 Apr 2023 12:53:47 -0700 Subject: [PATCH 03/15] Adding door module docstring. --- porchlight/door.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/porchlight/door.py b/porchlight/door.py index 3118953..762a6ae 100644 --- a/porchlight/door.py +++ b/porchlight/door.py @@ -1,4 +1,8 @@ -""" +"""Function adapter classes for callable Python objects. + +This module contains definitions for BaseDoor, Door, and DynamicDoor. It also +defines the DoorError exception and DoorWarning warning. + .. |Basedoor| replace:: :py:class:`~porchlight.door.BaseDoor` """ import inspect @@ -58,7 +62,7 @@ class BaseDoor: 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. + following would fail if used to initialize a Door. .. code-block:: python @@ -77,6 +81,12 @@ def fxn(x): _base_function : :py:obj:`~typing.Callable` This holds a reference to the function being managed by the `BaseDoor` instance. + + Notes + ----- + + In version 2.0, this class will be permanently renamed or refactored. It + is strongly suggested you use |Door| or |PorchlightAdapter| unless + absolutely necessary, as that will remain compatible and unchanged. """ _base_function: Callable From 420c93a05684a9344c810203a048e56f530c937d Mon Sep 17 00:00:00 2001 From: "Teal, D" Date: Wed, 12 Apr 2023 13:58:42 -0700 Subject: [PATCH 04/15] Updating door module docstring. --- porchlight/door.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/porchlight/door.py b/porchlight/door.py index 762a6ae..7efdb21 100644 --- a/porchlight/door.py +++ b/porchlight/door.py @@ -3,6 +3,10 @@ This module contains definitions for BaseDoor, Door, and DynamicDoor. It also defines the DoorError exception and DoorWarning warning. +These objects all take a python callable object in some form, extract metadata +from the object (if possible), and provide a calling interface with optional +checks and actions (see individual descriptions). + .. |Basedoor| replace:: :py:class:`~porchlight.door.BaseDoor` """ import inspect @@ -32,7 +36,7 @@ class DoorWarning(Warning): class BaseDoor: - """Contains the basic information about a function such as expected + """Contains basic information about a function such as expected arguments, type annotations, and named return values. Attributes @@ -85,8 +89,9 @@ def fxn(x): Notes ----- + In version 2.0, this class will be permanently renamed or refactored. It - is strongly suggested you use |Door| or |PorchlightAdapter| unless - absolutely necessary, as that will remain compatible and unchanged. + is strongly suggested you use `Door` or `PorchlightAdapter` unless + absolutely necessary, as those will remain forward and backward + compatible across the 2.0 update. """ _base_function: Callable @@ -136,7 +141,9 @@ def __init__( logging.debug(f"Door {self.name} initialized.") def __eq__(self, other) -> bool: - """Equality is defined as referencing the same base function.""" + """BaseDoor equality is defined as referencing the same base + function. + """ if isinstance(other, BaseDoor) and self.name is other.name: return True @@ -384,11 +391,11 @@ def _get_return_vals(function: Callable) -> List[str]: for i, line in enumerate(lines): orig_line = line.strip() - # Remove comments + # Remove comments. if "#" in line: line = line[: line.index("#")] - # Ignore empty lines + # Ignore empty lines. if not line.strip(): continue @@ -398,12 +405,12 @@ def _get_return_vals(function: Callable) -> List[str]: # Ignore empty lines # Check for matches for both, in case there's something like - # def wtf(): return 5 - # Which is atrocious but possible. + # def example(): return 5 + # Which is atrocious but a valid definition. defmatch = re.match(defmatch_str, line) retmatch = re.match(retmatch_str, line) - # Ignore decorators + # Ignore decorators. if re.match(r"^\s*@\w+.*", line): continue From f4a776b166ecba9d39398748fab8656e0e815cce Mon Sep 17 00:00:00 2001 From: "Teal, D" Date: Wed, 12 Apr 2023 17:50:11 -0700 Subject: [PATCH 05/15] Finished first pass of door.py docstrings Included some comment cleanup as well, and have identified a list of things that need to be changed for v1.2. Nothing major so far. --- porchlight/door.py | 164 +++++++++++++++++++++++++-------------------- 1 file changed, 92 insertions(+), 72 deletions(-) diff --git a/porchlight/door.py b/porchlight/door.py index 7efdb21..a2a84a9 100644 --- a/porchlight/door.py +++ b/porchlight/door.py @@ -7,7 +7,15 @@ from the object (if possible), and provide a calling interface with optional checks and actions (see individual descriptions). -.. |Basedoor| replace:: :py:class:`~porchlight.door.BaseDoor` +.. |BaseDoor| replace:: :py:class:`~porchlight.door.BaseDoor` +.. |Door| replace:: :py:class:`~porchlight.door.Door` +.. |DynamicDoor| replace:: :py:class:`~porchlight.door.DynamicDoor` +.. |BasePorchlightAdapter| replace:: + :py:class:`~porchlight.door.BasePorchlightAdapter` +.. |PorchlightAdapter| replace:: + :py:class:`~porchlight.door.PorchlightAdapter` +.. |DynamicPorchlightAdapter| replace:: + :py:class:`~porchlight.door.DynamicPorchlightAdapter` """ import inspect import re @@ -41,29 +49,29 @@ class BaseDoor: Attributes ---------- - arguments : :py:obj:`dict`, :py:obj:`str`: :class:`~typing.Type` - Dictionary of all arguments taken as input when the `BaseDoor` object + arguments : :`dict`, `str`: :class:`~typing.Type` + Dictionary of all arguments taken as input when the |BaseDoor| object is called. - positional_only : :py:obj:`list` of :py:obj:`str` + positional_only : `list` of `str` List of positional-only arguments accepted by the function. - keyword_args : :py:obj:`dict`, :py:obj:`str`: :class:`~typing.Any` - Keyword arguments accepted by the `BaseDoor` as input when called. This + keyword_args : `dict`, `str`: :class:`~typing.Any` + Keyword arguments accepted by the |BaseDoor| as input when called. This includes all arguments that are not positional-only. Positional arguments without a default value are assigned a :class:~porchlight.param.Empty` value instead of their default value. - n_args : :py:obj:`int` - Number of arguments accepted by this `BaseDoor` + n_args : `int` + Number of arguments accepted by this |BaseDoor| - name : :py:obj:`str` + name : `str` The name of the function as visible from the base function's __name__. - return_types : :py:obj:`dict` of :py:obj:`str`, :py:obj:`Type` pairs. + return_types : `dict` of `str`, `Type` pairs. Values returned by any return statements in the base function. - return_vals : :py:obj:`list` of :py:obj:`str` + return_vals : `list` of `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 used to initialize a Door. @@ -77,19 +85,20 @@ def fxn(x): return x - typecheck : :py:obj:`bool` - If True, when arguments are passed to the `BaseDoor`'s base function - the input types are checked against the types in `BaseDoor.arguments`. - If there is a mismatch, a `TypeError` will be thrown. + typecheck : `bool` + If True, when arguments are passed to the |BaseDoor|'s base function + the input types are checked against the types in + :py:attr:`BaseDoor.arguments`. If there is a mismatch, a `TypeError` + will be thrown. - _base_function : :py:obj:`~typing.Callable` - This holds a reference to the function being managed by the `BaseDoor` + _base_function : `~typing.Callable` + This holds a reference to the function being managed by the |BaseDoor| instance. Notes ----- + In version 2.0, this class will be permanently renamed or refactored. It - is strongly suggested you use `Door` or `PorchlightAdapter` unless + is strongly suggested you use |Door| or |PorchlightAdapter| unless absolutely necessary, as those will remain forward and backward compatible across the 2.0 update. """ @@ -109,29 +118,31 @@ def __init__( typecheck: bool = True, returned_def_to_door: bool = False, ): - """Initializes the BaseDoor class. It takes any callable (function, + """Initializes the |BaseDoor| class. It takes any callable (function, lambda, method...) and inspects it to get at its arguments and structure. if typecheck is True (default True), the type of inputs passed to - BaseDoor.__call__ will be checked for matches to known input Types. + :py:class:`BaseDoor.__call__` will be checked for matches to known + input Types. Parameters ---------- function : :py:class:`typing.Callable` The callable/function to be managed by the BaseDoor class. - typecheck : :py:obj:`bool`, optional - If `True`, the `BaseDoor` object will assert that arguments passed - to `__call__` (when the `BaseDoor` itself is called like a - function) have the type expected by type annotations and any user - specifications. By default, this is `True`. + typecheck : `bool`, optional + If `True`, the |BaseDoor| object will assert that arguments passed + to :py:class:`BaseDoor.__call__` (when the |BaseDoor| itself is + called like a function) have the type expected by type annotations + and any user specifications. By default, this is `True`. - returned_def_to_door : :py:obj:`bool`, optional + returned_def_to_door : `bool`, optional Returns a Door generated from the output of the base function. - Note, this is not the same as a DynamicDoor, and internal - variables/updating is not handled as with a DynamicDoor. This just - calls Door's initializer on the output of the base function. + Note, this is not the same as a |DynamicDoor|, and internal + variables/updating is not handled as with a |DynamicDoor|. This + just creates a new |Door| instance using the output of the base + function. """ self._returned_def_to_door = returned_def_to_door self._base_function = function @@ -141,7 +152,7 @@ def __init__( logging.debug(f"Door {self.name} initialized.") def __eq__(self, other) -> bool: - """BaseDoor equality is defined as referencing the same base + """|BaseDoor| equality is defined as referencing the same base function. """ if isinstance(other, BaseDoor) and self.name is other.name: @@ -167,7 +178,7 @@ def _inspect_base_callable(self): """Inspect the BaseDoor's baseline callable for primary attributes. This checks for type annotations, return statements, and all - information accessible to :py:obj:`inspect.Signature` relevant to + information accessible to `inspect.Signature` relevant to |BaseDoor|. """ # Need to find the un-wrapped function that actually takes the @@ -298,7 +309,7 @@ def _inspect_base_callable(self): @property def __closure__(self): - """Since BaseDoor is a wrapper, and we use utils.get_all_source to + """Since |BaseDoor| is a wrapper, and we use utils.get_all_source to retrieve source, this mimicks the type a function wrapper would have here. """ @@ -384,7 +395,6 @@ def _get_return_vals(function: Callable) -> List[str]: # there must exist non-\n whitespace for all lines after a funciton # definition. defmatch_str = r"^(\ )+def\s+" - retmatch_str = r".*\s+return\s(.*)" retmatch_str = r"^\s+(?:return|yield)\s(.*)" indentmatch_str = r"^(\s)*" @@ -455,9 +465,8 @@ def _get_return_vals(function: Callable) -> List[str]: 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. + # This is undefined, not an error. Issue a warning + # since this may be unexpected behavior. source_file = inspect.getfile(function) msg = ( @@ -466,8 +475,8 @@ def _get_return_vals(function: Callable) -> List[str]: f"{source_file}: {start_line+i}) " f"{orig_line.strip()}\n While not crucial to " f"this function, be aware that this means no " - f"return value will be modified by this " - f"callable." + f"return parameter will be modified by this " + f"callable in a Neighborhood." ) logger.warning(msg) @@ -492,7 +501,7 @@ def kwargs(self): class Door(BaseDoor): - """Inherits from and extends :class:`~porchlight.door.BaseDoor`""" + """Extends |BaseDoor| with Neighborhood-specific methods and handling.""" def __init__( self, @@ -506,18 +515,17 @@ def __init__( name: str = "", typecheck: bool = False, ): - """Initializes the :py:class:`~porchlight.door.Door` object using a - callable. + """Initializes the |Door| object. Arguments --------- function : Callable - A callable object to be parsed by :py:class:`~BaseDoor`. + A callable object to be parsed by |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 + accept "a" and "b" and arguments instead of "x" and "y", one could use .. code-block:: python @@ -532,21 +540,20 @@ def fxn(x): 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: + If `True`, will not parse the function using |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. + |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 + override |BaseDoor| if ``wrapped`` is ``False``. keyword_args : dict, keyword-only, optional @@ -556,7 +563,7 @@ def fxn(x): name : str, keyword-only, optional Overrides the default name for the Door if provided. - typecheck : :py:obj:`bool`, optional + typecheck : `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 @@ -611,6 +618,7 @@ def _initialize_wrapped_function( self.function_initialized = True def __call__(self, *args, **kwargs): + # To account for @Door behavior in one call stack, if not self.function_initialized: # Need to recieve the function. if len(args) != 1: @@ -639,12 +647,9 @@ def __call__(self, *args, **kwargs): return self if self.wrapped: - result = self._base_function(*args, **kwargs) - return result + return self._base_function(*args, **kwargs) - # Check argument mappings. if not self.argmap: - # Just pass arguments normally return super().__call__(*args, **kwargs) input_kwargs = {} @@ -670,7 +675,9 @@ def __call__(self, *args, **kwargs): return result def map_arguments(self): - """Maps arguments if self.argmap is not {}.""" + """Maps arguments if self.argmap is not empty and the function has been + initialized. + """ if not self.function_initialized: msg = "Door has not yet been initialized with a function." logging.error(msg) @@ -700,11 +707,12 @@ def map_arguments(self): logger.error(msg) raise DoorError(msg) + # Replace arguments with their mapped versions in the arguments + # dictionaries. 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: self.keyword_args[mapped_name] = self.keyword_args[old_name] @@ -761,7 +769,10 @@ def __repr__(self): return super().__repr__().replace("BaseDoor", "Door") @property - def original_arguments(self): + def original_arguments(self) -> dict[str, Type]: + """Returns a dict with original positional arguments required by the + base function. + """ arguments = copy.copy(self.arguments) for i, arg in enumerate(self.arguments): @@ -775,7 +786,10 @@ def original_arguments(self): return arguments @property - def original_kw_arguments(self): + def original_kw_arguments(self) -> dict[str, Type]: + """Returns a dict with original keyword arguments required by the base + function. + """ arguments = copy.copy(self.keyword_args) for i, arg in enumerate(self.keyword_args): @@ -789,7 +803,8 @@ def original_kw_arguments(self): return arguments @property - def original_return_vals(self): + def original_return_vals(self) -> list[str]: + """Returns a list with original return values of the base function.""" return_vals = copy.copy(self.return_vals) # Also change outputs that contain the same name. @@ -800,7 +815,7 @@ def original_return_vals(self): return return_vals @property - def argument_mapping(self): + def argument_mapping(self) -> dict[str, str]: return self.argmap @argument_mapping.setter @@ -808,11 +823,16 @@ def argument_mapping(self, value): self.arguments = self.original_arguments self.keyword_args = self.original_kw_arguments self.argmap = value + + # Need to re-execute the argument mapper. By this time, the function + # must be initialized. self.map_arguments() @property def variables(self) -> List[str]: - """Returns a list of all known return values and input arguments.""" + """Returns a list of all known return values and input arguments as + strs. + """ all_vars = [] for arg in self.arguments: @@ -827,7 +847,7 @@ def variables(self) -> List[str]: @property def required_arguments(self) -> List[str]: - """Returns a list of arguments with no default values.""" + """Returns a list of arguments with no default value.""" required = [] for x in self.arguments: @@ -840,18 +860,17 @@ def required_arguments(self) -> List[str]: class DynamicDoor(Door): """A dynamic door takes a door-generating function as its initializer. - Unlike :py:class:`~porchlight.door.BaseDoor` and - :py:class:`~porchlight.door.Door`, dynamic doors will only parse the + Unlike |BaseDoor| and |Door|, |DynamicDoor| will only parse the definition's source once it is generated. - These objects a function that returns a :py:class:`~porchlight.door.Door`. - The `DynamicDoor` then contains identical attributes to the generated door. + These objects a function that returns a |Door|. + The |DynamicDoor| then contains identical attributes to the generated door. Once called again, all attributes update to match the most recent call. Attributes ---------- _door_generator : `Callable` - A function returning a `~porchlight.door.Door` as its output. + A function returning a |Door| as its output. generator_args : `List` List of arguments to be passed as positional arguments to the @@ -868,7 +887,8 @@ def __init__( generator_args: List = [], generator_kwargs: Dict = {}, ): - """Initializes the Dynamic Door. When __call__ is invoked, the door + """Initializes the |DynamicDoor|. When + :py:meth:`porchlight.door.DynamicDoor.__call__` is invoked, the door generator is called. Arguments @@ -901,8 +921,8 @@ def __init__( def __call__(self, *args, **kwargs) -> Any: """Executes the function stored in - `~porchlight.door.DynamicDoorc._base_function` once - `~porchlight.door.DynamicDoor.update` has executed. + :py:meth:`~porchlight.door.DynamicDoor._base_function` once + :py:meth:`~porchlight.door.DynamicDoor.update` has executed. Arguments --------- @@ -922,7 +942,7 @@ def __call__(self, *args, **kwargs) -> Any: def call_without_update(self, *args, **kwargs) -> Any: """Executes the function stored in `~porchlight.door.DynamicDoor._base_function` *WITHOUT* executing - `~porchlight.door.DynamicDoor.update()` + :py:meth:`~porchlight.door.DynamicDoor.update()` Arguments --------- @@ -965,7 +985,7 @@ def __repr__(self): def update(self): """Updates the DynamicDoor using `DynamicDoor._door_generator` - This method is called when DynamicDoor.__call__ is invoked. + This method is called when :py:meth:`DynamicDoor.__call__` is invoked. """ self._last_door = self._cur_door From a4bf2b992143c93ccef7f79c6a2f73e5ad1b91e0 Mon Sep 17 00:00:00 2001 From: "Teal, D" Date: Wed, 12 Apr 2023 17:59:49 -0700 Subject: [PATCH 06/15] Further cleanup of door module --- porchlight/door.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/porchlight/door.py b/porchlight/door.py index a2a84a9..d0574d5 100644 --- a/porchlight/door.py +++ b/porchlight/door.py @@ -603,9 +603,8 @@ def fxn(x): 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` + """Initializes a function that is auto-wrapped by |Door| instead of + being passed to |BaseDoor|. """ if not self.name: self.name = "AutoWrappedFunctionDoor" @@ -874,11 +873,12 @@ class DynamicDoor(Door): generator_args : `List` List of arguments to be passed as positional arguments to the - `_door_generator` function. + :py:attr:`DynamicDoor._door_generator` function. generator_kwards : `Dict` Dict of key: value pairs representing keyword arguments to be passed as - positional arguments to the `_door_generator` function. + positional arguments to the :py:attr:`DynamicDoor._door_generator` + function. """ def __init__( @@ -888,7 +888,7 @@ def __init__( generator_kwargs: Dict = {}, ): """Initializes the |DynamicDoor|. When - :py:meth:`porchlight.door.DynamicDoor.__call__` is invoked, the door + :py:meth:`DynamicDoor.__call__` is invoked, the door generator is called. Arguments @@ -921,18 +921,17 @@ def __init__( def __call__(self, *args, **kwargs) -> Any: """Executes the function stored in - :py:meth:`~porchlight.door.DynamicDoor._base_function` once - :py:meth:`~porchlight.door.DynamicDoor.update` has executed. + :py:attr:`DynamicDoor._base_function` once + :py:meth:`DynamicDoor.update` has executed. Arguments --------- *args : positional arguments - Arguments directly passed to - `~porchlight.door.DynamicDoor._base_function`. + Arguments directly passed to :py:attr:`DynamicDoor._base_function`. **kwargs : keyword arguments Keyword arguments directly passed to - `~porchlight.door.DynamicDoor._base_function`. + :py:attr:`DynamicDoor._base_function`. """ self.update() result = super().__call__(*args, **kwargs) @@ -941,18 +940,18 @@ def __call__(self, *args, **kwargs) -> Any: def call_without_update(self, *args, **kwargs) -> Any: """Executes the function stored in - `~porchlight.door.DynamicDoor._base_function` *WITHOUT* executing + :py:attr:`DynamicDoor._base_function` *WITHOUT* executing :py:meth:`~porchlight.door.DynamicDoor.update()` Arguments --------- *args : positional arguments Arguments directly passed to - `~porchlight.door.DynamicDoor._base_function`. + :py:attr:`DynamicDoor._base_function`. **kwargs : keyword arguments Keyword arguments directly passed to - `~porchlight.door.DynamicDoor._base_function`. + :py:attr:`DynamicDoor._base_function`. """ # Give a specific, useful message if self._base_function is not # initialized. @@ -983,7 +982,7 @@ def __repr__(self): return outstr def update(self): - """Updates the DynamicDoor using `DynamicDoor._door_generator` + """Updates the DynamicDoor using :py:attr:`DynamicDoor._door_generator` This method is called when :py:meth:`DynamicDoor.__call__` is invoked. """ From ac9dedcbb95a0793b31a05dcf9cfd90ac7f91a64 Mon Sep 17 00:00:00 2001 From: "Teal, D" Date: Wed, 12 Apr 2023 21:30:01 -0700 Subject: [PATCH 07/15] Adding file for all substitutions in docs --- docs/source/substitutions.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 docs/source/substitutions.rst diff --git a/docs/source/substitutions.rst b/docs/source/substitutions.rst new file mode 100644 index 0000000..014a0c1 --- /dev/null +++ b/docs/source/substitutions.rst @@ -0,0 +1,10 @@ +.. |porchlight| replace:: **porchlight** +.. |BaseDoor| replace:: :py:class:`~porchlight.door.BaseDoor` +.. |Door| replace:: :py:class:`~porchlight.door.Door` +.. |DynamicDoor| replace:: :py:class:`~porchlight.door.DynamicDoor` +.. |Neighborhood| replace:: :py:class:`~porchlight.neighborhood.Neighborhood` +.. |PorchlightMediator| replace:: :py:class:`~porchlight.neighborhood.PorchlightMediator` +.. |BasePorchlightAdapter| replace:: :py:class:`~porchlight.door.BasePorchlightAdapter` +.. |PorchlightAdapter| replace:: :py:class:`~porchlight.door.PorchlightAdapter` +.. |DynamicPorchlightAdapter| replace:: :py:class:`~porchlight.door.DynamicPorchlightAdapter` +.. |Param| replace:: :py:class:`~porchlight.param.Param` From 2b442e006ac8fcad3590e2cac6e7dac388a2764e Mon Sep 17 00:00:00 2001 From: "Teal, D" Date: Wed, 12 Apr 2023 21:30:34 -0700 Subject: [PATCH 08/15] Significant changes to docs, substitutions Using the new `docs/source/substitutions.rst` file, now the docstrings are (for the most part) more readable than before. Not quite done, needs some proofreading, but a huge step in the right direction. --- docs/index.rst | 4 +- docs/source/door.rst | 2 + docs/source/neighborhood.rst | 4 +- docs/source/param.rst | 2 + docs/source/utils.rst | 5 +- porchlight/__init__.py | 15 +++++ porchlight/door.py | 9 --- porchlight/neighborhood.py | 107 +++++++++++++++++++---------------- 8 files changed, 82 insertions(+), 66 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 0e13bf0..7e3ee4f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,3 +1,5 @@ +.. include:: source/substitutions.rst + Welcome to |porchlight|'s documentation! ======================================== @@ -26,5 +28,3 @@ Indices and tables This project is under active development. Please report any bugs you encounter to https://github.com/teald/porchlight/issues! - -.. |porchlight| replace:: **porchlight** diff --git a/docs/source/door.rst b/docs/source/door.rst index 4c7976b..c6e6784 100644 --- a/docs/source/door.rst +++ b/docs/source/door.rst @@ -1,3 +1,5 @@ +.. include:: substitutions.rst + `door` module ============= diff --git a/docs/source/neighborhood.rst b/docs/source/neighborhood.rst index 98ca6e4..091b875 100644 --- a/docs/source/neighborhood.rst +++ b/docs/source/neighborhood.rst @@ -1,3 +1,5 @@ +.. include:: substitutions.rst + `neighborhood` module ===================== @@ -9,5 +11,3 @@ class between different functions that have been re-cast as :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 9a5063e..a102402 100644 --- a/docs/source/param.rst +++ b/docs/source/param.rst @@ -1,3 +1,5 @@ +.. include:: substitutions.rst + `param` module ============== diff --git a/docs/source/utils.rst b/docs/source/utils.rst index f7483b3..a184592 100644 --- a/docs/source/utils.rst +++ b/docs/source/utils.rst @@ -1,3 +1,5 @@ +.. include:: substitutions.rst + `utils` module ============== @@ -24,6 +26,3 @@ Typing utilities :members: :show-inheritance: :private-members: - - -.. |porchlight| replace:: **porchlight** diff --git a/porchlight/__init__.py b/porchlight/__init__.py index a562524..e128b2b 100644 --- a/porchlight/__init__.py +++ b/porchlight/__init__.py @@ -1,3 +1,18 @@ +"""`porchlight` is a library meant to reduce the complexity and stress of +coupling models or providing a very simple python API. + +.. include:: substitutions.rst + +It contains three classes especially imported by this file to the main +package-level namespace: ++ |Neighborhood| ++ |Door| ++ |Param| + +Please see their respective documentation for details on usage, or check out +the |porchlight| :doc:`quickstart` guide + +""" import os from .door import Door diff --git a/porchlight/door.py b/porchlight/door.py index d0574d5..e74c9d8 100644 --- a/porchlight/door.py +++ b/porchlight/door.py @@ -7,15 +7,6 @@ from the object (if possible), and provide a calling interface with optional checks and actions (see individual descriptions). -.. |BaseDoor| replace:: :py:class:`~porchlight.door.BaseDoor` -.. |Door| replace:: :py:class:`~porchlight.door.Door` -.. |DynamicDoor| replace:: :py:class:`~porchlight.door.DynamicDoor` -.. |BasePorchlightAdapter| replace:: - :py:class:`~porchlight.door.BasePorchlightAdapter` -.. |PorchlightAdapter| replace:: - :py:class:`~porchlight.door.PorchlightAdapter` -.. |DynamicPorchlightAdapter| replace:: - :py:class:`~porchlight.door.DynamicPorchlightAdapter` """ import inspect import re diff --git a/porchlight/neighborhood.py b/porchlight/neighborhood.py index 3adfb19..aa9811a 100644 --- a/porchlight/neighborhood.py +++ b/porchlight/neighborhood.py @@ -1,4 +1,14 @@ -"""Defines the `Neighborhood` class.""" +"""A mediator object for sequentially executing callables with a variable set +of data values. + +The primary mediator object is the |Neighborhood| class. It acts as an +interface for other parts of the |porchlight| library, meaning it will manage +|Door| creation, |Param| assignment, and other checks. It can be further +extended using user-defined |Door|. + +Also contains the definition for +:py:class:`~porchlight.neighborhood.NeighborhoodError`. +""" from . import door from . import param from .utils.typing_functions import decompose_type @@ -15,31 +25,29 @@ class NeighborhoodError(Exception): class Neighborhood: """A neighborhood manages the interactions between Doors. It consists of a - modifiable collection of :class:`~porchlight.door.Door` (or - :class:`~porchlight.door.BaseDoor`) objects. + modifiable collection of |Door| (or |BaseDoor|) objects. Attributes ---------- - _doors : :py:obj:`dict`, :py:obj:`str`: :class:`~porchlight.door.Door` - Contains all data for :class:`~porchlight.door.Door` objects. The keys - are, by default, the :meth:`~porchlight.door.Door.name` - property for the corresponding :class:`~porchlight.door.Door` values. + _doors : ``dict``, ``str``: |Door| + Contains all data for |Door| objects. The keys are, by default, the + :meth:`~porchlight.door.Door.name` property for the corresponding + |Door| values. - _params : :py:obj:`dict`, :py:obj:`str`: :class:`~porchlight.param.Param` + _params : ``dict``, ``str``: |Param| Contains all the parameters currently known to and managed by the - :class:`~porchlight.neighborhood.Neighborhood` object. + |Neighborhood| object. - _call_order : :py:obj:`list`, :py:obj:`str` - The order in which the :class:`~porchlight.door.Door` objects in - `_doors` are called. By default, this is the order in which - :class:`~porchlight.door.Door`s are added to the `Neighborhood`. + _call_order : ``list``, ``str`` + The order in which the |Door| objects in `_doors` are called. By + default, this is the order in which |Door| are added to the + |Neighborhood|. - initialization : :py:obj:`list` of ``Callable``, `keyword-only` - These will be called once the - :class:`~porchlight.neighborhood.Neighborhood` object begins any + initialization : ``list`` of ``Callable``, `keyword-only` + These will be called once the |Neighborhood| object begins any execution (e.g., via - :py:meth:`~porchlight.neighborhood.Neighborhood.run_step`) will - run these callables. This will only execute again if + :py:meth:`~porchlight.neighborhood.Neighborhood.run_step`) will run + these callables. This will only execute again if ``Neighborhood.has_initialized`` is set to ``False``. """ @@ -120,18 +128,18 @@ def add_function( Parameters ---------- function : Callable - The function to be added. This is converted to a - :class:`~porchlight.door.Door` object by this method. + The function to be added. This is converted to a |Door| object by + this method. overwrite_defaults : bool, optional If `True`, will overwrite any parameters shared between the `function` and `Neighborhood._params` to be equal to the defaults set by `function`. If `False` (default), no parameters that exist - in the `Neighborhood` object already will be changed. + in the |Neighborhood| object already will be changed. dynamic_door : bool, optional - If `True` (default `False`), then the output(s) of this `Door` will - be converted into a `Door` or set of `Door`s in the `Neighborhood` + If `True` (default `False`), then the output(s) of this |Door| will + be converted into a |Door| or set of |Door| in the |Neighborhood| object. """ new_door = door.Door(function) @@ -149,11 +157,10 @@ def add_door( Parameters ---------- - new_door : :class:`~porchlight.door.Door`, - :class:`~porchlight.door.DynamicDoor`, or :py:obj:`list` of - :class:`~porchlight.door.Door` objects. + new_door : |Door|, + |DynamicDoor|, or ``list`` of |Door| objects. - Either a single initialized `door.Door` object or a list of them. + Either a single initialized |Door| object or a list of them. If a list is provided, this function is called for each item in the list. @@ -161,11 +168,11 @@ def add_door( If `True`, will overwrite any parameters shared between the `new_door` and `Neighborhood._params` to be equal to the defaults set by `new_door`. If `False` (default), no parameters that exist - in the `Neighborhood` object already will be changed. + in the |Neighborhood| object already will be changed. dynamic_door : bool, optional - If `True` (default `False`), then the output(s) of this `Door` will - be converted into a `Door` or set of `Door`s in the `Neighborhood` + If `True` (default `False`), then the output(s) of this |Door| will + be converted into a |Door| or set of |Door| in the |Neighborhood| object. """ if isinstance(new_door, List): @@ -243,12 +250,12 @@ def template_ddoor(param_name): self.add_param(ret_val, param.Empty()) def remove_door(self, name: str): - """Removes a :class:`~porchlight.door.Door` from :attr:`_doors`. + """Removes a |Door| from :attr:`_doors`. Parameters ---------- - name : :py:obj:`str` - The name of the :class:`~porchlight.door.Door` to be removed. It + name : ``str`` + The name of the |Door| to be removed. It must correspond to a key in `Neighborhood._doors` attribute. Raises @@ -285,23 +292,23 @@ def set_param( Parameters ---------- - parameter_name : :py:obj:`str` + parameter_name : ``str`` The name of the parameter to modify. new_value : `Any` The value to be assigned to this parameter. - constant : :py:obj:`bool` - Will be passed to :class:`~porchlight.param.Param` as the + constant : ``bool`` + Will be passed to |Param| as the `constant` keyword argument. - ignore_constant : :py:obj:`bool`, optional, keyword-only + ignore_constant : ``bool``, optional, keyword-only When assigning this parameter, it will ignore the `constant` attribute of the current parameter. Raises ------ - :class:`~porchlight.param.ParameterError` + |ParameterError| Is raised if the parameter attempting to be changed has `True` for its `constant` attribute. Will not be raised by this method if `ignore_constant` is `True`. @@ -326,20 +333,20 @@ def add_param( constant: bool = False, restrict: Union[Callable, None] = None, ): - """Adds a new parameter to the `Neighborhood` object. + """Adds a new parameter to the |Neighborhood| object. The parameters all correspond to arguments passed directly to the - :class:`~porchlight.param.Param` initializer. + |Param| initializer. Parameters ---------- - parameter_name : :py:obj:`str` + parameter_name : ``str`` Name of the parameter being created. value : `Any` Parameter value - constant : :py:obj:`bool`, optional + constant : ``bool``, optional If `True`, the parameter is set to constant. restrict : :py:class:`~typing.Callable` or None, optional @@ -387,7 +394,7 @@ def call_all_doors(self): """Calls every door currently present in the neighborhood object. This order is currently dictated by the order in which - :class:`~porchlight.door.Door`s are added to the `Neighborhood`. + |Door| are added to the |Neighborhood|. The way this is currently set up, it will not handle positional arguments. That is, if an input cannot be passed using its variable @@ -531,14 +538,14 @@ def gather_door_arguments( self, input_door: door.Door, defaults: Dict[str, Any] = {} ) -> Tuple[List, Dict]: """This retrieves all parameters required by a - :py:class:`~porchlight.door.Door`, returning them as a list (positional + |Door|, returning them as a list (positional arguments) and a dictionary (keyword arguments). If there are no positional-only arguments and/or no no keyword arguments, then empty objects are returned. Arguments --------- - input_door : :py:class:`~porchlight.door.Door` + input_door : |Door| The door to gather necessary parameters for. defaults : dict[str, Any] @@ -557,7 +564,7 @@ def gather_door_arguments( Notes ----- The return values must be unpacked before being used to call the - :py:class:`~porchlight.door.Door`. + |Door|. """ # Gather the arguments needed by the door. Defaults are folded into a # new dictionary to keep them temporary. @@ -582,7 +589,7 @@ def initialize(self): """Runs initialization functions present in :py:attr:`~porchlight.neighborhood.Neighborhood.initialization` if ``has_initialized`` is ``False`` for this - :py:class:`~porchlight.neighborhood.Neighborhood`. + |Neighborhood|. """ # Do nothing if initialization has already happened. if self.has_initialized or not self.initialization: @@ -647,7 +654,7 @@ def finalize(self): ``Neighborhood.finalization``. It must be invoked directly by the user. Unlike initialization, finalization will add new constant - :py:class:`~porchlight.param.Param`s to the ``Neighborhood`` object. + |Param| to the `|Neighborhood|` object. """ # Ensure finalization is iterable, if not raise either a ValueError # (because it is not a valid object) or TypeError (because it is not a @@ -726,13 +733,13 @@ def order_doors(self, order: List[str]): """Allows the doors to be ordered when called. If this is never called, the call order will be equivalent to the order - in which the doors are added to the `Neighborhood`. As of right now, + in which the doors are added to the |Neighborhood|. As of right now, all doors present must be included in the `order` argument or a `KeyError` will be thrown. Arguments --------- - order : :py:obj:`list`, str + order : ``list``, str The order for doors to be called in. Each `str` must correspond to a key in `Neighborhood._doors`. """ From 4d47dac94a2f89b624b3a74f329776bd097d123e Mon Sep 17 00:00:00 2001 From: "Teal, D" Date: Wed, 12 Apr 2023 22:13:16 -0700 Subject: [PATCH 09/15] Updates to param.py docstrings --- docs/source/substitutions.rst | 2 ++ porchlight/param.py | 59 ++++++++++++++++++++++++++++------- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/docs/source/substitutions.rst b/docs/source/substitutions.rst index 014a0c1..07ec570 100644 --- a/docs/source/substitutions.rst +++ b/docs/source/substitutions.rst @@ -8,3 +8,5 @@ .. |PorchlightAdapter| replace:: :py:class:`~porchlight.door.PorchlightAdapter` .. |DynamicPorchlightAdapter| replace:: :py:class:`~porchlight.door.DynamicPorchlightAdapter` .. |Param| replace:: :py:class:`~porchlight.param.Param` +.. |Empty| replace:: :py:class:`~porchlight.param.Empty` +.. |ParameterError| replace:: :py:class:`~porchlight.param.ParameterError` diff --git a/porchlight/param.py b/porchlight/param.py index d08c4e8..39e70dc 100644 --- a/porchlight/param.py +++ b/porchlight/param.py @@ -1,3 +1,9 @@ +"""A constainer class for arbitrary data, with type and state checking. + +|Param| is the primary class introduces in this file. |Empty| is a singleton +class denoting an "empty" parameter. The :py:class:`ParameterError` class is +also defined here. +""" from typing import Any, Callable, Type, Union import logging @@ -6,7 +12,7 @@ class ParameterError(Exception): - """Error for Param-specific issues.""" + """Error for Param-specific exceptions.""" pass @@ -14,7 +20,12 @@ class ParameterError(Exception): class Empty: """An empty class representing missing parameters values. - At initializtion, if an instance does not already exist it is created. + At initialization, if an instance does not already exist it is created. No + instance of Empty exists until it has been instantiated once. + + It is recommended that |Empty| be used over :py:obj:`None` to denote + parameters that have not been initialized with any value, so that + :py:obj:`None` can be treated unambiguously when using |porchlight|. """ def __new__(cls): @@ -37,11 +48,13 @@ def __neq__(self, other): class Param: - """Parameter class. while not frozen, for most purposes it should not be - modified outside of a porchlight object. + """Container class for arbitrary Python data. + + Although mutable, editing |Param| objects directly is strongly discouraged + unless absolutely necessary. - `Param` uses `__slots__`, and no attributes other than those listed below - may be assigned to `Param` objects. + |Param| uses `__slots__`, and no attributes other than those listed below + may be assigned to |Param| objects. Attributes ---------- @@ -50,15 +63,37 @@ class Param: _value : :py:class:`~typing.Any` Value of the parameter. If the parameter does not contain an assigned - value, this should be :class:`~porchlight.param.Empty` + value, this should be |Empty|. _type : :py:class:`~typing.type` - The type corresponding to the type of `Param._value` + The type corresponding to the type of :py:attr:`Param._value`. constants : :py:obj:`bool` - True if this object should be considered a constant. If the `Param` - value is modified by :class:`Param.value`'s `setter`, but `constant` is - True, a :class:`~porchlight.param.ParameterError` will be raised. + True if this object should be considered a constant. If the |Param| + value is modified by :py:attr:`Param.value`'s `setter`, but + `constant` is True, a :py:class:`~porchlight.param.ParameterError` will + be raised. + + restrict : `Callable` or `None` + If a callable, it will be invoked on the parameter whenever the + parameter is changed. If it evaluates to False, a |ParameterError| is + raised. + + Example: "temperature" parameter should not be negative or zero in our + model: + + .. code-block::python + + temp = Param( + "temperature", + restrict=lambda x: x > 0 + ) + + # The below would raise a ParameterError, the check occuring + # automatically. + temp.value = -500 + + See :py:attr:`Param.value` for further details. """ # A parameter, to be updated from the API, needs to be replaced rather than @@ -85,7 +120,7 @@ def __init__( assigned value, this should be `~porchlight.param.Empty` constant : :py:obj:`bool` - True if this object should be considered a constant. If the `Param` + True if this object should be considered a constant. If the |Param| value is modified by `Param.value`'s `setter`, but `constant` is True, a :class:`~porchlight.param.ParameterError` will be raised. From f885459e8698a1401b4407b1894909d261211ae0 Mon Sep 17 00:00:00 2001 From: "Teal, D" Date: Wed, 12 Apr 2023 22:18:18 -0700 Subject: [PATCH 10/15] [BUG] remove unnecessary if statement --- porchlight/door.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/porchlight/door.py b/porchlight/door.py index e74c9d8..8bc5d1d 100644 --- a/porchlight/door.py +++ b/porchlight/door.py @@ -215,10 +215,6 @@ def _inspect_base_callable(self): f"source: {e}" ) - if isinstance(function, Callable): - # This is still a function, and has otherwise worked. - pass - # Ensure the function can be inspected. If not, raise # NotImplementedError if not self._can_inspect(function): From 9559922614d03a50a2e7ebbcb14966f6664397fb Mon Sep 17 00:00:00 2001 From: "Teal, D" Date: Wed, 12 Apr 2023 22:39:32 -0700 Subject: [PATCH 11/15] Some refactoring of `Door.map_arguments` Mostly updated comments and docstring. No change to the functionality/inner workings beyond moving the bulk of the code out of an if statement. --- porchlight/door.py | 108 +++++++++++++++++++++++++++------------------ 1 file changed, 64 insertions(+), 44 deletions(-) diff --git a/porchlight/door.py b/porchlight/door.py index 8bc5d1d..107570d 100644 --- a/porchlight/door.py +++ b/porchlight/door.py @@ -663,69 +663,89 @@ def __call__(self, *args, **kwargs): def map_arguments(self): """Maps arguments if self.argmap is not empty and the function has been initialized. + + This is done by modifying the door arguments directly, and using the + argument mapping as a converter between the two systems if the original + arguments are needed. This makes the |Door| mimic the same arguments + when accessed by a |Neighborhood| or when arguments are given to it. + + Mapped arguments are *overridden*, meaning they will no longer be + recognized by the Door as appropriate arguments passed through + :py:meth:`Door.__call__`. """ + # Function metadata is required for argument mapping. Check this before + # checking argument mapping, since this should always be the case for + # Doors accepting a new argument map. if not self.function_initialized: msg = "Door has not yet been initialized with a function." logging.error(msg) raise DoorError(msg) - if self.argmap: - Door._check_argmap(self.argmap) + if not self.argmap: + return - arg_order = tuple(self.arguments.keys()) - kwarg_order = tuple(self.kwargs.keys()) + # After catching any yet-forseen inconsistencies that might arise in + # argument mapping, preserve the structure of the original arguments + # (especially order) and generate a new set of argument and return + # values using the mapped arguments. + Door._check_argmap(self.argmap) - for mapped_name, old_name in self.argmap.items(): - # Catch mappings that would conflict with an existing key. - if mapped_name in self.arguments: - msg = ( - f"Conflicting map key: {mapped_name} is in arguments " - f"list." - ) + arg_order = tuple(self.arguments.keys()) + kwarg_order = tuple(self.kwargs.keys()) - logger.error(msg) - raise DoorError(msg) + for mapped_name, old_name in self.argmap.items(): + # Catch conflicts with existing keys. + if mapped_name in self.arguments: + msg = ( + f"Conflicting map key: {mapped_name} is in arguments " + f"list." + ) - 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) + logger.error(msg) + raise DoorError(msg) + + # Check that the name exists in the Door's known arguments and + # return values, raise an error if not. + 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) - # Replace arguments with their mapped versions in the arguments - # dictionaries. - if old_name in self.arguments: - self.arguments[mapped_name] = self.arguments[old_name] - del self.arguments[old_name] + # Replace arguments with their mapped versions in the arguments + # dictionaries. + if old_name in self.arguments: + self.arguments[mapped_name] = self.arguments[old_name] + del self.arguments[old_name] - if old_name in self.keyword_args: - self.keyword_args[mapped_name] = self.keyword_args[old_name] + if old_name in self.keyword_args: + self.keyword_args[mapped_name] = self.keyword_args[old_name] - # Need to change the parameter name to reflect the mapping. - self.keyword_args[mapped_name]._name = mapped_name + # Need to change the Param name to reflect the mapping. + self.keyword_args[mapped_name]._name = mapped_name - del self.keyword_args[old_name] + del self.keyword_args[old_name] - # Also change outputs that contain the same 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 + # Also change outputs that contain the same 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()} + # Re-construct the newly mapped dictionaries, with all values in order. + rev_argmap = {v: k for k, v in self.argmap.items()} - arg_order = ( - k if k not in rev_argmap else rev_argmap[k] for k in arg_order - ) + arg_order = ( + k if k not in rev_argmap else rev_argmap[k] for k in arg_order + ) - kwarg_order = ( - k if k not in rev_argmap else rev_argmap[k] for k in kwarg_order - ) + kwarg_order = ( + k if k not in rev_argmap else rev_argmap[k] for k in kwarg_order + ) - self.arguments = {a: self.arguments[a] for a in arg_order} - self.keyword_args = {a: self.keyword_args[a] for a in kwarg_order} + self.arguments = {a: self.arguments[a] for a in arg_order} + self.keyword_args = {a: self.keyword_args[a] for a in kwarg_order} def _check_argmap(argmap): """Assesses if an argument mapping is valid, raises an appropriate From a446c86c53334dc04098b2eeed7d7fd3fdc6430e Mon Sep 17 00:00:00 2001 From: "Teal, D" Date: Wed, 12 Apr 2023 22:42:44 -0700 Subject: [PATCH 12/15] Version bump to 1.1.0 --- docs/conf.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 9f749d2..fadd790 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 = "1.0.2" +release = "1.1.0" # -- General configuration --------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 8238eba..d743c63 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "porchlight" -version = "1.0.2" +version = "1.1.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" From 7d98b566bdfa25a542157ac91884969e95f998a3 Mon Sep 17 00:00:00 2001 From: "Teal, D" Date: Wed, 12 Apr 2023 23:17:08 -0700 Subject: [PATCH 13/15] Quickstart edits --- docs/other/quickstart.rst | 66 ++++++++++++++++++++++------------- docs/source/substitutions.rst | 1 + 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/docs/other/quickstart.rst b/docs/other/quickstart.rst index bb4a503..daf12b7 100644 --- a/docs/other/quickstart.rst +++ b/docs/other/quickstart.rst @@ -1,12 +1,14 @@ .. role:: python(code) :language: python +.. include:: ../source/substitutions.rst + Quickstart ========== -Welcome to a short guide to hitting the ground running with |porchlight|! This -tutorial will step through the basics of installing and using |porchlight|. If -you are looking for more advanced examples, see the examples available in +Welcome to a short guide to working with |porchlight|! This tutorial will step +through the basics of installing and using |porchlight|. If you are looking for +more advanced examples, see the examples available in `the porchlight github repository `_. Requirements @@ -14,6 +16,9 @@ Requirements |porchlight| requires *Python version 3.9 or higher*. +**Note:** If you download the repository you'll notice that requirements.txt is +not empty; these requirements are for building these docs, and are not used by +the main |porchlight| library. Installation ------------ @@ -23,7 +28,7 @@ instructions: * `Python 3.9 or above `_ -You can install |porchlight| directly using :code:`pip`: +You can install |porchlight| using :code:`pip`: .. code-block:: console @@ -37,13 +42,22 @@ environment! To get started, just import the library: import porchlight + Type annotations and |porchlight| --------------------------------- -Within |porchlight|, type annotation are allowed and encouraged. Generally, save -for a few *very special cases*, you can ignore type annotations when writing -your code. |porchlight|, via the |Door| class in particular, will note type -annotations if they are present and otherwise will ignore them. +Within |porchlight|, type annotation are allowed and encouraged. Generally, +save for a few *very special cases*, [#annotationspecialcases]_ you can ignore +type annotations when writing your code. |porchlight|, via the |Door| class in +particular, will note type annotations if they are present and otherwise will +ignore them. + +.. [#annotationspecialcases] + + |DynamicDoor| generator functions require |Door| outputs to be type-hinted, + to discourage using generic Callables that may result in unexpected + behavior. + Creating a |Neighborhood| object -------------------------------- @@ -52,7 +66,8 @@ The |Neighborhood| object collects various functions, extracts information about the function from existing metadata (using the `inspect `_ module in the CPython standard library) and the source code itself. Adding a function -to a +to a |Neighborhood| is as straightforward as passing the function's name, +whether defined locally or otherwise: .. code-block:: python @@ -68,7 +83,8 @@ to a neighborhood.add_function(my_function) At this point, |porchlight| will parse the function and store metadata about -it. The `str` representation of Neighborhood contains most of the data: +it. The `str` representation of Neighborhood contains most of the data in a +very dense format: .. code-block:: python @@ -80,15 +96,16 @@ it. The `str` representation of Neighborhood contains most of the data: A few things are now kept track of by the |Neighborhood| automatically: -1. The function arguments, now tracked as a |param| object. The default values - found were saved (in our case, it found `z = 0`), and any parameters not yet +1. The function arguments, now tracked as |Param| objects. The default values + found were saved (in our case, it found :python:`z = 0`), and any parameters not yet assigned a value have been given the :py:class:`~porchlight.param.Empty` value. 2. Function return variables. We'll explore this in more detail later, but one - important note here: the return variable name is important! + important note here: *the return variable names are critically important to + keep consistent!* -Right now, our |Neighborhood| is a -fully-fledged, if tiny, model. Let's set our variables and run it! +Right now, our |Neighborhood| is a fully-fledged, if tiny, model. Let's set our +variables and run it! .. code-block:: python @@ -113,7 +130,7 @@ obviously. We could manage our own :code:`x`, :code:`y`, and :code:`z` in a heartbeat, and all |porchlight| *really* did was what we could do with something as simple as :python:`y = my_function(2, 0)`. Let's add another function to our neighborhood and call -:meth:`~porchlight.neighborhood.Neighborhood.run_step` +:py:meth:`~porchlight.neighborhood.Neighborhood.run_step` .. code-block:: python @@ -142,7 +159,7 @@ function to our neighborhood and call 3) x = 2, y = 13, z = 15 4) x = 2, y = 19, z = 24 -As we see, we are now running a system of two functions that share variables. +We are now running a system of two functions that share variables. As we step forward, the functions are called sequentially and the parameters are updated directly. @@ -152,6 +169,13 @@ to know when and what to run, check, and modify. To really leverage |porchlight|, we'll need to get to know these objects a bit better on their own. +By default, the functions are called sequentially in the order they were added +to the |Neighborhood|. To re-arrange them, +:py:meth:`~porchlight.neighborhood.Neighborhood.order_doors` takes a list of +the |Door| names and will modify the call order appropriately. This does +require the list to have each |Door| name, spelled correctly. + + |Param| objects --------------- @@ -303,10 +327,4 @@ Closing Nuances will not include a dedicated stable branch until v1.0.0. That means that you need to be cautious with versions before v1.0.0, and some changes may break your code. You can generally assume that increments of 0.0.1 are non-breaking. - 0.1.0 increments may be breaking. - -.. |porchlight| replace:: **porchlight** -.. _Python: https://www.python.org/downloads/ -.. |Neighborhood| replace:: :py:class:`~porchlight.neighborhood.Neighborhood` -.. |Door| replace:: :py:class:`~porchlight.door.Door` -.. |Param| replace:: :py:class:`~porchlight.param.Param` + 0.1.0 increments may be breaking, check the appropriate Release Notes. diff --git a/docs/source/substitutions.rst b/docs/source/substitutions.rst index 07ec570..60823af 100644 --- a/docs/source/substitutions.rst +++ b/docs/source/substitutions.rst @@ -10,3 +10,4 @@ .. |Param| replace:: :py:class:`~porchlight.param.Param` .. |Empty| replace:: :py:class:`~porchlight.param.Empty` .. |ParameterError| replace:: :py:class:`~porchlight.param.ParameterError` +.. _Python: https://www.python.org/downloads/ From 54bed7fce33cf802a459371646f458aba13dc6f0 Mon Sep 17 00:00:00 2001 From: "Teal, D" Date: Wed, 12 Apr 2023 23:23:45 -0700 Subject: [PATCH 14/15] Adding support for future migration to __init__.py v1.2.0 will migrate `Door`->`PorchlightAdapter` and `Neighborhood`->`PorchlightMediator`. For information about that change, you'll need to see the eventual issue ticket/pull request for that. --- porchlight/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/porchlight/__init__.py b/porchlight/__init__.py index e128b2b..88254cd 100644 --- a/porchlight/__init__.py +++ b/porchlight/__init__.py @@ -13,6 +13,7 @@ the |porchlight| :doc:`quickstart` guide """ +import logging import os from .door import Door @@ -20,7 +21,11 @@ from .param import Param -# Initialize logging -import logging +# Aliases for migration to generic names, >=v1.2.0. +PorchlightAdapter = Door +PorchlightMediator = Neighborhood + +# Initialize logging with a NullHandler to let user decide on a handler if they +# so choose. logging.getLogger(__name__).addHandler(logging.NullHandler()) From 7efc6fcb3026c16473a2028f446e7d97a37ad0d3 Mon Sep 17 00:00:00 2001 From: "Teal, D" Date: Wed, 12 Apr 2023 23:26:46 -0700 Subject: [PATCH 15/15] PorchlightContainer <-> Param in __init__.py --- porchlight/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/porchlight/__init__.py b/porchlight/__init__.py index 88254cd..a6bddfd 100644 --- a/porchlight/__init__.py +++ b/porchlight/__init__.py @@ -24,6 +24,7 @@ # Aliases for migration to generic names, >=v1.2.0. PorchlightAdapter = Door PorchlightMediator = Neighborhood +PorchlightContainer = Param # Initialize logging with a NullHandler to let user decide on a handler if they