-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #26 from teald/17-decorated-doors-are-not-correctl…
…y-parsed-by-basedoor Fixing bug with decorators; now use __closure__ to get wrapped function
- Loading branch information
Showing
4 changed files
with
211 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |