duck.contrib.reloader.dependency_graphยถ

Provides a class to build a reverse dependency graph for Python modules, track runtime/dynamic imports lazily, and determine safe reload order.

Intended for use in hot-reload systems, e.g., DuckSightReloader.


Key Concepts:

  1. Reverse Dependency Graph

    • Maps a module to the set of modules that depend on it.

    • Example: module_a.py module_b.py -> imports module_a module_c.py -> imports module_b

      Reverse graph:

      {
          "module_a": {"module_b"},
          "module_b": {"module_c"}
      }
      
    • If module_a changes, we reload module_b and module_c in safe order.

  2. Static Imports

    • Collected using AST parsing from the source code.

    • Detects top-level and nested imports (in functions/classes).

  3. Runtime Imports

    • Captured dynamically by patching Pythonโ€™s built-in import.

    • Tracks imports that happen inside setup functions or dynamically loaded code.

  4. Safe Reload Workflow

    • Initialize DependencyGraph before importing your main app.

    • Optionally pre-build the graph for all files at startup.

    • On file change:

      1. Lazily build graph for changed file

      2. Merge into global graph

      3. Compute affected modules with get_modules_to_reload()

      4. Reload affected modules using importlib.reload()

      5. Apply SETTINGS.reload() or other mutable object updates

    • Repeat for subsequent file changes.

  5. Usage Summary:

    from duck.contrib.reloader.dependency_graph import DependencyGraph
    
    # Initialize BEFORE importing app
    graph = DependencyGraph(project_root=".")
    
    # Optional: pre-build graph for all project files
    for py_file in Path(".").rglob("*.py"):
        local_graph, _ = graph.build_graph_for_file(str(py_file))
        graph.merge_graph(local_graph)
    
    # Import app after graph is ready
    from duck.app import App
    app = App()
    app.run()
    
    # On file change:
    local_graph, changed_module = graph.build_graph_for_file(changed_file)
    graph.merge_graph(local_graph)
    affected = graph.get_modules_to_reload(changed_module)
    for mod_name in affected:
        importlib.reload(sys.modules[mod_name])
    
  6. Notes:

    • Always initialize DependencyGraph before any other imports to capture runtime imports.

    • Supports lazy updates: only rebuild for changed files, not the whole project.

    • Leaf-first reload order ensures safe reloading of dependencies.

    • Runtime imports captured automatically; manual additions possible via add_runtime_dependency().


Author: Brian Musakwa

Module Contentsยถ

Classesยถ

DependencyGraph

Tracks module dependencies (static and runtime) and provides methods to determine safe reload order for affected modules.

APIยถ

class duck.contrib.reloader.dependency_graph.DependencyGraph(project_root: str = '.')[source]ยถ

Tracks module dependencies (static and runtime) and provides methods to determine safe reload order for affected modules.

Initialization

Initialize the dependency graph.

Parameters:

project_root โ€“ Root directory of the project for module path resolution.

__repr__()[source]ยถ
_tracked_import(name, globals=None, locals=None, fromlist=(), level=0)[source]ยถ

Intercepts dynamic imports and updates the dependency graph lazily.

Parameters:

name โ€“ Name of the module being imported.

add_runtime_dependency(module_name: str, imported_module: str)[source]ยถ

Manually add a runtime dependency (optional).

Parameters:
  • module_name โ€“ The module that imported another.

  • imported_module โ€“ The module that was imported.

build_graph_for_file(file_path: str) โ†’ tuple[dict, str][source]ยถ

Build a reverse dependency graph for a specific file lazily.

Parameters:

file_path โ€“ File to parse.

Returns:

Local graph {imported_module: set([current_module])}, Current module name

Return type:

tuple[dict, str]

get_modules_to_reload(changed_module: str) โ†’ list[str][source]ยถ

Return a list of modules affected by a changed module, in safe reload order (leaf-first).

Parameters:

changed_module โ€“ Module that has changed.

Returns:

Modules to reload in order.

Return type:

list[str]

merge_graph(local_graph: dict)[source]ยถ

Merge a local graph into the global reverse dependency graph.

Parameters:

local_graph โ€“ {imported_module: set([dependent_modules])}

module_name_from_path(file_path: str) โ†’ str[source]ยถ

Convert a file path to a Python module name relative to project root.

Parameters:

file_path โ€“ Path to the Python file.

Returns:

Python module name (dot-separated).

Return type:

str

static parse_imports(source: str) โ†’ set[source]ยถ

Parse all imported module names in a Python source file.

Parameters:

source โ€“ Python source code.

Returns:

Names of imported modules.

Return type:

set

restore_import()[source]ยถ

Restore the original built-in import function.

stop()[source]ยถ

Cleanup the DependancyGraph by restoring the default __import__ function.