Skip to content

Python examples

These examples run against Flask, the sample project for the Python documentation. Each example calls one method of the CLDK Python SDK and shows the output of a real run over the Flask checkout. To use your own code, change project_path.

Clone the Flask repository.

Terminal window
git clone https://github.com/pallets/flask.git

Build a CLDK analysis object that points at the checkout. The analysis_level parameter controls the depth of the analysis. symbol_table, the default, finds modules, classes, and methods. call_graph adds the caller and callee edges. Every example on this page uses the call_graph level.

from cldk import CLDK
from cldk.analysis import AnalysisLevel
analysis = CLDK.python(
project_path="flask",
analysis_level=AnalysisLevel.call_graph,
)

For the backend configuration, see codeanalyzer-python.

Symbol table: modules, classes, and methods

Section titled “Symbol table: modules, classes, and methods”

The symbol table holds the modules, the classes, and the methods that the backend finds in the project.

Each analyzed Python file becomes one module, keyed by its path from the root of the repository.

modules = analysis.get_modules()
print(len(modules))
# 35
symbol_table = analysis.get_symbol_table()
for path in sorted(symbol_table)[:5]:
print(path)
# docs/conf.py
# examples/celery/make_celery.py
# examples/celery/src/task_app/__init__.py
# examples/celery/src/task_app/tasks.py
# examples/celery/src/task_app/views.py

The key of each class is its dotted signature from the root of the repository.

classes = analysis.get_classes()
print(len(classes))
# 52
for name in sorted(classes)[:5]:
print(name)
# src.flask.app.Flask
# src.flask.blueprints.Blueprint
# src.flask.cli.AppGroup
# src.flask.cli.CertParamType
# src.flask.cli.FlaskGroup

Pass the dotted signature to get_class to get one class object.

pyclass = analysis.get_class("src.flask.app.Flask")
print(len(pyclass.callables))
# 35
print(sorted(pyclass.callables)[:5])
# ['__call__', '__init__', '__init_subclass__', 'app_context', 'async_to_sync']

get_methods_in_class returns the methods that the class declares in its own body.

methods = analysis.get_methods_in_class("src.flask.blueprints.Blueprint")
print(sorted(methods))
# ['__init__', 'get_send_file_max_age', 'open_resource', 'send_static_file']

Pass the class signature and the method name to get_method.

src.flask.sansio.scaffold.Scaffold.route
method = analysis.get_method("src.flask.sansio.scaffold.Scaffold", "route")
print(method.signature)
print([parameter.name for parameter in method.parameters])
# ['self', 'rule', 'options']

get_constructors returns the constructors of a class, keyed by name.

constructors = analysis.get_constructors("src.flask.app.Flask")
print(list(constructors))
# ['__init__']

get_classes_by_criteria matches the class signatures against your inclusion strings.

src.flask.blueprints.Blueprint
blueprint_classes = analysis.get_classes_by_criteria(inclusions=["Blueprint"])
for name in sorted(blueprint_classes):
print(name)
# src.flask.sansio.blueprints.Blueprint
# src.flask.sansio.blueprints.BlueprintSetupState

get_imports returns the imports of every module, keyed by file path. Each import carries a name and a module.

imports = analysis.get_imports()
wrappers = imports["src/flask/wrappers.py"]
for imported in wrappers[:4]:
print(imported.name, "from", imported.module)
# annotations from __future__
# t from typing
# BadRequest from werkzeug.exceptions
# HTTPException from werkzeug.exceptions

Call graph: callers, callees, and reachability

Section titled “Call graph: callers, callees, and reachability”

The call graph is a networkx.DiGraph. Its edges point from the caller to the callee. Each edge carries a provenance value that names the technique that found the call. jedi marks an edge from symbol resolution, and defuse marks an edge from the def-use linker.

Call get_call_graph to get the graph of the whole project.

cg = analysis.get_call_graph()
print(cg.number_of_nodes(), "callables,", cg.number_of_edges(), "edges")
# 811 callables, 1250 edges

get_callers finds every callable that calls the target method. The ('defuse',) value on the edge means that the def-use linker found this call.

callers = analysis.get_callers(
target_class_name="src.flask.app.Flask",
target_method_declaration="full_dispatch_request",
)
print(callers["target_method"])
# src.flask.app.Flask.full_dispatch_request
for detail in callers["caller_details"]:
print(detail["caller_signature"], detail["edge"]["provenance"])
# src.flask.app.Flask.wsgi_app ('defuse',)

