Skip to content

Commit

Permalink
feat: add support for python 3.9
Browse files Browse the repository at this point in the history
  • Loading branch information
taobojlen committed Jul 6, 2024
1 parent adbbfd8 commit 12339a3
Show file tree
Hide file tree
Showing 10 changed files with 78 additions and 47 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
django-version: ["4.2", "5.0"]
python-version: ["3.9", "3.10", "3.11", "3.12"]
django-version: ["4.2", "5.0", "5.1"]
name: Test (Python ${{ matrix.python-version }}, Django ${{ matrix.django-version }})

steps:
Expand Down
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1 +1 @@
python 3.12.4
python 3.9.19
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Added

- Add support for Python 3.9+

## 0.1.2 - 2024-07-06

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ install-hooks:

install:
$(MAKE) install-hooks
uv pip compile requirements.in -o requirements.txt && uv pip compile requirements-dev.in -o requirements-dev.txt && uv pip install -r requirements.txt && uv pip install -r requirements-dev.txt
uv pip compile requirements-dev.in -o requirements-dev.txt && uv pip sync requirements-dev.txt

ci:
pip install -r requirements.txt && pip install -r requirements-dev.txt
Expand Down
4 changes: 1 addition & 3 deletions requirements-dev.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# ensure that dev deps are constrained to production deps
-c requirements.txt

Django~=4.2
pytest~=8.2.2
pytest-django~=4.8.0
factory-boy~=3.3.0
Expand Down
22 changes: 16 additions & 6 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@
# uv pip compile requirements-dev.in -o requirements-dev.txt
asgiref==3.8.1
# via
# -c requirements.txt
# django
# django-stubs
backports-tarfile==1.2.0
# via jaraco-context
build==1.2.1
# via -r requirements-dev.in
certifi==2024.6.2
# via requests
charset-normalizer==3.3.2
# via requests
django==5.0.6
django==4.2.13
# via
# -c requirements.txt
# -r requirements-dev.in
# django-stubs
# django-stubs-ext
django-stubs==5.0.2
Expand All @@ -22,14 +23,19 @@ django-stubs-ext==5.0.2
# via django-stubs
docutils==0.21.2
# via readme-renderer
exceptiongroup==1.2.1
# via pytest
factory-boy==3.3.0
# via -r requirements-dev.in
faker==26.0.0
# via factory-boy
idna==3.7
# via requests
importlib-metadata==8.0.0
# via twine
# via
# build
# keyring
# twine
iniconfig==2.0.0
# via pytest
jaraco-classes==3.4.0
Expand Down Expand Up @@ -96,15 +102,19 @@ ruff==0.5.0
six==1.16.0
# via python-dateutil
sqlparse==0.5.0
# via django
tomli==2.0.1
# via
# -c requirements.txt
# django
# build
# django-stubs
# pytest
twine==5.1.1
# via -r requirements-dev.in
types-pyyaml==6.0.12.20240311
# via django-stubs
typing-extensions==4.12.2
# via
# asgiref
# django-stubs
# django-stubs-ext
urllib3==2.2.2
Expand Down
1 change: 0 additions & 1 deletion requirements.in

This file was deleted.

8 changes: 0 additions & 8 deletions requirements.txt

This file was deleted.

17 changes: 10 additions & 7 deletions src/zealot/listeners.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from contextlib import contextmanager
from contextvars import ContextVar, Token
from fnmatch import fnmatch
from typing import NotRequired, Type, TypedDict
from typing import Optional, Type, TypedDict

from django.conf import settings
from django.db import models
Expand All @@ -17,7 +17,7 @@
class QuerySource(TypedDict):
model: type[models.Model]
field: str
instance_key: str | None # e.g. `User:123`
instance_key: Optional[str] # e.g. `User:123`


_is_in_context = ContextVar("in_context", default=False)
Expand All @@ -27,7 +27,7 @@ class QuerySource(TypedDict):

class AllowListEntry(TypedDict):
model: str
field: NotRequired[str]
field: Optional[str]


class Listener(ABC):
Expand Down Expand Up @@ -59,7 +59,7 @@ def _alert(self, model: type[models.Model], field: str, message: str):
model_match = fnmatch(
f"{model._meta.app_label}.{model.__name__}", entry["model"]
)
field_match = fnmatch(field, entry.get("field", "*"))
field_match = fnmatch(field, entry.get("field") or "*")
if model_match and field_match:
is_allowlisted = True
break
Expand Down Expand Up @@ -87,7 +87,10 @@ def error_class(self):
return NPlusOneError

