Skip to content

Commit

Permalink
Merge pull request #26 from teald/17-decorated-doors-are-not-correctl…
Browse files Browse the repository at this point in the history
…y-parsed-by-basedoor

Fixing bug with decorators; now use __closure__ to get wrapped function
  • Loading branch information
teald authored Nov 6, 2022
2 parents 2deb253 + 268f192 commit 4bb18d4
Show file tree
Hide file tree
Showing 4 changed files with 211 additions and 12 deletions.
4 changes: 3 additions & 1 deletion porchlight/door.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from .param import Empty, ParameterError, Param
from .utils.typing_functions import decompose_type
from .utils.inspect_functions import get_all_source

import typing
from typing import Any, Callable, Dict, List, Type
Expand Down Expand Up @@ -241,7 +242,7 @@ def _get_return_vals(function: Callable) -> List[str]:
"""
return_vals = []

lines, start_line = inspect.getsourcelines(function)
lines, start_line = get_all_source(function)

# Tracking indentation for python-like parsing.
cur_indent = 0
Expand Down Expand Up @@ -282,6 +283,7 @@ def _get_return_vals(function: Callable) -> List[str]:
if re.match(r"\s*@\w+.*", line):
continue

# Catch in-function definitions and ignore them.
if defmatch and i > 0 and main_def_found:
checking_for_returns = False
last_check_indent = cur_indent
Expand Down
22 changes: 11 additions & 11 deletions porchlight/tests/test_basedoor.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,21 +168,21 @@ def int_func():
self.assertEqual(result, [["y"]])

# TODO: Below is commented out intentionally for github issue #17.
# # Test decorators.
# def dummy_decorator(fun) -> Callable:
# def wrapper(*args, **kwargs):
# return fun(*args, **kwargs)
# Test decorators.
def dummy_decorator(fun) -> Callable:
def wrapper(*args, **kwargs):
return fun(*args, **kwargs)

# return wrapper
return wrapper

# @dummy_decorator
# def test_decorator() -> int:
# x = 1
# return x
@dummy_decorator
def test_decorator() -> int:
x = 1
return x

# result = BaseDoor._get_return_vals(test_decorator)
result = BaseDoor._get_return_vals(test_decorator)

# self.assertEqual(result, [["x"]])
self.assertEqual(result, [["x"]])

def test___eq__(self):
@BaseDoor
Expand Down
159 changes: 159 additions & 0 deletions porchlight/tests/test_utils_inspect_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"""Unit testing for porchlight/utils/inspect_functions."""
import porchlight.utils.inspect_functions as inspect_functions
import inspect
import unittest


def _helper_indent_and_newline(
lines: list[str], indent: int, indent_blank: bool = True
):
"""Takes a list of strings and returns those strings."""
for i, line in enumerate(lines):
if line:
line = " " * indent + line

if i < len(lines):
line += "\n"

lines[i] = line

return lines


class TestInspectFunctions(unittest.TestCase):
def test_get_all_source(self):
# The baseline case should reduce directly to inspec.getsourcelines
def test1(bing_bong: bool = True) -> str:
"""This is my internal docstring."""
switch = bing_bong

# This is a comment line.
result = "The switch is "
result += "on" if switch else "off"

return result

expected_result = inspect.getsourcelines(test1)

result = inspect_functions.get_all_source(test1)

self.assertEqual(result[0], expected_result[0])
self.assertEqual(result[1], expected_result[1])

# Decorator handling.
def test2_decorator(fxn):
"""This is a dummy wrapper."""

def wrapper(*args, **kwargs):
result = fxn(*args, **kwargs)
return result

return wrapper

@test2_decorator
def test2(a, b, c):
"""A docstring for this test function."""
# A comment in this test function.
total = sum(a, b, c)
outstr = f"{total} = {a} + {b} + {c}"
return outstr

expected_result = [
"@test2_decorator",
"def test2(a, b, c):",
' """A docstring for this test function."""',
" # A comment in this test function.",
" total = sum(a, b, c)",
' outstr = f"{total} = {a} + {b} + {c}"',
" return outstr",
]

expected_result = _helper_indent_and_newline(expected_result, 8)

result = inspect_functions.get_all_source(test2)
self.assertEqual(result[0], expected_result)

# Decorators with arguments.
def test3_decorator(message):
def test3_decorator(fxn):
def wrapped_fxn(*args, **kwargs):
result = fxn(*args, **kwargs)
return f"{message}\n{result}"

return wrapped_fxn

return test3_decorator

@test3_decorator("This was a unit test: ")
def test3(*vector_values):
vector = [f"{x:1.3e}" for x in vector_values]
vectorstr = "".join(vector)

return vectorstr

expected_result = [
'@test3_decorator("This was a unit test: ")',
"def test3(*vector_values):",
' vector = [f"{x:1.3e}" for x in vector_values]',
' vectorstr = "".join(vector)',
"",
" return vectorstr",
]

expected_result = _helper_indent_and_newline(expected_result, 8)

result = inspect_functions.get_all_source(test3)
self.assertEqual(result[0], expected_result)

# With multiple decorators.
def test4_dec1(fxn):
def wrapper1(*args, **kwargs):
result = fxn(*args, **kwargs)
return result

return wrapper1

def test4_dec2(fxn):
def wrapper2(*args, **kwargs):
result = fxn(*args, **kwargs)
return result

return wrapper2

@test4_dec1
@test4_dec1
def test4_1():
pass

expected_result = [
"@test4_dec1",
"@test4_dec1",
"def test4_1():",
" pass",
]

expected_result = _helper_indent_and_newline(expected_result, 8)
result = inspect_functions.get_all_source(test4_1)

self.assertEqual(result[0], expected_result)

@test4_dec1
@test4_dec2
def test4_2():
pass

expected_result = [
"@test4_dec1",
"@test4_dec2",
"def test4_2():",
" pass",
]

expected_result = _helper_indent_and_newline(expected_result, 8)
result = inspect_functions.get_all_source(test4_2)

self.assertEqual(result[0], expected_result)


if __name__ == "__main__":
unittest.main()
38 changes: 38 additions & 0 deletions porchlight/utils/inspect_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Tools for introspection of functions extending what :py:module:`inspect` can
do.
"""
import inspect

from typing import Callable, List, Tuple, Type


def get_all_source(function: Callable) -> Tuple[List[str], int]:
"""Retrieves all source code related to a given function, even if it has
been otherwise wrapped.
It returns a tuple containing a list of strings containing the source code
and an integer (starting line number). This is output by the eventual call
to `inspect.getsourcelines` on the wrapped function.
Arguments
---------
function : Callable
A defined function to get the source code for.
"""
if not isinstance(function, Callable):
raise TypeError(
f"Source lines can only be retrieved for Callable "
f"objects, not {type(function)}."
)

if "__closure__" in dir(function) and function.__closure__:
# Recursively dive down. the first closure cell value should be the
# next function down.
cell = function.__closure__[0].cell_contents

if isinstance(cell, Callable) and not isinstance(cell, Type):
return get_all_source(function.__closure__[0].cell_contents)

sourcelines = inspect.getsourcelines(function)

return sourcelines

0 comments on commit 4bb18d4

Please sign in to comment.