class

Analyzer::Python::Django

Inherits Analyzer::Python::PythonEngine < Analyzer < FileHelper < Reference < Object

path("tenants/<int:pk>/", include(get_model_urls("tenancy", "tenant"))): a urlconf entry mounting the url list a project function builds out of a view registry, which decorators on the view classes fill at import time (@register_model_view(Tenant, "edit")). Neither name means anything to this code: the builder is recognised by its body (it subscripts a module-level registry with its own parameters, filters the entries on one, and makes a path() from another), the writer by appending a dict to that same registry slot, and each decorator application is evaluated against the writer's signature.

Constants

ACTION_BRANCH_RE = /\A(?:if|elif)\s+self\.action\s*(==|in)\s*(.+?):\s*(.*)\z/
APP_CONFIG_LABEL_RE = /\bapps\.get_app_config\s*\(\s*['"]([^'"]+)['"]\s*\)/
DECORATOR_METHOD_NAME_REGEXES = HTTP_METHODS_EXCLUDING_QUERY.to_h do |m| {m, /[^a-zA-Z0-9](#{m})[^a-zA-Z0-9]/} end

HTTP_METHODS is a fixed table (from PythonEngine), so the decorator-stack scan and the request.method == "..." scan below (each an .each loop run per decorator line / per function-body line) can look up a precompiled regex per method name instead of interpolating and recompiling one on every iteration. Purely a lookup-table hoist — the matched text/capture groups are unchanged, so it doesn't affect the line/offset values computed elsewhere in this file.

DECORATOR_METHOD_NAME_REGEXES also uses HTTP_METHODS_EXCLUDING_QUERY, for the OTHER reason that constant documents: it bare-word-matches a method name anywhere in a decorator's text (this scan is not CBV- specific — it runs over the decorator stack above a plain function too), and real decorators (e.g. DRF-spectacular's OpenApiParameter("query", ...)) commonly carry query as a parameter name rather than a verb restriction. REQUEST_METHOD_NAME_REGEXES keeps the full list: it only fires on an explicit, quoted request.method == "QUERY" comparison, which is unambiguous developer-written evidence, not a name collision.

DICT_KEY_RE = /\[\s*['"](\w+)['"]\s*\]/
DJANGO_AUTH_FORM_FIELDS = {"AuthenticationForm" => ["username", "password"] of ::String, "PasswordChangeForm" => ["old_password", "new_password1", "new_password2"] of ::String, "SetPasswordForm" => ["new_password1", "new_password2"] of ::String, "AdminPasswordChangeForm" => ["password1", "password2"] of ::String, "PasswordResetForm" => ["email"] of ::String, "UserCreationForm" => ["username", "password1", "password2"] of ::String, "BaseUserCreationForm" => ["username", "password1", "password2"] of ::String}

The input fields of the forms django.contrib.auth.forms ships, which the scan never holds.

DJANGO_AUTH_FORM_IMPORTS = DJANGO_AUTH_FORM_FIELDS.keys.to_h do |name| {name, /^\s*from\s+django\.contrib\.auth\.forms\s+import\s+(?:\([^)]*|[^\n]*)\b#{name}\b/m} end
DJANGO_BUILTIN_VIEW_METHODS = {"LoginView" => ["GET", "POST"] of ::String, "LogoutView" => ["GET", "POST"] of ::String, "PasswordChangeView" => ["GET", "POST"] of ::String, "PasswordChangeDoneView" => ["GET"] of ::String, "PasswordResetView" => ["GET", "POST"] of ::String, "PasswordResetDoneView" => ["GET"] of ::String, "PasswordResetConfirmView" => ["GET", "POST"] of ::String, "PasswordResetCompleteView" => ["GET"] of ::String, "TemplateView" => ["GET"] of ::String, "RedirectView" => ["GET"] of ::String}

Verbs Django's own class-based views accept, for a urlconf that mounts one directly (auth_views.PasswordResetView.as_view()): the class lives in site-packages, outside the scan, so its body cannot be read. LogoutView answered GET too below Django 5.

DRF_ACTION_DECORATOR_RE = /@(?:[A-Za-z_][A-Za-z0-9_]*\.)?(action|list_route|detail_route)\s*\(/

@action(...), and the @list_route(...) / @detail_route(...) it replaced in DRF 3.8 (still the API of projects that vendor DRF, like Taiga): a list route is detail=False, a detail route detail=True.

DRF_HTTP_METHOD_NAMES_RE = /^\s+http_method_names\s*=\s*[\[(]([^\])]*)[\])]/m
DRF_REGISTER_ASSIGN_RE = /\A\s*([A-Za-z_]\w*)\s*=\s*([A-Za-z_]\w*)\s*\.\s*register\s*\(/
DRF_ROUTE_MAPPING_SET_RE = /\broutes\s*\[\s*([02])\s*\]\s*\.\s*mapping\s*\[\s*['"](\w+)['"]\s*\]\s*=\s*['"](\w+)['"]/
DRF_ROUTE_MAPPING_UPDATE_RE = /\broutes\s*\[\s*([02])\s*\]\s*\.\s*mapping\s*\.\s*update\s*\(\s*\{([^}]*)\}/

Verbs a project router class adds to DRF's own route table, as {detail?, verb, viewset method}: NetBox-style bulk operations, self.routes[0].mapping.update({"put": "bulk_update"}). Index 0 of SimpleRouter.routes is the list route, 2 the detail route.

DRF_SLASHLESS_ROUTER_RE = /\b([A-Za-z_][A-Za-z0-9_]*)\s*(?::[^=\n]+)?=\s*[A-Za-z_][\w.]*Router\s*\([^)]*\btrailing_slash\s*=\s*False\b/

router = routers.DefaultRouter(trailing_slash=False)

DRF_TOKEN_VIEW_RE = /\b(?:ObtainAuthToken|Token(?:ObtainPair|ObtainSliding|Refresh|RefreshSliding|Verify|Blacklist)View)\b/

http_method_names = ["get", "post"] on a view class.

FORM_BINDING_RE = /(?<![\w.])([A-Za-z_][\w.]*)\s*\(\s*(?:[^()]*?,\s*)?(?:data\s*=\s*)?request\.(POST|GET)\b(?!\s*\.|\s*\[)/

WebhookForm(request.POST) / SearchForm(data=request.GET): a form bound to the request reads every field its class declares, from the body or the query string. Only a class the scan holds counts. ponytail: declared fields only; a ModelForm's Meta.fields columns are not resolved. Earlier arguments may come first: AuthenticationForm(request, data=request.POST), PasswordChangeForm(user=request.user, data=...).

FORM_LIBRARY_BASES = ["Form", "ModelForm", "BaseForm", "BaseModelForm"] of ::String
FORM_LOCAL_BINDING_RE = /^\s*form\s*=\s*([A-Za-z_][\w.]*)\s*\(/m

The local form (what form.cleaned_data reads) is bound only to classes whose field set is closed, at least once to the request.

FSTRING_FIELD_RE = /\{([A-Za-z_]\w*)\}/
FSTRING_ROUTE_RE = /(?<![\w'"])[rR]?[fF][rR]?(['"])([^'"\n]*\{[A-Za-z_]\w*\}[^'"\n]*)\1/

path(f"{prefix}admin/login/", view): an f-string route interpolating a module-level string reads that string, its first top-level binding (the default deployment, as path(prefix, include(...)) mounts at ""). A literal with any other interpolation is left as written.

GET_SERIALIZER_CLASS_DEF_RE = /^\s+def\s+get_serializer_class\s*\(/
IMPORT_STRING_CALL = /(?<![\w.])(?:module_loading\.)?import_string\s*\(/
JSON_BODY_ALIAS_RE = /^\s*([A-Za-z_]\w*)\s*=\s*json\.loads\s*\(\s*request\.body\b[\w.()]*\s*\)\s*$/

data = json.loads(request.body) (or request.body.decode()): the local's top-level keys are JSON body fields.

MAPPING_HELPER_CALL_RE = /(?<![\w.])([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)?)\s*\(\s*(?:self\.)?request\.(GET|POST|data|query_params)\s*,\s*[rf]?['"]([^'"]+)['"]/

bool_from_request(request.GET, "include_predictions", False): a project function handed a request mapping and a key reads that key when its body looks the key up in the mapping (params.get(key, ...), params[key]), judged by its definition.

METHOD_BRANCH_RE = /^\s*(?:if|elif)\b.*(?<![\w.])request\.method\b/
MODEL_FIELD_RE = /^(\s+)([a-z_]\w*)\s*:\s*(?!ClassVar\b)\S/
MODEL_VALIDATE_RE = /(?<![\w.])([A-Za-z_][\w.]*)\.model_validate(?:_json)?\s*\(\s*(?:json\.loads\s*\(\s*)?request\.\w+/

Spec.model_validate(request.json) / Model.model_validate_json( request.body): a Pydantic model the view validates the request body with names the JSON fields it reads, its annotated attributes. ponytail: field names as declared; Field(alias=...) and alias generators are not applied.

MODULE_STRING_RE = /^([A-Za-z_]\w*)\s*(?::\s*str\s*)?=\s*[rR]?(['"])([^'"\n]*)\2\s*$/m
OUTPUT_ONLY_FIELD_RE = /\A(?:SerializerMethodField|ReadOnlyField|HiddenField|\w*IdentityField)\z/

The input fields class target and its project bases declare as name = <...>Field( / <...>Serializer( attributes, the read-only ones left out. Serializers and Django forms declare them alike. Field classes DRF never reads from input: HyperlinkedIdentityField (and project subclasses keeping the name) forces read_only.

PAGINATE_CALL_RE = /\bself\.paginate_queryset\s*\(/
PAGINATION_CLASS_RE = /^\s+pagination_class\s*=\s*([A-Za-z_][\w.]*)\s*$/m
PAGINATION_PARAM_ATTR = /^\s+((?:page|page_size|limit|offset|cursor)_query_param)\s*=\s*(?:['"](\w+)['"]|None)/m
RAW_BODY_RE = /(?<![\w.])request\.body\b/

Verb checks and request reads in a function view's (or a helper's) body lines. A view that reads the raw body takes one: unrestricted, it answers POST.

REGEX_CBV_METHOD_DEF = /\s+(?:async\s+)?def\s+(#{HTTP_METHODS_EXCLUDING_QUERY.join("|")})\s*\(/

def get(...) / async def post(...) method heads in class-based views. Precompiled — an interpolated literal would be recompiled on every line of every CBV class body. Uses HTTP_METHODS_EXCLUDING_QUERY (see PythonEngine): Django's CBV dispatch (View.dispatch) looks up request.method.lower() against the class's http_method_names allowlist, which does not include query upstream — unlike Flask's duck-typed getattr(self, request.method.lower()) dispatch, a def query(self): method is genuinely inert unless a view manually widens that allowlist, so matching it here would report a phantom route.

REGEX_INCLUDE_URLS = /\binclude\s*\(\s*(?:\(\s*)?r?['"]([^'"\\]*)['"]/

The optional ( admits the tuple form include(("app.urls", "app")).

REGEX_ROOT_URLCONF = /\s*ROOT_URLCONF\s*=\s*r?['"]([^'"\\]*)['"]/

Regular expressions for extracting Django URL configurations

REGISTRY_FILTER_RE = /\[\s*['"](\w+)['"]\s*\]\s*==\s*([A-Za-z_]\w*)\b/
REGISTRY_INCLUDE_RE = /\A\s*include\s*\(\s*([A-Za-z_][\w.]*)\s*\(([\s\S]*)\)\s*\)\s*\z/
REGISTRY_PATH_RE = /(?<![\w.])path\s*\(\s*([^,()]+?)\s*,/
REGISTRY_SLOT_RE = /\b([A-Za-z_]\w*)((?:\[\s*['"]\w+['"]\s*\])+)((?:\[\s*[A-Za-z_]\w*\s*\])+)/
REQUEST_ALIAS_RE = /^\s*([A-Za-z_]\w*)\s*=\s*(request\.(?:META|headers|GET|POST|COOKIES|FILES|data|query_params))\s*$/

headers = request.META then headers.get("HTTP_USER_AGENT"): reads through a local bound to a request mapping, spelled as reads of the mapping itself.

REQUEST_HELPER_CALL_RE = /(?<![\w.])([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)?)\s*\(\s*request\s*[,)]/

return webhook_form(request, channel): a function view that hands its request to a project function runs it to serve the request, so the verbs it tests and the data it reads are the view's. Each comes back as {verbs of the request.method branch calling it (nil outside one), whether the call follows such a branch (it serves the other verbs), its file, its body lines}, two calls deep.

REQUEST_METHOD_NAME_REGEXES = HTTP_METHODS.to_h do |m| {m, /['"](#{m})['"]/} end
REQUEST_PARAM_FIELD_MAP = {"GET" => {["GET"], "query"}, "POST" => {["POST"], "form"}, "COOKIES" => {nil, "cookie"}, "META" => {nil, "header"}, "headers" => {nil, "header"}, "FILES" => {["POST"], "form"}, "data" => {["POST", "PUT", "PATCH"], "form"}, "query_params" => {nil, "query"}, "QUERY_PARAMS" => {nil, "query"}, "DATA" => {["POST", "PUT", "PATCH"], "form"}, "json_body" => {["POST"], "json"}}

Map request parameters to their respective fields

REQUEST_PARAM_FIELD_PATTERNS = REQUEST_PARAM_FIELD_MAP.map do |field_name, tuple| {field_name, tuple[0], tuple[1], Regex.new("request\\.#{field_name}\\[[rf]?['\"]([^'\"]*)['\"]\\]"), Regex.new("request\\.#{field_name}\\.get(?:list)?\\([rf]?['\"]([^'\"]*)['\"]"), Regex.new("['\"]([^'\"]+)['\"]\\s+(?:not\\s+)?in\\s+request\\.#{field_name}\\b(?!\\s*[.\\[])")} end

Precompiled per-field access patterns so extract_params_from_line never rebuilds a PCRE2 regex per request field. Compiled once from REQUEST_PARAM_FIELD_MAP. {field_name, field_methods, param_type, bracket_re, get_re}

REQUEST_PARAM_TYPE_MAP = {"query" => nil, "form" => ["POST", "PUT", "PATCH"], "json" => ["POST", "PUT", "PATCH"], "cookie" => nil, "header" => nil}

Map request parameter types to HTTP methods

REQUEST_VERB_ALIAS_RE = /^\s*([A-Za-z_]\w*)\s*=\s*(request\.(?:GET|POST|data|query_params))\s+if\s+request\.method\s*==\s*['"](\w+)['"]\s+else\s+(request\.(?:GET|POST|data|query_params))\s*$/

data = request.POST if request.method == "POST" else request.GET: which mapping the local names depends on the verb served.

RETURN_NAME_RE = /\Areturn\s+([A-Za-z_][\w.]*)\s*\z/
ROUTE_CALL_RE = /\b(?:url|path|re_path|register|([A-Za-z_]\w*_path))\s*\(/

Argument lists of each top-level url|path|re_path|register(...) call in text. A call nested in another's arguments (an inline include([...]) body) is skipped here and read when that include is expanded. Scans bytes: every delimiter is ASCII, so UTF-8 is safe and each call costs one forward pass. url|path|re_path|register(, or a project wrapper named *_path( (Zulip's rest_path); group 1 is set for the wrapper.

SELF_REQUEST_CALL_RE = /(?=\b(self|super\(\s*\))\.([A-Za-z_]\w*)\s*\(([^()\n]*))/

What a class-based view method body reads besides its own request.X lines: forms it binds to the request, and the self methods it hands request to (self.get_return_url(request, obj)), two calls deep. Zero-width, so scan also sees a call nested in another's arguments (self.paginate_queryset(self.get_queryset())).

SERIALIZER_CLASS_RE = /^\s+serializer_class\s*=\s*([A-Za-z_][A-Za-z0-9_.]*)\s*$/m

Body fields a handler reads through the DRF serializer it hands request.data: the fields that serializer class (or a project base) declares. get_serializer means the view's serializer_class. ponytail: declared fields only; a ModelSerializer's Meta.fields model columns are not resolved.

SERIALIZER_DATA_RE = /\b([A-Za-z_][A-Za-z0-9_.]*)\s*\(\s*(?:[^()]*?,\s*)?data\s*=\s*[^,()]*\brequest\.data\b/

XSerializer(data=request.data) / self.get_serializer(data=...).

SERIALIZER_FIELD_RE = /^(\s+)([a-z_][a-z0-9_]*)\s*=\s*(?:[A-Za-z_][A-Za-z0-9_]*\.)*([A-Z][A-Za-z0-9_]*(?:Field|Serializer))\s*\(/

A declared serializer field: name = serializers.CharField(.

SETTINGS_LOADED_RE = /^\s*([A-Za-z_]\w*)\s*=\s*([A-Za-z_][\w.]*)\s*\(\s*settings\.([A-Z][A-Z0-9_]*)\s*\)\s*$/m

login_form = import_string(settings.USER_LOGIN_FORM): a local bound to the class a setting names by dotted path, through Django's import_string or a project function that ends in it.

STAR_IMPORT_RE = /^from\s+([\w.]+)\s+import\s+\*/m
TAGGER = "django_analyzer"
URL_GROUP_FLATTEN_RE = /\A(?:(?:functools\.)?reduce\s*\(\s*(?:operator\.)?(?:add|iadd|concat)\s*,\s*([A-Za-z_]\w*)\s*\)|sum\s*\(\s*([A-Za-z_]\w*)\s*,\s*\[\s*\]\s*\))\z/
URL_LIST_COPY_RE = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?::[^=\n]+)?(\+?=)\s*(?:list\s*\(\s*)?([A-Za-z_][A-Za-z0-9_]*)\s*\)?\s*$/
URL_LIST_JOIN = "\n# noir:url-list-join\n"

name = list(other), name: T = other, name += other. Joins the verbatim pieces of an extended helper list; a comment line, so route scanning reads through it, and split back so each piece keeps its source line.

URL_LIST_LITERAL_RE = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?::[^=\n]+)?(\+?=|\.\s*extend\s*\()\s*\[/

name = [, name: list[...] = [, name += [ and name.extend([.

URLCONF_AUTH_WRAPPER_RE = /\b(login_required|permission_required|staff_member_required|user_passes_test)\s*\(/

An auth decorator applied in the urlconf itself: path("saved/", login_required(SavedView.as_view())), permission_required("x")(view). The view file never shows it, so the auth tagger (which reads decorators at the handler's line) cannot see it; the analyzer tags the endpoint it builds from that pattern instead.

VERB_KEYWORD_RE = /\A\s*(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*=(?!=)\s*(.+)\z/m

rest_path("users/me", GET=get_me, PATCH=update_me): the keyword names the verb, its value the view serving it.

VIEW_CLASS_RE = /class\s+([^\s(:]+)\s*[\(:]/
VIEW_DEF_RE = /(?:async\s+)?def\s+([^\s(]+)\s*\(/
VIEW_LOADER_BINDING_RE = /^(\([\w,\s]+\)|[\w, \t]+?)\s*=\s*get_class(?:es)?\(\s*['"]([\w.]+)['"]\s*,\s*([\[(][^\])]*[\])]|['"]\w+['"])/m

A module-level X = get_class("search.views", "X") or A, X = get_classes("m", ["A", "X"]) (Oscar's dynamic loader).

WS_PATTERNS_ASSIGN = /^\s*websocket_urlpatterns\s*(?::[^=\n]*)?\+?=/m

Class methods

tech_name
Source

Instance methods

analyze
Source
extract_endpoints(django_urls : DjangoUrls) : Array(Endpoint)

Extract endpoints from a Django URL configuration file

Source
extract_endpoints_from_file(url : String, filepath : String, function_or_class_name : String, hops = 0)

Extract endpoints from a given file

Source
extract_params_from_line(line : String, endpoint_methods : Array(String), cleaned_data = true)

Extract parameters from a line of code

Source
filter_params(method : String, params : Array(Param))

Filter parameters based on HTTP method

Source
find_root_django_urls

Find all root Django URLs

Source
tech

Instance-side view of the same declaration. The per-file rescues live on this base class, which has no way to name the analyzer that is running inside them, so a skipped file could not be attributed to a tech. Deriving it from analyzer_for keeps the name written exactly once.

Source

Nested types