Skip to content

Instantly share code, notes, and snippets.

@simonw
Last active December 7, 2024 02:06
Show Gist options
  • Save simonw/03776d9f80534aa8e5348580dc6a800b to your computer and use it in GitHub Desktop.
Save simonw/03776d9f80534aa8e5348580dc6a800b to your computer and use it in GitHub Desktop.

2024-09-25T16:04:52 conversation: 01j8myw0ga2q9y6em2d2zcg8yb

Model: o1-preview

Prompt:

files-to-prompt **/*.py -c | llm -m o1-preview "

The middleware test is failing showing all ogf these - why is MiddlewareAfter repeated so many times?

['MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware5', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware2', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware5', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware4', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware5', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware2', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware5', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware', 'MiddlewareBefore']"

Full context sent the model
<documents>
<document index="1">
<source>djp/__init__.py</source>
<document_content>
from .hookspecs import hookimpl
from . import hookspecs
import itertools
import os
import pathlib
from pluggy import PluginManager
import sys
from typing import List
import types

pm = PluginManager("djp")
pm.add_hookspecs(hookspecs)
pm.load_setuptools_entrypoints("djp")


def _module_from_path(path, name):
    # Adapted from http://sayspy.blogspot.com/2011/07/how-to-import-module-from-just-file.html
    mod = types.ModuleType(name)
    mod.__file__ = path
    with open(path, "r") as file:
        code = compile(file.read(), path, "exec", dont_inherit=True)
    exec(code, mod.__dict__)
    return mod


plugins_dir = os.environ.get("DJP_PLUGINS_DIR")
if plugins_dir:
    for filepath in pathlib.Path(plugins_dir).glob("*.py"):
        mod = _module_from_path(str(filepath), name=filepath.stem)
        try:
            pm.register(mod)
        except ValueError as ex:
            print(ex, file=sys.stderr)
            # Plugin already registered
            pass


class Before:
    def __init__(self, item: str):
        self.item = item


class After:
    def __init__(self, item: str):
        self.item = item


class Position:
    def __init__(self, item: str, before=None, after=None):
        assert not (before and after), "Cannot specify both before and after"
        self.item = item
        self.before = before
        self.after = after


def installed_apps() -> List[str]:
    return ["djp"] + list(itertools.chain(*pm.hook.installed_apps()))


def middleware(current_middleware: List[str]):
    before = []
    after = []
    default = []
    position_items = []

    for batch in pm.hook.middleware():
        for item in batch:
            if isinstance(item, Before):
                before.append(item.item)
            elif isinstance(item, After):
                after.append(item.item)
            elif isinstance(item, Position):
                position_items.append(item)
            elif isinstance(item, str):
                default.append(item)
            else:
                raise ValueError(f"Invalid item in middleware hook: {item}")

    combined = before + to_list(current_middleware) + default + after

    # Handle Position items
    for item in position_items:
        if item.before:
            try:
                idx = combined.index(item.before)
                combined.insert(idx, item.item)
            except ValueError:
                raise ValueError(f"Cannot find item to insert before: {item.before}")
        elif item.after:
            try:
                idx = combined.index(item.after)
                combined.insert(idx + 1, item.item)
            except ValueError:
                raise ValueError(f"Cannot find item to insert after: {item.after}")

    print("")
    print("combined", combined)
    print("")

    return combined


def urlpatterns():
    return list(itertools.chain(*pm.hook.urlpatterns()))


def settings(current_settings):
    # First wrap INSTALLED_APPS
    installed_apps_ = to_list(current_settings["INSTALLED_APPS"])
    installed_apps_ += installed_apps()
    current_settings["INSTALLED_APPS"] = installed_apps_

    # Now MIDDLEWARE
    current_settings["MIDDLEWARE"] = middleware(current_settings["MIDDLEWARE"])

    # Now apply any other settings() hooks
    pm.hook.settings(current_settings=current_settings)


def get_plugins():
    plugins = []
    plugin_to_distinfo = dict(pm.list_plugin_distinfo())
    for plugin in pm.get_plugins():
        plugin_info = {
            "name": plugin.__name__,
            "hooks": [h.name for h in pm.get_hookcallers(plugin)],
        }
        distinfo = plugin_to_distinfo.get(plugin)
        if distinfo:
            plugin_info["version"] = distinfo.version
            plugin_info["name"] = (
                getattr(distinfo, "name", None) or distinfo.project_name
            )
        plugins.append(plugin_info)
    return plugins


def to_list(tuple_or_list):
    if isinstance(tuple_or_list, tuple):
        return list(tuple_or_list)
    return tuple_or_list

</document_content>
</document>
<document index="2">
<source>djp/hookspecs.py</source>
<document_content>
from pluggy import HookimplMarker
from pluggy import HookspecMarker

hookspec = HookspecMarker("djp")
hookimpl = HookimplMarker("djp")


@hookspec
def installed_apps():
    """Return a list of Django app strings to be added to INSTALLED_APPS"""


@hookspec
def middleware():
    """
    Return a list of Django middleware class strings to be added to MIDDLEWARE.
    Optionally wrap with djp.Before() or djp.After() to specify ordering
    """


@hookspec
def urlpatterns():
    """Return a list of url patterns to be added to urlpatterns"""


@hookspec
def settings(current_settings):
    """Modify current_settings in place to finish configuring settings.py"""

</document_content>
</document>
<document index="3">
<source>djp/management/__init__.py</source>
<document_content>

</document_content>
</document>
<document index="4">
<source>djp/management/commands/__init__.py</source>
<document_content>

</document_content>
</document>
<document index="5">
<source>djp/management/commands/showplugins.py</source>
<document_content>
from django.core.management.base import BaseCommand
from djp import get_plugins
import json


class Command(BaseCommand):
    help = "Show installed plugins"

    def handle(self, *args, **options):
        plugins = get_plugins()
        print(json.dumps(plugins, indent=2))

</document_content>
</document>
<document index="6">
<source>docs/conf.py</source>
<document_content>
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import datetime
from subprocess import PIPE, Popen

# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))


# -- General configuration ------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
#
# needs_sphinx = '1.0'

# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ["myst_parser", "sphinx_copybutton"]
myst_enable_extensions = ["colon_fence"]

# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]

# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = ".rst"

# The master toctree document.
master_doc = "index"

# General information about the project.
project = "DJP"
author = "Simon Willison"
copyright = "{}, {}".format(datetime.date.today().year, author)


# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
pipe = Popen("git describe --tags --always", stdout=PIPE, shell=True)
git_version = pipe.stdout.read().decode("utf8")

if git_version:
    version = git_version.rsplit("-", 1)[0]
    release = git_version
else:
    version = ""
    release = ""

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = "en"

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"

# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False


# -- Options for HTML output ----------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
#
html_theme = "furo"

# Theme options are theme-specific and customize the look and feel of a theme
# further.  For a list of options available for each theme, see the
# documentation.

html_theme_options = {}
html_title = project

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = []


# -- Options for HTMLHelp output ------------------------------------------

# Output file base name for HTML help builder.
htmlhelp_basename = "project-doc"


# -- Options for LaTeX output ---------------------------------------------

latex_elements = {
    # The paper size ('letterpaper' or 'a4paper').
    #
    # 'papersize': 'letterpaper',
    # The font size ('10pt', '11pt' or '12pt').
    #
    # 'pointsize': '10pt',
    # Additional stuff for the LaTeX preamble.
    #
    # 'preamble': '',
    # Latex figure (float) alignment
    #
    # 'figure_align': 'htbp',
}

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
#  author, documentclass [howto, manual, or own class]).
latex_documents = [
    (
        master_doc,
        "project.tex",
        project,
        author,
        "manual",
    )
]


# -- Options for manual page output ---------------------------------------

# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
    (
        master_doc,
        "project",
        project,
        [author],
        1,
    )
]


# -- Options for Texinfo output -------------------------------------------

# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
#  dir menu entry, description, category)
texinfo_documents = [
    (
        master_doc,
        "project",
        project,
        author,
        "project",
        "Documentation for {}".format(project),
        "Miscellaneous",
    )
]

</document_content>
</document>
<document index="7">
<source>tests/plugins/installed_apps.py</source>
<document_content>
import djp


@djp.hookimpl
def installed_apps():
    return ["tests.test_project.app1"]

</document_content>
</document>
<document index="8">
<source>tests/plugins/middleware.py</source>
<document_content>
import djp


@djp.hookimpl(specname="middleware", tryfirst=True)
def middleware1():
    return [
        "tests.test_project.middleware.Middleware",
        "tests.test_project.middleware.Middleware2",
        "tests.test_project.middleware.Middleware3",
        djp.Before("tests.test_project.middleware.MiddlewareBefore"),
        djp.After("tests.test_project.middleware.MiddlewareAfter"),
    ]


@djp.hookimpl(specname="middleware")
def middleware2():
    return [
        djp.Position(
            "tests.test_project.middleware.Middleware4",
            before="tests.test_project.middleware.Middleware2",
        ),
        djp.Position(
            "tests.test_project.middleware.Middleware5",
            before="tests.test_project.middleware.Middleware3",
        ),
    ]

</document_content>
</document>
<document index="9">
<source>tests/plugins/settings.py</source>
<document_content>
import djp


@djp.hookimpl
def settings(current_settings):
    current_settings["FROM_PLUGIN"] = "x"

</document_content>
</document>
<document index="10">
<source>tests/plugins/urlpatterns.py</source>
<document_content>
from django.urls import path
from django.http import HttpResponse
import djp


@djp.hookimpl
def urlpatterns():
    return [path("from-plugin/", lambda request: HttpResponse("Hello from a plugin"))]

</document_content>
</document>
<document index="11">
<source>tests/test_django_plugins.py</source>
<document_content>
from django.conf import settings
from django.test.client import Client


def test_middleware_order():
    assert settings.MIDDLEWARE == [
        "tests.test_project.middleware.MiddlewareBefore",
        "tests.test_project.middleware.Middleware",
        "tests.test_project.middleware.Middleware4",
        "tests.test_project.middleware.Middleware2",
        "tests.test_project.middleware.Middleware5",
        "tests.test_project.middleware.Middleware3",
        "tests.test_project.middleware.MiddlewareAfter",
    ]


def test_middleware():
    print("Here we go")
    response = Client().get("/")
    print("Done")
    assert response["X-DJP-Middleware-After"] == "MiddlewareAfter"
    assert response["X-DJP-Middleware"] == "Middleware"
    assert response["X-DJP-Middleware-Before"] == "MiddlewareBefore"
    request = response._request
    assert hasattr(request, "_notes")
    assert request._notes == ["MiddlewareAfter", "Middleware", "MiddlewareBefore"]
    assert False


def test_urlpatterns():
    response = Client().get("/from-plugin/")
    assert response.content == b"Hello from a plugin"


def test_settings():
    assert settings.FROM_PLUGIN == "x"


def test_installed_apps():
    assert "tests.test_project.app1" in settings.INSTALLED_APPS

</document_content>
</document>
<document index="12">
<source>tests/test_project/__init__.py</source>
<document_content>

</document_content>
</document>
<document index="13">
<source>tests/test_project/app1/__init__.py</source>
<document_content>

</document_content>
</document>
<document index="14">
<source>tests/test_project/middleware.py</source>
<document_content>
def request_note(request, response, note):
    print("request_note", note)
    if not hasattr(request, "_notes"):
        request._notes = []
    request._notes.append(note)
    response._request = request


class Middleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        print("__call__ of middleware")
        response = self.get_response(request)
        response["X-DJP-Middleware"] = "Middleware"
        request_note(request, response, "Middleware")
        print("end __call__ of middleware")
        return response


class Middleware2:
    def __init__(self, get_response):
        print("__call__ of {}".format(self.__class__.__name__))
        self.get_response = get_response
        print("end __call__ of {}".format(self.__class__.__name__))

    def __call__(self, request):
        response = self.get_response(request)
        request_note(request, response, self.__class__.__name__)
        return self.get_response(request)


class Middleware3(Middleware2):
    pass


class Middleware4(Middleware2):
    pass


class Middleware5(Middleware2):
    pass


class MiddlewareBefore:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        print("__call__ of {}".format(self.__class__.__name__))
        response = self.get_response(request)
        response["X-DJP-Middleware-Before"] = "MiddlewareBefore"
        request_note(request, response, "MiddlewareBefore")
        print("end __call__ of {}".format(self.__class__.__name__))
        return response


class MiddlewareAfter:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        print("__call__ of {}".format(self.__class__.__name__))
        response = self.get_response(request)
        response["X-DJP-Middleware-After"] = "MiddlewareAfter"
        request_note(request, response, "MiddlewareAfter")
        print("end __call__ of {}".format(self.__class__.__name__))
        return response

</document_content>
</document>
<document index="15">
<source>tests/test_project/settings.py</source>
<document_content>
import djp

SECRET_KEY = "django-insecure-test-key"
DEBUG = True
ALLOWED_HOSTS = ["*"]

INSTALLED_APPS = (
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
)

MIDDLEWARE = tuple()

ROOT_URLCONF = "tests.test_project.urls"

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": ":memory:",
    }
}

TEMPLATES = [
    {
        "BACKEND": "django.template.backends.django.DjangoTemplates",
        "APP_DIRS": True,
    }
]

USE_TZ = True

djp.settings(globals())

</document_content>
</document>
<document index="16">
<source>tests/test_project/urls.py</source>
<document_content>
from django.http import HttpResponse
from django.urls import path
import djp

urlpatterns = [
    path("", lambda request: HttpResponse("Hello world"))
] + djp.urlpatterns()

</document_content>
</document>
</documents>
 
The middleware test is failing showing all ogf these - why is MiddlewareAfter repeated so many times?

 ['MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware5', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware2', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware5', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware4', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware5', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware2', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware5', 'MiddlewareAfter', 'Middleware3', 'MiddlewareAfter', 'Middleware', 'MiddlewareBefore']

Response:

The reason you're seeing MiddlewareAfter repeated multiple times is because in your Middleware2, Middleware3, Middleware4, and Middleware5 classes, the __call__ method is calling self.get_response(request) twice. This causes the remaining middleware in the stack (including MiddlewareAfter) to be invoked multiple times, leading to the repeated entries in your request._notes list.

Here's the problematic code in your Middleware2 (and similarly in Middleware3, Middleware4, Middleware5):

class Middleware2:
    def __init__(self, get_response):
        print("__call__ of {}".format(self.__class__.__name__))
        self.get_response = get_response
        print("end __call__ of {}".format(self.__class__.__name__))

    def __call__(self, request):
        response = self.get_response(request)
        request_note(request, response, self.__class__.__name__)
        return self.get_response(request)  # <-- Second call to get_response

In the __call__ method, you first call self.get_response(request) to get the response, you then add a note to the request, and finally, you make another call to self.get_response(request) when returning. This second call effectively re-invokes the rest of the middleware chain and the view, causing MiddlewareAfter (and any middleware after Middleware2) to be executed again. This process repeats with each middleware that incorrectly calls self.get_response multiple times.

To fix this, you should modify your middleware classes to only call self.get_response(request) once per request. Here's how you can correct Middleware2:

class Middleware2:
    def __init__(self, get_response):
        print("__call__ of {}".format(self.__class__.__name__))
        self.get_response = get_response
        print("end __call__ of {}".format(self.__class__.__name__))

    def __call__(self, request):
        response = self.get_response(request)
        request_note(request, response, self.__class__.__name__)
        return response  # Return the response obtained from the first call

Repeat this change for Middleware3, Middleware4, and Middleware5:

class Middleware3(Middleware2):
    pass

class Middleware4(Middleware2):
    pass

class Middleware5(Middleware2):
    pass

By ensuring that each middleware calls self.get_response(request) only once, you prevent the middleware chain from being traversed multiple times. This should result in MiddlewareAfter being executed only once, and your request._notes list should now contain the expected entries.

After making these changes, rerun your tests, and you should see the correct output:

['MiddlewareAfter', 'Middleware', 'MiddlewareBefore']

Explanation of the issue:

  • Multiple Calls to self.get_response: In your middleware, calling self.get_response(request) more than once causes the entire middleware chain (including the view) to be processed multiple times.

  • Impact on Middleware Chain: Each time you call self.get_response(request), it processes the request through all the remaining middleware. If called multiple times, it multiplies the execution of downstream middleware and the view, leading to unexpected behaviors and duplication.

  • Best Practice: Middleware should process the request, possibly modify the request or response, and then return the response from a single call to self.get_response.

Reference:

From the Django documentation on writing middleware:

"Each middleware component is responsible for doing some specific function. They accept the request, do something, and pass the request to the next middleware component (if needed). They can also modify the response before sending it back to the client."

Conclusion:

By adjusting your middleware to adhere to the correct pattern of calling self.get_response(request) only once, you align with Django's middleware framework expectations, and your test should pass as intended.

@simonw
Copy link
Author

simonw commented Sep 25, 2024

{"completion_tokens": 2538, "prompt_tokens": 4354, "total_tokens": 6892, "completion_tokens_details": {"reasoning_tokens": 1664}}

At $15/million for input and $60/m for output I think that works out at 30 cents: https://chatgpt.com/c/66f46deb-0474-8006-bffe-472e21d25132

$15.00 / million input tokens
$60.00 / million output* tokens

I used 2538 input and 4354 output - use Python calculate cost

# Define the rates for input and output tokens
input_token_rate = 15.00 / 1_000_000
output_token_rate = 60.00 / 1_000_000

# Number of tokens used
input_tokens_used = 2538
output_tokens_used = 4354

# Calculate the cost
input_cost = input_tokens_used * input_token_rate
output_cost = output_tokens_used * output_token_rate
total_cost = input_cost + output_cost

total_cost
0.29931

@Malaeu
Copy link

Malaeu commented Sep 26, 2024

Thx for this and many more contributions!! Are you on tier 5?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment