Analyzer::Python::PythonEngine
Inherits Analyzer < FileHelper < Reference < Object
Constants
Regex for valid Python module names
HTTP method names commonly used in REST APIs. query (RFC 10008) is
included for frameworks whose class-based-view dispatch is duck-typed
(Flask/Quart/Sanic call getattr(self, request.method.lower()), so a
def query(self): method genuinely serves QUERY requests).
HTTP_METHODS minus query. Analyzers pattern-match a bare method
name against source text in more than one shape — a def <verb>(...)
head inside a confirmed handler class, or a verb token appearing
anywhere in a decorator's raw text — and for two reasons a match on
query there is not reliable evidence of QUERY support:
- The framework dispatches via an explicit method allowlist rather
than duck-typing (Django's
http_method_names, Tornado'sSUPPORTED_METHODS), so a same-named helper method — a common name for search/filter logic — is genuinely inert, not a route. - The match is a bare-word scan of arbitrary decorator text, where
queryroutinely appears as an unrelated parameter name (e.g. DRF-spectacular'sOpenApiParameter("query", ...)).
Shared here so each consuming analyzer doesn't redefine the same subtraction with its own copy of the justification.
Indentation size in spaces; different sizes can cause analysis issues
name = at the head of a call argument, excluding ==/>=/<=/!=.
FastAPI / django-ninja parameter markers. q: str = Query("abc") does
not default q to the object Query("abc") — the marker is a
declaration, and the default it carries is its first positional
argument or its default= keyword.
Python spellings for "there is no value here". ... (Ellipsis) is how
FastAPI marks a required parameter, so it is an absence too.
Regex for valid Python variable names
Python's two triple-quote fences, spelled as an explicit array because
%w[""" '''] reads as a %w element containing quote characters.
Class methods
Standard Python/pytest/unittest test-file conventions. A file
under any of these patterns ships with python -m pytest or
python -m unittest and never serves real traffic in
production. Centralized so every analyzer can opt in via
next if python_test_path?(path).
/tests/,/test/— pytest discovery defaults and common framework fixture packages (Django, Litestar, FastAPI all use variants)tests.py— the legacy Django per-app test moduletest_*.py— unittest / pytest default discovery*_test.py— pytest-go style suffix (rare in Python, but cheap to include)
Instance methods
Build 1-hop callees observed in body (a handler's Python
source). body_start_line is the 0-based file line at which
body's first character sits, so tree-sitter rows can be
translated into absolute call-site lines (1-indexed). Use when
one body maps to multiple endpoints (e.g. Sanic's multi-method
routes) so the tree-sitter parse happens once and the same
Callee list gets pushed onto each endpoint.
Note: callers using parse_code_block(lines[def_idx..]) should
pass def_idx because that helper keeps the def line. Callers
using extract_function_body(lines, def_idx) should pass
def_idx + 1 because that helper skips the def line.
When definition_base_path is provided, callees with reachable
same-file or imported Python definitions are rewritten to that
definition location; unresolved callees keep their call-site
path/line.
Walk forward from decorator_line past any stacked decorators,
decorator continuation lines, blank lines, and comments to the
actual def / async def that they apply to. Returns the 0-based
line of the def, or nil if none is found before a non-decorator/
non-blank statement.
This exists because real-world Python decorator stacks
(@app.post(...) + @auth_required, multi-line route decorators,
blank-line spacers, or a # comment between the route decorator
and the def) make the "def is at decorator_line + 1" assumption
silently wrong — both for parameter extraction and for handler-body
parsing.
Resolve every import and from … import … in the file to
{name => {filepath, package_type}}. Thin delegator over
Noir::ImportGraph::Python.find_imported_modules so future
Python analyzers (or new tagger logic) can call the resolver
directly without going through PythonEngine.
See find_imported_modules — same delegator.
Finds all parameters in JSON objects within a given code block
Given a 0-based index into lines whose content line is
known to open a Python call whose ( is unbalanced, join
continuation lines until the running paren delta drops to ≤ 0.
Returns the joined string with newlines collapsed to single
spaces so analyzer-side regexes don't need a multi-line flag.
The caller is expected to short-circuit (return line) when
the call already balances on the same line — this method
always walks forward at least once.
Parses a function or class definition from a string or an array of strings
Parses the definition of a function from the source lines starting at a given index
Convenience wrapper around build_callees_from: parse + push in
one call when one body maps to exactly one endpoint.
Endpoint#push_callee enforces dedup and the per-endpoint cap.
Net [ − ] count on a single Python source line.
Used where the construct being followed is a collection literal
rather than a call: Django's urlpatterns = [...] / inline
include([...]), aiohttp's route-table lists, and the
Flask / Quart collection assignments that add both deltas
together to follow a mixed [... (...) ...] continuation.
Net ( − ) count on a single Python source line.
Used by analyzers that walk source line-by-line and need to
join continuation lines into one logical call (e.g. multi-line
decorators in FastAPI / Litestar / Sanic / Bottle, multi-line
Route(...) entries in Starlette).
Number of physical lines a def/class signature occupies at the
head of lines, i.e. up to and including the line that carries the
suite-introducing : at bracket depth 0. A single-line header
returns 1. Used by parse_code_block to avoid treating a wrapped
signature's continuation lines (notably the ) -> T: closer at
column 0) as the end of the body.
The request-ready value a Python parameter default evaluates to.
Anything that is not a literal has no value, and returning the source
text instead is worse than returning nothing: value is what the
curl / httpie / PowerShell / Postman / OpenAPI builders put on the
wire, so a leaked expression became -H 'x_token: Header(None)' and
?q=Query(). Callers only ever ask this to turn a default into a
value, so a non-literal answers with the empty string — the same thing
a parameter with no default at all produces.