├── tests ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-39.pyc │ └── test_nestd.cpython-39-pytest-5.4.3.pyc └── test_nestd.py ├── .gitignore ├── pyproject.toml ├── LICENCE.md ├── README.md ├── nestd └── __init__.py └── poetry.lock /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *dist 2 | *__pycache__ 3 | -------------------------------------------------------------------------------- /tests/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparckles/nestd/HEAD/tests/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /tests/__pycache__/test_nestd.cpython-39-pytest-5.4.3.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sparckles/nestd/HEAD/tests/__pycache__/test_nestd.cpython-39-pytest-5.4.3.pyc -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "nestd" 3 | version = "0.3.2" 4 | description = "A package to extract your nested functions!" 5 | authors = ["Sanskar Jethi "] 6 | readme = "README.md" 7 | classifiers = [ 8 | "Development Status :: 3 - Alpha", 9 | "Environment :: Web Environment", 10 | "Intended Audience :: Developers", 11 | "License :: OSI Approved :: BSD License", 12 | "Operating System :: OS Independent", 13 | "Topic :: Internet :: WWW/HTTP", 14 | "Programming Language :: Python :: 3", 15 | "Programming Language :: Python :: 3.7", 16 | "Programming Language :: Python :: 3.8", 17 | "Programming Language :: Python :: 3.9", 18 | "Programming Language :: Python :: 3.10", 19 | "Programming Language :: Python :: 3.11", 20 | "Programming Language :: Python :: Implementation :: CPython" 21 | ] 22 | 23 | [tool.poetry.dependencies] 24 | python = "^3.8" 25 | 26 | [tool.poetry.dev-dependencies] 27 | pytest = "^5.2" 28 | 29 | [build-system] 30 | requires = ["poetry-core>=1.0.0"] 31 | build-backend = "poetry.core.masonry.api" 32 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2023, Sanskar Jethi 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /tests/test_nestd.py: -------------------------------------------------------------------------------- 1 | from nestd import nested, get_all_nested, get_all_deep_nested 2 | import random 3 | 4 | 5 | def dummy_function(): 6 | test_variable = "hello, world" 7 | 8 | def inner_function(): 9 | nonlocal test_variable 10 | return test_variable 11 | 12 | 13 | def dummy_function_with_two_inner_functions(): 14 | test_variable = "hello, world" 15 | test_array = [1, 2, 3] 16 | 17 | def inner_function(): 18 | nonlocal test_variable 19 | return test_variable 20 | 21 | def inner_function_2(): 22 | nonlocal test_array 23 | return test_array[1:] 24 | 25 | 26 | def dummy_function_with_nested_inner_functions(): 27 | 28 | test_array = [1, 2, 3] 29 | 30 | def math(): 31 | nonlocal test_array 32 | 33 | def sum(): 34 | nonlocal test_array 35 | 36 | def sum_of_array(): 37 | nonlocal test_array 38 | inside_arr = [random.randint(1, 10)] * len(test_array) 39 | return test_array + inside_arr 40 | 41 | def multi_of_array(): 42 | nonlocal test_array 43 | inside_arr = [random.randint(1, 10)] * len(test_array) 44 | for i in range(len(test_array)): 45 | inside_arr[i] = inside_arr[i] * test_array[i] 46 | return inside_arr 47 | 48 | ans = 0 49 | for i in test_array: 50 | ans += i 51 | return ans 52 | 53 | def multiply(): 54 | nonlocal test_array 55 | ans = 1 56 | for i in test_array: 57 | ans = ans * i 58 | 59 | return ans 60 | 61 | return test_array 62 | 63 | def stats(): 64 | nonlocal test_array 65 | 66 | def mean(): 67 | nonlocal test_array 68 | return sum(test_array) / len(test_array) 69 | 70 | return test_array 71 | 72 | 73 | def test_nested_function(): 74 | inner_function = nested(dummy_function, "inner_function", test_variable="hello") 75 | assert "hello" == inner_function() 76 | 77 | 78 | def test_2_nested_functions(): 79 | all_inner_functions = get_all_nested( 80 | dummy_function_with_two_inner_functions, "hello_world", [1, 2] 81 | ) 82 | inner_function, inner_function_2 = all_inner_functions 83 | 84 | assert inner_function[0] == "inner_function" 85 | assert inner_function[1]() == "hello_world" 86 | 87 | assert inner_function_2[0] == "inner_function_2" 88 | assert inner_function_2[1]() == [2] 89 | 90 | 91 | def test_3_nested_functions(): 92 | inner_functions = get_all_deep_nested( 93 | dummy_function_with_nested_inner_functions, 94 | test_array=[1, 2, 3], 95 | ) 96 | 97 | assert inner_functions["math"]() == [1, 2, 3] 98 | assert inner_functions["sum"]() == 6 99 | assert inner_functions["mean"]() == 2.0 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nested 2 | 3 | Extract your nested functions! 4 | 5 | ## Installation 6 | 7 | ```python3 8 | pip install nestd 9 | ``` 10 | 11 | 12 | ## Usage 13 | 14 | ```python3 15 | from nestd import nested, get_all_nested 16 | 17 | 18 | def dummy_function(): 19 | test_variable = "hello, world" 20 | def inner_function(): 21 | nonlocal test_variable 22 | return test_variable 23 | 24 | 25 | def dummy_function_with_two_inner_functions(): 26 | test_variable = "hello, world" 27 | test_array = [1, 2, 3] 28 | def inner_function(): 29 | nonlocal test_variable 30 | return test_variable 31 | 32 | def inner_function_2(): 33 | nonlocal test_array 34 | return test_array[1:] 35 | 36 | 37 | def test_nested_function(): 38 | inner_function = nested(dummy_function, "inner_function", test_variable="hello" ) 39 | assert "hello" == inner_function() 40 | 41 | def test_2_nested_functions(): 42 | all_inner_functions = get_all_nested(dummy_function_with_two_inner_functions, "hello_world", [1,2]) 43 | inner_function, inner_function_2 = all_inner_functions 44 | 45 | assert inner_function[0] == "inner_function" 46 | assert inner_function[1]() == "hello_world" 47 | 48 | assert inner_function_2[0] == "inner_function_2" 49 | assert inner_function_2[1]() == [2] 50 | ``` 51 | 52 | 53 | To perform a very deep nested search 54 | 55 | ```python3 56 | def dummy_function_with_nested_inner_functions(): 57 | 58 | test_array = [1, 2, 3] 59 | 60 | def math(): 61 | nonlocal test_array 62 | 63 | def sum(): 64 | nonlocal test_array 65 | 66 | def sum_of_array(): 67 | nonlocal test_array 68 | inside_arr = [random.randint(1, 10)] * len(test_array) 69 | return test_array + inside_arr 70 | 71 | def multi_of_array(): 72 | nonlocal test_array 73 | inside_arr = [random.randint(1, 10)] * len(test_array) 74 | for i in range(len(test_array)): 75 | inside_arr[i] = inside_arr[i] * test_array[i] 76 | return inside_arr 77 | 78 | ans = 0 79 | for i in test_array: 80 | ans += i 81 | return ans 82 | 83 | def multiply(): 84 | nonlocal test_array 85 | ans = 1 86 | for i in test_array: 87 | ans = ans * i 88 | 89 | return ans 90 | 91 | return test_array 92 | 93 | def stats(): 94 | nonlocal test_array 95 | 96 | def mean(): 97 | nonlocal test_array 98 | return sum(test_array) / len(test_array) 99 | 100 | return test_array 101 | 102 | 103 | def test_3_nested_functions(): 104 | inner_functions = get_all_deep_nested( 105 | dummy_function_with_nested_inner_functions, 106 | test_array=[1, 2, 3], 107 | ) 108 | 109 | assert inner_functions["math"]() == [1, 2, 3] 110 | assert inner_functions["sum"]() == 6 111 | assert inner_functions["mean"]() == 2.0 112 | 113 | ``` 114 | 115 | ## Contributor Guidelines 116 | 117 | Feel free to open an issue for any clarification or for any suggestions. 118 | 119 | 120 | ## To Develop Locally 121 | 122 | 1. `poetry install` to install the dependencies 123 | 2. `pytest tests` to run the tests 124 | -------------------------------------------------------------------------------- /nestd/__init__.py: -------------------------------------------------------------------------------- 1 | """Gain access to the code objects of inner functions for testing purposes.""" 2 | import types 3 | from pprint import pprint 4 | 5 | 6 | def free_var(val): 7 | """A function that wraps free variables.""" 8 | 9 | def nested(): 10 | return val 11 | 12 | return nested.__closure__[0] 13 | 14 | 15 | def nested(outer, inner_name, **free_vars): 16 | """Find the code object of an inner function and return it as a callable object. 17 | 18 | Arguments: 19 | outer (function or method): A function object with an inner function. 20 | inner_name (str): The name of the inner function we want access to 21 | **free_vars (dict(str: any)): A dictionary with values for the free 22 | variables in the context of the inner function. 23 | Returns: 24 | A function object for the inner function, with context variables set. 25 | """ 26 | if not isinstance(outer, (types.FunctionType, types.MethodType)): 27 | raise Exception("Outer function is not a function or a method type") 28 | 29 | outer = outer.__code__ 30 | 31 | for const in outer.co_consts: 32 | if isinstance(const, types.CodeType) and const.co_name == inner_name: 33 | # just need to check why the free_var call is required 34 | # update the documentation of the free_var call 35 | return types.FunctionType( 36 | const, 37 | globals(), 38 | None, 39 | None, 40 | tuple(free_var(free_vars[name]) for name in const.co_freevars), 41 | ) 42 | 43 | 44 | def get_all_nested(fx, *context_vars): 45 | """Return all nested functions of a function. 46 | 47 | Arguments: 48 | fx (function or method): A function object with inner function(s). 49 | *context_vars: context variables corressponding inner functions in the order of occurence. 50 | 51 | Returns: 52 | A list of tuples, with the first element as the function name and the second element as function object. 53 | e.g. [("inner_function", ), ....] 54 | """ 55 | if not isinstance(fx, (types.FunctionType, types.MethodType)): 56 | raise Exception("Supplied param is not a function or a method type") 57 | 58 | fx = fx.__code__ 59 | context_variables = list(context_vars) 60 | 61 | output = [] 62 | for const in fx.co_consts: 63 | if isinstance(const, types.CodeType): 64 | context = tuple( 65 | free_var(context_variables[0]) for name in const.co_freevars 66 | ) 67 | context_variables = context_variables[1:] 68 | output.append( 69 | ( 70 | const.co_name, 71 | types.FunctionType(const, globals(), None, None, context), 72 | ) 73 | ) 74 | 75 | return output 76 | 77 | 78 | def get_all_deep_nested(fx, dict={}, **free_vars): 79 | """Find the code object of an inner function recursively and return it as a callable object. 80 | 81 | Arguments: 82 | fx (function or method): A function object with an inner function. 83 | dict a Dictionary by default set to be empty to storing the values in recursion. 84 | **free_vars (dict(str: any)): A dictionary with values for the free 85 | variables in the context of the inner function. 86 | Returns: 87 | A dictionary with Key as Function Name and Value as Function Object 88 | e.g. {"inner_funciton":,.....} 89 | """ 90 | if not isinstance(fx, (types.FunctionType, types.MethodType)): 91 | raise Exception("Supplied param is not a function or a method type") 92 | 93 | fx = fx.__code__ 94 | for const in fx.co_consts: 95 | if isinstance(const, types.CodeType): 96 | fun = types.FunctionType( 97 | const, 98 | globals(), 99 | None, 100 | None, 101 | tuple(free_var(free_vars[name]) for name in const.co_freevars), 102 | ) 103 | dict[const.co_name] = fun 104 | get_all_deep_nested(fun, dict, **free_vars) 105 | 106 | return dict 107 | 108 | 109 | __version__ = "0.3.0" 110 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "atomicwrites" 3 | version = "1.4.1" 4 | description = "Atomic file writes." 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 8 | 9 | [[package]] 10 | name = "attrs" 11 | version = "21.4.0" 12 | description = "Classes Without Boilerplate" 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 16 | 17 | [package.extras] 18 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] 19 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 20 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] 21 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] 22 | 23 | [[package]] 24 | name = "colorama" 25 | version = "0.4.5" 26 | description = "Cross-platform colored terminal text." 27 | category = "dev" 28 | optional = false 29 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 30 | 31 | [[package]] 32 | name = "more-itertools" 33 | version = "8.13.0" 34 | description = "More routines for operating on iterables, beyond itertools" 35 | category = "dev" 36 | optional = false 37 | python-versions = ">=3.5" 38 | 39 | [[package]] 40 | name = "packaging" 41 | version = "21.3" 42 | description = "Core utilities for Python packages" 43 | category = "dev" 44 | optional = false 45 | python-versions = ">=3.6" 46 | 47 | [package.dependencies] 48 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 49 | 50 | [[package]] 51 | name = "pluggy" 52 | version = "0.13.1" 53 | description = "plugin and hook calling mechanisms for python" 54 | category = "dev" 55 | optional = false 56 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 57 | 58 | [package.extras] 59 | dev = ["pre-commit", "tox"] 60 | 61 | [[package]] 62 | name = "py" 63 | version = "1.11.0" 64 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 65 | category = "dev" 66 | optional = false 67 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 68 | 69 | [[package]] 70 | name = "pyparsing" 71 | version = "3.0.9" 72 | description = "pyparsing module - Classes and methods to define and execute parsing grammars" 73 | category = "dev" 74 | optional = false 75 | python-versions = ">=3.6.8" 76 | 77 | [package.extras] 78 | diagrams = ["railroad-diagrams", "jinja2"] 79 | 80 | [[package]] 81 | name = "pytest" 82 | version = "5.4.3" 83 | description = "pytest: simple powerful testing with Python" 84 | category = "dev" 85 | optional = false 86 | python-versions = ">=3.5" 87 | 88 | [package.dependencies] 89 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 90 | attrs = ">=17.4.0" 91 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 92 | more-itertools = ">=4.0.0" 93 | packaging = "*" 94 | pluggy = ">=0.12,<1.0" 95 | py = ">=1.5.0" 96 | wcwidth = "*" 97 | 98 | [package.extras] 99 | checkqa-mypy = ["mypy (==v0.761)"] 100 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 101 | 102 | [[package]] 103 | name = "wcwidth" 104 | version = "0.2.5" 105 | description = "Measures the displayed width of unicode strings in a terminal" 106 | category = "dev" 107 | optional = false 108 | python-versions = "*" 109 | 110 | [metadata] 111 | lock-version = "1.1" 112 | python-versions = "^3.8" 113 | content-hash = "c27944f25b55067b06883f1cea204be7d97841a4b8228fab69b91895347494ad" 114 | 115 | [metadata.files] 116 | atomicwrites = [ 117 | {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, 118 | ] 119 | attrs = [ 120 | {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, 121 | {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, 122 | ] 123 | colorama = [ 124 | {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, 125 | {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, 126 | ] 127 | more-itertools = [ 128 | {file = "more-itertools-8.13.0.tar.gz", hash = "sha256:a42901a0a5b169d925f6f217cd5a190e32ef54360905b9c39ee7db5313bfec0f"}, 129 | {file = "more_itertools-8.13.0-py3-none-any.whl", hash = "sha256:c5122bffc5f104d37c1626b8615b511f3427aa5389b94d61e5ef8236bfbc3ddb"}, 130 | ] 131 | packaging = [ 132 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 133 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 134 | ] 135 | pluggy = [ 136 | {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, 137 | {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, 138 | ] 139 | py = [ 140 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 141 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 142 | ] 143 | pyparsing = [ 144 | {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, 145 | {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, 146 | ] 147 | pytest = [ 148 | {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, 149 | {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, 150 | ] 151 | wcwidth = [ 152 | {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, 153 | {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, 154 | ] 155 | --------------------------------------------------------------------------------