def notify(
self, model: Type[models.Model], field: str, instance_key: str | None
self,
model: Type[models.Model],
field: str,
instance_key: Optional[str],
):
if not _is_in_context.get():
return
Expand All @@ -102,7 +105,7 @@ def notify(
message = f"N+1 detected on {model.__name__}.{field}"
self._alert(model, field, message)

def ignore(self, instance_key: str | None):
def ignore(self, instance_key: Optional[str]):
"""
Tells the listener to ignore N+1s arising from this instance.
Expand Down Expand Up @@ -134,7 +137,7 @@ def setup() -> Token:
return _is_in_context.set(True)


def teardown(token: Token | None = None):
def teardown(token: Optional[Token] = None):
n_plus_one_listener.reset()
if token:
_is_in_context.reset(token)
Expand Down
59 changes: 41 additions & 18 deletions src/zealot/patch.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import functools
import importlib
import inspect
from typing import Any, Callable, NotRequired, Type, TypedDict, Unpack
from typing import Any, Callable, Optional, Type, TypedDict, Union

from django.db import models
from django.db.models.fields.related_descriptors import (
Expand All @@ -19,21 +19,23 @@


class QuerysetContext(TypedDict):
args: NotRequired[Any]
kwargs: NotRequired[Any]
args: Optional[Any]
kwargs: Optional[Any]

# This is only used for many-to-many relations. It contains the call args
# when `create_forward_many_to_many_manager` is called.
manager_call_args: NotRequired[dict[str, Any]]
manager_call_args: Optional[dict[str, Any]]

# used by ReverseManyToOne. a django model instance.
instance: NotRequired[models.Model]
instance: Optional[models.Model]


Parser = Callable[[QuerysetContext], QuerySource]


def get_instance_key(instance: models.Model | dict[str, Any]) -> str | None:
def get_instance_key(
instance: Union[models.Model, dict[str, Any]],
) -> Optional[str]:
if isinstance(instance, models.Model):
return f"{instance.__class__.__name__}:{instance.pk}"
else:
Expand Down Expand Up @@ -70,8 +72,16 @@ def wrapper(*args, **kwargs):
def patch_queryset_function(
queryset_func: Callable[..., models.QuerySet],
parser: Parser,
**context: Unpack[QuerysetContext],
context: Optional[QuerysetContext] = None,
):
if context is None:
context = {
"args": None,
"kwargs": None,
"manager_call_args": None,
"instance": None,
}

@functools.wraps(queryset_func)
def wrapper(*args, **kwargs):
queryset = queryset_func(*args, **kwargs)
Expand All @@ -88,7 +98,7 @@ def wrapper(*args, **kwargs):
queryset._clone = patch_queryset_function( # type: ignore
queryset._clone, # type: ignore
parser,
**context,
context=context,
)
queryset._fetch_all = patch_queryset_fetch_all(
queryset, parser, context
Expand All @@ -106,10 +116,10 @@ def patch_forward_many_to_one_descriptor():
"""

def parser(context: QuerysetContext) -> QuerySource:
assert "args" in context
assert "args" in context and context["args"] is not None
descriptor = context["args"][0]

if "kwargs" in context:
if "kwargs" in context and context["kwargs"] is not None:
instance = context["kwargs"]["instance"]
instance_key = get_instance_key(instance)
else:
Expand All @@ -129,7 +139,7 @@ def parser(context: QuerysetContext) -> QuerySource:

def parse_related_parts(
model: Type[models.Model],
related_name: str | None,
related_name: Optional[str],
related_model: Type[models.Model],
) -> tuple[type[models.Model], str]:
field_name = related_name or f"{related_model._meta.model_name}_set"
Expand All @@ -140,9 +150,10 @@ def patch_reverse_many_to_one_descriptor():
def parser(context: QuerysetContext) -> QuerySource:
assert (
"manager_call_args" in context
and context["manager_call_args"] is not None
and "rel" in context["manager_call_args"]
)
assert "instance" in context
assert "instance" in context and context["instance"] is not None
rel = context["manager_call_args"]["rel"]
model, field = parse_related_parts(
rel.model, rel.related_name, rel.related_model
Expand All @@ -165,8 +176,12 @@ def wrapper(self, instance):
self.get_queryset = patch_queryset_function(
self.get_queryset,
parser,
manager_call_args=manager_call_args,
instance=instance,
context={
"args": None,
"kwargs": None,
"manager_call_args": manager_call_args,
"instance": instance,
},
)
return func(self, instance)

Expand All @@ -183,10 +198,10 @@ def wrapper(self, instance):

def patch_reverse_one_to_one_descriptor():
def parser(context: QuerysetContext) -> QuerySource:
assert "args" in context
assert "args" in context and context["args"] is not None
descriptor = context["args"][0]
field = descriptor.related.field
if "kwargs" in context:
if "kwargs" in context and context["kwargs"] is not None:
instance = context["kwargs"]["instance"]
instance_key = get_instance_key(instance)
else:
Expand All @@ -206,9 +221,10 @@ def patch_many_to_many_descriptor():
def parser(context: QuerysetContext) -> QuerySource:
assert (
"manager_call_args" in context
and context["manager_call_args"] is not None
and "rel" in context["manager_call_args"]
)
assert "args" in context
assert "args" in context and context["args"] is not None
rel = context["manager_call_args"]["rel"]
manager = context["args"][0]
model = manager.instance.__class__
Expand All @@ -230,7 +246,14 @@ def patched_create_forward_many_to_many_manager(*args, **kwargs):
)
manager = create_forward_many_to_many_manager(*args, **kwargs)
manager.get_queryset = patch_queryset_function(
manager.get_queryset, parser, manager_call_args=manager_call_args
manager.get_queryset,
parser,
context={
"args": None,
"kwargs": None,
"manager_call_args": manager_call_args,
"instance": None,
},
)
return manager

Expand Down

0 comments on commit 12339a3

Please sign in to comment.