Analyze Python functions and extract dependency source for reconstruction
Project description
funcsnap
funcsnap takes a live Python function or method, walks the objects it closes over, and collects enough information to reconstruct a self-contained Python source string you can exec() or save as a single file.
You get two complementary outputs:
get_source_functions(func, ...)— a flat dictionary keyed by stable IDs, with source text, imports, and dependency edges (external_vars). Use this when you want to inspect the graph, filter it, or emit your own format.reconstruct(results)— one concatenated module string (imports, globals, definitions, optional shims) in dependency order, ready forexec().
By default, anything installed in the interpreter environment (stdlib, site-packages, venv layout) is not expanded into separate definitions; those dependencies remain references so you do not accidentally inline large third-party trees.
Install
pip install funcsnap
Quick start
from funcsnap import get_source_functions, reconstruct
results = get_source_functions(your_func)
code = reconstruct(results)
namespace = {}
exec(code, namespace)
# Call the emitted names from `namespace` (usually the function’s short name).
Public API: get_source_functions, reconstruct (funcsnap/__init__.py).
get_source_functions — result shape
The return value is a dict[str, dict]: a flat map, not a nested tree. The “tree” is the graph you get by following each entry’s external_vars list.
Keys (IDs)
- Functions:
{module}.{function_name}(e.g.mypkg.util.foo) - Classes:
{module}.{ClassName} - Captured globals (non-function, non-class):
{module}.{name}withtype: "variable"
Entry fields
| Field | Used for | Meaning |
|---|---|---|
type |
all | "function", "class", or "variable" |
value |
all | Source text for functions/classes; repr(value) text for variables |
file |
function, class | Path to the defining .py file (when known) |
imports |
function, class | Top-level import lines from that file that bind names used in the snippet |
external_vars |
function, class | List of other result keys or module names this piece depends on |
Packages under the environment roots (see Path filters) are usually not given their own keys; they still appear in external_vars (e.g. a numpy reference) so you know what is missing from the bundle.
Worked example
Save as demo.py and run python demo.py (a real file is needed so source can be read reliably):
from funcsnap import get_source_functions
SCALE = 10
def helper(x):
return x * SCALE
def root(y):
return helper(y) + 1
if __name__ == "__main__":
results = get_source_functions(root)
print(sorted(results.keys()))
You should see something like:
['__main__.SCALE', '__main__.helper', '__main__.root']
One entry (the root function) looks like this (ellipsis added for readability):
results["__main__.root"] == {
"type": "function",
"value": "def root(y):\n return helper(y) + 1\n",
"file": "/path/to/demo.py",
"imports": [],
"external_vars": ["__main__.helper"],
}
The captured constant:
results["__main__.SCALE"] == {
"type": "variable",
"value": "10", # repr(SCALE)
}
helper links to SCALE via its own external_vars; root links to helper. That list is what reconstruct() uses to order definitions.
Optional: narrowing with include_dirs
If you only want to recurse into code under certain roots, pass absolute directories. The entrypoint function you pass in is always recorded; dependencies are only expanded when their defining file lies under one of the include paths (after the default environment skip).
import tempfile
from funcsnap import get_source_functions
def root(y):
return y + 1
# Empty directory: nothing under it matches dependency files → only the root remains.
with tempfile.TemporaryDirectory() as tmp:
results = get_source_functions(root, include_dirs=[tmp])
assert sorted(results.keys()) == ["__main__.root"]
See Path filtering for the full rules.
Path filtering: include_dirs, exclude_dirs, and the default environment skip
Resolution uses FunctionAnalyzer._should_analyze in this order:
- Environment directories — paths from
sysconfig.get_paths(),site.getsitepackages(), andsite.getusersitepackages(). If a dependency’s file path lies under one of these roots, funcsnap does not recurse into it (you do not pull in the stdlib or installed packages as inlined definitions). References can still appear inexternal_vars. exclude_dirs— additional absolute subtrees to skip. Recursion into those files stops; IDs from excluded code may still appear inexternal_vars, so dependencies are not silently forgotten.include_dirs— if notNone, a file is analyzed only when it falls under one of these directories (after step 1). The root function you pass in is still stored.
Use absolute paths; funcsnap normalizes with os.path.abspath.
reconstruct — single module string
reconstruct(results) turns the flat map into one string with labeled sections:
- Imports — needed import lines gathered from all entries. Imports are omitted when every name they bind will already be defined in the bundle. Relative imports are not emitted as-is (they break under a bare
exec()without package context); when needed, funcsnap may buildtypes.SimpleNamespaceshims so attribute access likeprim.clampstill resolves after definitions exist. - Variables —
variableentries becomename = <repr>assignments. - Definitions — functions and classes, ordered by a topological sort on
external_vars(Kahn’s algorithm), with a cycle fallback if the graph is not a strict DAG. - Name collisions — if the same short name (last segment of the key) appears for more than one full ID, definitions are emitted under prefixed names such as
_mylib_core_primitives__scale, derived from the module path. A trailing collision alias block sets the barescaleto a canonical choice and exposes the others asscale__<module_slug>. - Module shims — emitted after definitions when relative-import shims need already-defined names.
Safety / correctness: these steps are about making the bundle importable and consistent (renames, import elision, avoiding broken relative imports). They are not a sandbox: never exec() untrusted code you did not audit.
Example continuation from the worked example: reconstruct(results) can produce:
# --- Variables ---
SCALE = 10
# --- Definitions ---
def helper(x):
return x * SCALE
def root(y):
return helper(y) + 1
You can exec that string, then call namespace["root"](...) (or write it to a .py file and run it).
Limitations
- Dynamic or opaque objects (C extensions, unusual
inspectcases) may not yield usable source. - Relative imports inside the original package need special handling;
reconstruct()avoids emitting them verbatim and may use shims, but complex package layouts can still require manual fixes. - The analyzer follows static name use in the source text plus runtime globals; highly dynamic patterns may be incomplete.
Development
Clone the repo, create a virtual environment, then:
pip install -e ".[dev]"
ruff check .
pytest
Version strings are derived from Git tags (hatch-vcs). Without a repository or tags, builds use the fallback version from pyproject.toml.
PyPI releases (maintainers)
- Configure trusted publishing on PyPI for this repository and workflow
.github/workflows/publish.yml(optional GitHub Environmentpypi). - Merge your changes to
main, then tag and push:git tag v0.1.0 && git push origin v0.1.0(use the next semantic version). The publish workflow runs on tags matchingv*.
Ensure [project.urls] Repository in pyproject.toml points at https://github.com/kyrylo-gr/funcsnap so PyPI metadata matches the project home.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file funcsnap-0.0.1.dev3.tar.gz.
File metadata
- Download URL: funcsnap-0.0.1.dev3.tar.gz
- Upload date:
- Size: 29.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ba8df3c1072c8522090c32ebf64de82470ee4c8809ba9c0dc8d0f005dec10a5c
|
|
| MD5 |
65ba70381f40e81c1f23e706f61548e0
|
|
| BLAKE2b-256 |
7eceec97d5910356d7c39a991cfe41bfe946c349a3ae9f8c1471310315b98057
|
File details
Details for the file funcsnap-0.0.1.dev3-py3-none-any.whl.
File metadata
- Download URL: funcsnap-0.0.1.dev3-py3-none-any.whl
- Upload date:
- Size: 11.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9b4e49d8933e5096a5055a06446d89d460a39b841ddd9ff7278296f6377239a7
|
|
| MD5 |
fab3cfa3551e53f646604e16d8263b7d
|
|
| BLAKE2b-256 |
4f424a16b36619740c2369b0b49c085b45ae19bfe358813ffd0159915e03a8dd
|