Skip to main content

Analyze Python functions and extract dependency source for reconstruction

Project description

PyPI Python 3.10+ License Code style: ruff CI CodeFactor Download Stats Documentation

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:

  1. 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.
  2. reconstruct(results) — one concatenated module string (imports, globals, definitions, optional shims) in dependency order, ready for exec().

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} with type: "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:

  1. Environment directories — paths from sysconfig.get_paths(), site.getsitepackages(), and site.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 in external_vars.
  2. exclude_dirs — additional absolute subtrees to skip. Recursion into those files stops; IDs from excluded code may still appear in external_vars, so dependencies are not silently forgotten.
  3. include_dirs — if not None, 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:

  1. 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 build types.SimpleNamespace shims so attribute access like prim.clamp still resolves after definitions exist.
  2. Variablesvariable entries become name = <repr> assignments.
  3. 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.
  4. 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 bare scale to a canonical choice and exposes the others as scale__<module_slug>.
  5. 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 inspect cases) 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)

  1. Configure trusted publishing on PyPI for this repository and workflow .github/workflows/publish.yml (optional GitHub Environment pypi).
  2. 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 matching v*.

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

funcsnap-0.0.1.dev3.tar.gz (29.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

funcsnap-0.0.1.dev3-py3-none-any.whl (11.6 kB view details)

Uploaded Python 3

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

Hashes for funcsnap-0.0.1.dev3.tar.gz
Algorithm Hash digest
SHA256 ba8df3c1072c8522090c32ebf64de82470ee4c8809ba9c0dc8d0f005dec10a5c
MD5 65ba70381f40e81c1f23e706f61548e0
BLAKE2b-256 7eceec97d5910356d7c39a991cfe41bfe946c349a3ae9f8c1471310315b98057

See more details on using hashes here.

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

Hashes for funcsnap-0.0.1.dev3-py3-none-any.whl
Algorithm Hash digest
SHA256 9b4e49d8933e5096a5055a06446d89d460a39b841ddd9ff7278296f6377239a7
MD5 fab3cfa3551e53f646604e16d8263b7d
BLAKE2b-256 4f424a16b36619740c2369b0b49c085b45ae19bfe358813ffd0159915e03a8dd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page