get_callees finds every callable that the source method calls. A signature that starts with can://flask/@external/ names a target outside the analyzed modules.

callees = analysis.get_callees(
source_class_name="src.flask.app.Flask",
source_method_declaration="full_dispatch_request",
)
for detail in callees["callee_details"]:
print(detail["callee_signature"], detail["edge"]["provenance"])
# can://flask/@external/_warnings/warn ('jedi',)
# can://flask/@external/flask.app.Flask/dispatch_request ('jedi',)
# can://flask/@external/flask.app.Flask/finalize_request ('jedi',)
# can://flask/@external/flask.app.Flask/handle_user_exception ('jedi',)
# can://flask/@external/flask.app.Flask/preprocess_request ('jedi',)
# src.flask.app.Flask.dispatch_request ('defuse',)
# src.flask.app.Flask.finalize_request ('defuse',)
# src.flask.app.Flask.handle_user_exception ('defuse',)
# src.flask.app.Flask.preprocess_request ('defuse',)
# can://flask/@external/src.flask.signals.request_started/send ('defuse',)

The nodes of the graph are the callable signatures, so networkx answers reachability questions directly.

import networkx as nx
cg = analysis.get_call_graph()
source = "src.flask.app.Flask.wsgi_app"
sink = "src.flask.app.Flask.dispatch_request"
print(nx.has_path(cg, source, sink))
# True
edge = cg["src.flask.app.Flask.full_dispatch_request"][sink]
print(edge["type"], edge["weight"], edge["provenance"])
# CALL_DEP 1 ('defuse',)

CLDK returns typed model objects. Inspect these objects for the detail that the plain names do not carry.

A PyClass object holds the methods, the base classes, and the decorators of the class.

pyclass = analysis.get_class("src.flask.app.Flask")
print(pyclass.base_classes)
# ['App']
print(sorted(pyclass.callables)[-4:])
# ['test_request_context', 'update_template_context', 'url_for', 'wsgi_app']

A PyCallable object holds the signature, the parameters, the decorators, and the source span.

method = analysis.get_method("src.flask.sansio.scaffold.Scaffold", "route")
print(method.name, method.kind)
# route function
print(method.return_type)
# t.Callable[[T_route], T_route]
print([decorator.name for decorator in method.decorators])
# ['setupmethod']
print([(parameter.name, parameter.type) for parameter in method.parameters])
# [('self', 'Scaffold'), ('rule', 'str'), ('options', 't.Any')]
print(method.start_line, method.end_line)
# 344 373

get_method_bodies returns the source of each signature that you give it.

bodies = analysis.get_method_bodies(["src.flask.sansio.scaffold.Scaffold.route"])
print(bodies["src.flask.sansio.scaffold.Scaffold.route"])
# def route(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]:
# """Decorate a view function to register it with the given URL
# rule and options. Calls :meth:`add_url_rule`, which has more
# details about the implementation.
# ...
# """
#
# def decorator(f: T_route) -> T_route:
# endpoint = options.pop("endpoint", None)
# self.add_url_rule(rule, endpoint, f, **options)
# return f
#
# return decorator

get_sub_classes returns the classes that name the given class as a base class.

src.flask.sansio.app.App
subclasses = analysis.get_sub_classes("src.flask.sansio.scaffold.Scaffold")
for name in sorted(subclasses):
print(name)
# src.flask.sansio.blueprints.Blueprint

get_extended_classes returns the base class names. It gives the same list as the base_classes attribute of the class object.

bases = analysis.get_extended_classes("src.flask.app.Flask")
print(bases)
# ['App']

The backend marks the callables that code outside the project calls, such as CLI commands and route handlers. The get_entrypoints method is part of CLDK 2.0. The stable PyPI release is 1.5.0. Install the pre-release with pip install --pre cldk.

get_entrypoints returns one overview object for each callable that the backend marked as an entry point.

src.flask.cli.run_command
entrypoints = analysis.get_entrypoints()
for entrypoint in entrypoints[:5]:
print(entrypoint.signature)
# src.flask.cli.shell_command
# src.flask.cli.routes_command
# examples.tutorial.flaskr.db.init_db_command
# examples.tutorial.flaskr.blog.index