django-filthyfields 2.0.0__tar.gz → 2.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_filthyfields-2.0.2/MANIFEST.in +1 -0
- {django_filthyfields-2.0.0 → django_filthyfields-2.0.2}/PKG-INFO +2 -2
- {django_filthyfields-2.0.0 → django_filthyfields-2.0.2}/pyproject.toml +2 -2
- {django_filthyfields-2.0.0 → django_filthyfields-2.0.2}/src/django_filthyfields.egg-info/PKG-INFO +2 -2
- {django_filthyfields-2.0.0 → django_filthyfields-2.0.2}/src/django_filthyfields.egg-info/SOURCES.txt +2 -8
- {django_filthyfields-2.0.0 → django_filthyfields-2.0.2}/src/filthyfields/filthyfields.py +8 -4
- django_filthyfields-2.0.0/tests/test_async.py +0 -105
- django_filthyfields-2.0.0/tests/test_benchmark.py +0 -225
- django_filthyfields-2.0.0/tests/test_core.py +0 -656
- django_filthyfields-2.0.0/tests/test_m2m_fields.py +0 -105
- django_filthyfields-2.0.0/tests/test_non_regression.py +0 -195
- django_filthyfields-2.0.0/tests/test_save_fields.py +0 -220
- django_filthyfields-2.0.0/tests/test_timezone_aware_fields.py +0 -109
- {django_filthyfields-2.0.0 → django_filthyfields-2.0.2}/LICENSE +0 -0
- {django_filthyfields-2.0.0 → django_filthyfields-2.0.2}/README.md +0 -0
- {django_filthyfields-2.0.0 → django_filthyfields-2.0.2}/setup.cfg +0 -0
- {django_filthyfields-2.0.0 → django_filthyfields-2.0.2}/setup.py +0 -0
- {django_filthyfields-2.0.0 → django_filthyfields-2.0.2}/src/django_filthyfields.egg-info/dependency_links.txt +0 -0
- {django_filthyfields-2.0.0 → django_filthyfields-2.0.2}/src/django_filthyfields.egg-info/requires.txt +0 -0
- {django_filthyfields-2.0.0 → django_filthyfields-2.0.2}/src/django_filthyfields.egg-info/top_level.txt +0 -0
- {django_filthyfields-2.0.0 → django_filthyfields-2.0.2}/src/filthyfields/__init__.py +3 -3
- {django_filthyfields-2.0.0 → django_filthyfields-2.0.2}/src/filthyfields/_descriptor.c +0 -0
- {django_filthyfields-2.0.0 → django_filthyfields-2.0.2}/src/filthyfields/_descriptor.py +0 -0
- {django_filthyfields-2.0.0 → django_filthyfields-2.0.2}/src/filthyfields/compare.py +0 -0
- {django_filthyfields-2.0.0 → django_filthyfields-2.0.2}/src/filthyfields/py.typed +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
prune tests
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-filthyfields
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Tracking dirty fields on a Django model instance.
|
|
5
5
|
License-Expression: BSD-3-Clause
|
|
6
6
|
Project-URL: Homepage, https://github.com/oliverhaas/django-filthyfields
|
|
7
7
|
Project-URL: Documentation, https://oliverhaas.github.io/django-filthyfields/
|
|
8
8
|
Project-URL: Repository, https://github.com/oliverhaas/django-filthyfields.git
|
|
9
9
|
Keywords: django,filthyfields,dirtyfields,track,model,changes
|
|
10
|
-
Classifier: Development Status ::
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
11
|
Classifier: Framework :: Django
|
|
12
12
|
Classifier: Framework :: Django :: 6.0
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "django-filthyfields"
|
|
3
|
-
version = "2.0.
|
|
3
|
+
version = "2.0.2"
|
|
4
4
|
description = "Tracking dirty fields on a Django model instance."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "BSD-3-Clause"
|
|
@@ -14,7 +14,7 @@ keywords = [
|
|
|
14
14
|
"changes",
|
|
15
15
|
]
|
|
16
16
|
classifiers = [
|
|
17
|
-
"Development Status ::
|
|
17
|
+
"Development Status :: 5 - Production/Stable",
|
|
18
18
|
"Framework :: Django",
|
|
19
19
|
"Framework :: Django :: 6.0",
|
|
20
20
|
"Intended Audience :: Developers",
|
{django_filthyfields-2.0.0 → django_filthyfields-2.0.2}/src/django_filthyfields.egg-info/PKG-INFO
RENAMED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-filthyfields
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Tracking dirty fields on a Django model instance.
|
|
5
5
|
License-Expression: BSD-3-Clause
|
|
6
6
|
Project-URL: Homepage, https://github.com/oliverhaas/django-filthyfields
|
|
7
7
|
Project-URL: Documentation, https://oliverhaas.github.io/django-filthyfields/
|
|
8
8
|
Project-URL: Repository, https://github.com/oliverhaas/django-filthyfields.git
|
|
9
9
|
Keywords: django,filthyfields,dirtyfields,track,model,changes
|
|
10
|
-
Classifier: Development Status ::
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
11
|
Classifier: Framework :: Django
|
|
12
12
|
Classifier: Framework :: Django :: 6.0
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
{django_filthyfields-2.0.0 → django_filthyfields-2.0.2}/src/django_filthyfields.egg-info/SOURCES.txt
RENAMED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
2
3
|
README.md
|
|
3
4
|
pyproject.toml
|
|
4
5
|
setup.py
|
|
@@ -12,11 +13,4 @@ src/filthyfields/_descriptor.c
|
|
|
12
13
|
src/filthyfields/_descriptor.py
|
|
13
14
|
src/filthyfields/compare.py
|
|
14
15
|
src/filthyfields/filthyfields.py
|
|
15
|
-
src/filthyfields/py.typed
|
|
16
|
-
tests/test_async.py
|
|
17
|
-
tests/test_benchmark.py
|
|
18
|
-
tests/test_core.py
|
|
19
|
-
tests/test_m2m_fields.py
|
|
20
|
-
tests/test_non_regression.py
|
|
21
|
-
tests/test_save_fields.py
|
|
22
|
-
tests/test_timezone_aware_fields.py
|
|
16
|
+
src/filthyfields/py.typed
|
|
@@ -447,17 +447,21 @@ class DirtyFieldsMixin(models.Model, metaclass=_DirtyMeta):
|
|
|
447
447
|
self.save(update_fields=list(dirty_fields))
|
|
448
448
|
|
|
449
449
|
|
|
450
|
-
def capture_dirty_state(instances: Iterable[DirtyFieldsMixin]) -> None:
|
|
451
|
-
"""Snapshot dirty state
|
|
450
|
+
def capture_dirty_state(instances: DirtyFieldsMixin | Iterable[DirtyFieldsMixin]) -> None:
|
|
451
|
+
"""Snapshot dirty state — call before ``bulk_update()`` so ``was_dirty()`` works after. Accepts a single instance or an iterable."""
|
|
452
|
+
if isinstance(instances, DirtyFieldsMixin):
|
|
453
|
+
instances = (instances,)
|
|
452
454
|
for instance in instances:
|
|
453
455
|
instance._dirty_capture_was_dirty()
|
|
454
456
|
|
|
455
457
|
|
|
456
458
|
def reset_dirty_state(
|
|
457
|
-
instances: Iterable[DirtyFieldsMixin],
|
|
459
|
+
instances: DirtyFieldsMixin | Iterable[DirtyFieldsMixin],
|
|
458
460
|
fields: Iterable[str] | None = None,
|
|
459
461
|
) -> None:
|
|
460
|
-
"""Clear dirty state
|
|
462
|
+
"""Clear dirty state — call after ``bulk_update()``. ``fields`` accepts name or attname. Accepts a single instance or an iterable."""
|
|
463
|
+
if isinstance(instances, DirtyFieldsMixin):
|
|
464
|
+
instances = (instances,)
|
|
461
465
|
field_list = list(fields) if fields is not None else None
|
|
462
466
|
for instance in instances:
|
|
463
467
|
instance._dirty_reset_state(fields=field_list)
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from tests.models import ModelTest
|
|
4
|
-
|
|
5
|
-
pytestmark = [pytest.mark.django_db(transaction=True)]
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@pytest.mark.asyncio
|
|
9
|
-
async def test_asave_resets_dirty_state():
|
|
10
|
-
tm = await ModelTest.objects.acreate()
|
|
11
|
-
assert not tm.is_dirty()
|
|
12
|
-
|
|
13
|
-
tm.boolean = False
|
|
14
|
-
assert tm.is_dirty()
|
|
15
|
-
assert tm.get_dirty_fields() == {"boolean": True}
|
|
16
|
-
|
|
17
|
-
await tm.asave()
|
|
18
|
-
assert not tm.is_dirty()
|
|
19
|
-
assert tm.get_dirty_fields() == {}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@pytest.mark.asyncio
|
|
23
|
-
async def test_asave_captures_was_dirty():
|
|
24
|
-
tm = await ModelTest.objects.acreate(boolean=True, characters="original")
|
|
25
|
-
|
|
26
|
-
tm.characters = "modified"
|
|
27
|
-
assert tm.is_dirty()
|
|
28
|
-
|
|
29
|
-
await tm.asave()
|
|
30
|
-
|
|
31
|
-
assert tm.was_dirty()
|
|
32
|
-
assert tm.get_was_dirty_fields() == {"characters": "original"}
|
|
33
|
-
assert not tm.is_dirty()
|
|
34
|
-
|
|
35
|
-
# Save again with no changes
|
|
36
|
-
await tm.asave()
|
|
37
|
-
assert not tm.was_dirty()
|
|
38
|
-
assert tm.get_was_dirty_fields() == {}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
@pytest.mark.asyncio
|
|
42
|
-
async def test_asave_with_update_fields():
|
|
43
|
-
tm = await ModelTest.objects.acreate(boolean=True, characters="original")
|
|
44
|
-
|
|
45
|
-
tm.boolean = False
|
|
46
|
-
tm.characters = "modified"
|
|
47
|
-
assert tm.get_dirty_fields() == {"boolean": True, "characters": "original"}
|
|
48
|
-
|
|
49
|
-
await tm.asave(update_fields=["boolean"])
|
|
50
|
-
|
|
51
|
-
# `boolean` was persisted — clean. `characters` was not — still dirty.
|
|
52
|
-
assert tm.get_dirty_fields() == {"characters": "original"}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@pytest.mark.asyncio
|
|
56
|
-
async def test_arefresh_from_db_resets_dirty_state():
|
|
57
|
-
tm = await ModelTest.objects.acreate(boolean=True, characters="original")
|
|
58
|
-
alias = await ModelTest.objects.aget(pk=tm.pk)
|
|
59
|
-
alias.boolean = False
|
|
60
|
-
await alias.asave()
|
|
61
|
-
|
|
62
|
-
# tm still has stale local state
|
|
63
|
-
assert tm.boolean is True
|
|
64
|
-
|
|
65
|
-
await tm.arefresh_from_db()
|
|
66
|
-
assert tm.boolean is False
|
|
67
|
-
assert tm.get_dirty_fields() == {}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
@pytest.mark.asyncio
|
|
71
|
-
async def test_arefresh_from_db_partial_fields():
|
|
72
|
-
tm = await ModelTest.objects.acreate(characters="old value")
|
|
73
|
-
tm.boolean = False
|
|
74
|
-
tm.characters = "new value"
|
|
75
|
-
assert tm.get_dirty_fields() == {"boolean": True, "characters": "old value"}
|
|
76
|
-
|
|
77
|
-
await tm.arefresh_from_db(fields=["characters"])
|
|
78
|
-
assert tm.boolean is False
|
|
79
|
-
assert tm.characters == "old value"
|
|
80
|
-
assert tm.get_dirty_fields() == {"boolean": True}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
@pytest.mark.asyncio
|
|
84
|
-
async def test_aget_produces_clean_state():
|
|
85
|
-
created = await ModelTest.objects.acreate(boolean=True, characters="hello")
|
|
86
|
-
tm = await ModelTest.objects.aget(pk=created.pk)
|
|
87
|
-
|
|
88
|
-
assert not tm.is_dirty()
|
|
89
|
-
assert tm.get_dirty_fields() == {}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
@pytest.mark.asyncio
|
|
93
|
-
async def test_arefresh_from_db_with_from_queryset():
|
|
94
|
-
"""arefresh_from_db(from_queryset=...) resets dirty state, mirroring sync version."""
|
|
95
|
-
tm = await ModelTest.objects.acreate(boolean=True, characters="initial")
|
|
96
|
-
alias = await ModelTest.objects.aget(pk=tm.pk)
|
|
97
|
-
alias.characters = "updated"
|
|
98
|
-
await alias.asave()
|
|
99
|
-
|
|
100
|
-
tm.boolean = False
|
|
101
|
-
assert tm.get_dirty_fields() == {"boolean": True}
|
|
102
|
-
|
|
103
|
-
await tm.arefresh_from_db(from_queryset=ModelTest.objects.all())
|
|
104
|
-
assert tm.characters == "updated"
|
|
105
|
-
assert tm.get_dirty_fields() == {}
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
"""Benchmarks for dirty-field tracking overhead.
|
|
2
|
-
|
|
3
|
-
Skipped by default. To run:
|
|
4
|
-
|
|
5
|
-
uv run pytest tests/test_benchmark.py -m benchmark -s
|
|
6
|
-
|
|
7
|
-
The ``-s`` flag disables output capture so the result table is visible.
|
|
8
|
-
|
|
9
|
-
Two tests live here:
|
|
10
|
-
|
|
11
|
-
* ``test_benchmark_suite`` — in-process, compares plain Django vs this fork.
|
|
12
|
-
* ``test_benchmark_vs_upstream`` — 3-way comparison against upstream
|
|
13
|
-
``django-dirtyfields``. Spins up a throw-away venv via ``uv``, runs
|
|
14
|
-
:mod:`tests._upstream_benchmark_runner` there, and merges its numbers with
|
|
15
|
-
the in-process ones. First run is slow (venv setup); subsequent runs reuse
|
|
16
|
-
the cached venv under ``/tmp/dirtyfields-bench-upstream``.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
from __future__ import annotations
|
|
20
|
-
|
|
21
|
-
import json
|
|
22
|
-
import shutil
|
|
23
|
-
import subprocess
|
|
24
|
-
import sys
|
|
25
|
-
from pathlib import Path
|
|
26
|
-
|
|
27
|
-
import pytest
|
|
28
|
-
from django.db import models
|
|
29
|
-
|
|
30
|
-
from filthyfields import DirtyFieldsMixin
|
|
31
|
-
from tests._benchmark_common import (
|
|
32
|
-
ITERATIONS,
|
|
33
|
-
N_INSTANCES,
|
|
34
|
-
SCENARIOS,
|
|
35
|
-
populate,
|
|
36
|
-
run_paired,
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
pytestmark = [pytest.mark.benchmark, pytest.mark.django_db]
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class PlainBenchmarkModel(models.Model):
|
|
43
|
-
"""Plain Django model without dirty tracking (baseline)."""
|
|
44
|
-
|
|
45
|
-
char1 = models.CharField(max_length=100, default="")
|
|
46
|
-
char2 = models.CharField(max_length=100, default="")
|
|
47
|
-
char3 = models.CharField(max_length=100, default="")
|
|
48
|
-
char4 = models.CharField(max_length=100, default="")
|
|
49
|
-
text1 = models.TextField(default="")
|
|
50
|
-
text2 = models.TextField(default="")
|
|
51
|
-
int1 = models.IntegerField(default=0)
|
|
52
|
-
int2 = models.IntegerField(default=0)
|
|
53
|
-
int3 = models.IntegerField(default=0)
|
|
54
|
-
float1 = models.FloatField(default=0.0)
|
|
55
|
-
float2 = models.FloatField(default=0.0)
|
|
56
|
-
decimal1 = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
|
57
|
-
decimal2 = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
|
58
|
-
bool1 = models.BooleanField(default=False)
|
|
59
|
-
bool2 = models.BooleanField(default=False)
|
|
60
|
-
bool3 = models.BooleanField(default=False)
|
|
61
|
-
datetime1 = models.DateTimeField(null=True, default=None)
|
|
62
|
-
datetime2 = models.DateTimeField(null=True, default=None)
|
|
63
|
-
json1 = models.JSONField(default=dict)
|
|
64
|
-
json2 = models.JSONField(default=dict)
|
|
65
|
-
|
|
66
|
-
class Meta:
|
|
67
|
-
app_label = "tests"
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
class DirtyBenchmarkModel(DirtyFieldsMixin, models.Model):
|
|
71
|
-
"""Model using DirtyFieldsMixin."""
|
|
72
|
-
|
|
73
|
-
char1 = models.CharField(max_length=100, default="")
|
|
74
|
-
char2 = models.CharField(max_length=100, default="")
|
|
75
|
-
char3 = models.CharField(max_length=100, default="")
|
|
76
|
-
char4 = models.CharField(max_length=100, default="")
|
|
77
|
-
text1 = models.TextField(default="")
|
|
78
|
-
text2 = models.TextField(default="")
|
|
79
|
-
int1 = models.IntegerField(default=0)
|
|
80
|
-
int2 = models.IntegerField(default=0)
|
|
81
|
-
int3 = models.IntegerField(default=0)
|
|
82
|
-
float1 = models.FloatField(default=0.0)
|
|
83
|
-
float2 = models.FloatField(default=0.0)
|
|
84
|
-
decimal1 = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
|
85
|
-
decimal2 = models.DecimalField(max_digits=10, decimal_places=2, default=0)
|
|
86
|
-
bool1 = models.BooleanField(default=False)
|
|
87
|
-
bool2 = models.BooleanField(default=False)
|
|
88
|
-
bool3 = models.BooleanField(default=False)
|
|
89
|
-
datetime1 = models.DateTimeField(null=True, default=None)
|
|
90
|
-
datetime2 = models.DateTimeField(null=True, default=None)
|
|
91
|
-
json1 = models.JSONField(default=dict)
|
|
92
|
-
json2 = models.JSONField(default=dict)
|
|
93
|
-
|
|
94
|
-
class Meta:
|
|
95
|
-
app_label = "tests"
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
@pytest.fixture
|
|
99
|
-
def populated_benchmark_db():
|
|
100
|
-
"""Populate both benchmark models with N_INSTANCES rows of diverse data."""
|
|
101
|
-
populate(PlainBenchmarkModel)
|
|
102
|
-
populate(DirtyBenchmarkModel)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def _run_in_process() -> dict[str, tuple[float, float]]:
|
|
106
|
-
"""Run all scenarios in-process, returning ``{key: (plain_ms, fork_ms)}``."""
|
|
107
|
-
results: dict[str, tuple[float, float]] = {}
|
|
108
|
-
for key, _name, func in SCENARIOS:
|
|
109
|
-
results[key] = run_paired(
|
|
110
|
-
lambda f=func: f(PlainBenchmarkModel),
|
|
111
|
-
lambda f=func: f(DirtyBenchmarkModel),
|
|
112
|
-
)
|
|
113
|
-
return results
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def _py() -> str:
|
|
117
|
-
return f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def test_benchmark_suite(populated_benchmark_db, capsys):
|
|
121
|
-
"""Run all benchmark scenarios and print a summary table (plain vs fork)."""
|
|
122
|
-
results = _run_in_process()
|
|
123
|
-
|
|
124
|
-
with capsys.disabled():
|
|
125
|
-
print()
|
|
126
|
-
print("=" * 85)
|
|
127
|
-
print(f"Dirty Field Tracking Benchmark — Python {_py()}")
|
|
128
|
-
print("=" * 85)
|
|
129
|
-
print(f"Instances: {N_INSTANCES:,} | Fields: 20 | Iterations: {ITERATIONS}")
|
|
130
|
-
print()
|
|
131
|
-
print(f"{'Scenario':40} | {'Plain':>12} | {'Dirty':>12} | {'Overhead':>11}")
|
|
132
|
-
print("-" * 85)
|
|
133
|
-
for key, name, _func in SCENARIOS:
|
|
134
|
-
plain_ms, dirty_ms = results[key]
|
|
135
|
-
overhead = dirty_ms - plain_ms
|
|
136
|
-
print(f"{name:40} | {plain_ms:9.1f} ms | {dirty_ms:9.1f} ms | {overhead:+8.1f} ms")
|
|
137
|
-
print()
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
_UPSTREAM_VENV = Path("/tmp/dirtyfields-bench-upstream") # noqa: S108
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def _ensure_upstream_venv() -> Path:
|
|
144
|
-
"""Create (or reuse) a venv containing Django + upstream django-dirtyfields. Returns its python."""
|
|
145
|
-
python = _UPSTREAM_VENV / "bin" / "python"
|
|
146
|
-
marker = _UPSTREAM_VENV / ".installed"
|
|
147
|
-
if python.exists() and marker.exists():
|
|
148
|
-
return python
|
|
149
|
-
|
|
150
|
-
if shutil.which("uv") is None:
|
|
151
|
-
pytest.skip("`uv` not on PATH; can't set up the upstream comparison venv")
|
|
152
|
-
|
|
153
|
-
if _UPSTREAM_VENV.exists():
|
|
154
|
-
shutil.rmtree(_UPSTREAM_VENV)
|
|
155
|
-
|
|
156
|
-
try:
|
|
157
|
-
subprocess.run( # noqa: S603
|
|
158
|
-
["uv", "venv", "--quiet", "--python", _py(), str(_UPSTREAM_VENV)],
|
|
159
|
-
check=True,
|
|
160
|
-
)
|
|
161
|
-
subprocess.run( # noqa: S603
|
|
162
|
-
[
|
|
163
|
-
"uv",
|
|
164
|
-
"pip",
|
|
165
|
-
"install",
|
|
166
|
-
"--quiet",
|
|
167
|
-
"--python",
|
|
168
|
-
str(python),
|
|
169
|
-
"Django",
|
|
170
|
-
"django-dirtyfields",
|
|
171
|
-
],
|
|
172
|
-
check=True,
|
|
173
|
-
)
|
|
174
|
-
except subprocess.CalledProcessError as exc:
|
|
175
|
-
pytest.skip(f"Failed to set up upstream venv: {exc}")
|
|
176
|
-
|
|
177
|
-
marker.write_text("ok")
|
|
178
|
-
return python
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def _run_upstream(upstream_python: Path) -> dict[str, tuple[float, float]]:
|
|
182
|
-
"""Invoke the standalone upstream runner and parse its JSON output."""
|
|
183
|
-
project_root = Path(__file__).parent.parent
|
|
184
|
-
try:
|
|
185
|
-
result = subprocess.run( # noqa: S603
|
|
186
|
-
[str(upstream_python), "-m", "tests._upstream_benchmark_runner"],
|
|
187
|
-
capture_output=True,
|
|
188
|
-
text=True,
|
|
189
|
-
check=True,
|
|
190
|
-
cwd=str(project_root),
|
|
191
|
-
)
|
|
192
|
-
except subprocess.CalledProcessError as exc:
|
|
193
|
-
pytest.skip(f"Upstream benchmark runner failed:\n{exc.stderr}")
|
|
194
|
-
|
|
195
|
-
data = json.loads(result.stdout)
|
|
196
|
-
return {key: (row["plain_ms"], row["dirty_ms"]) for key, row in data["results"].items()}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def test_benchmark_vs_upstream(populated_benchmark_db, capsys):
|
|
200
|
-
"""3-way comparison: plain Django vs this fork vs upstream django-dirtyfields."""
|
|
201
|
-
upstream_python = _ensure_upstream_venv()
|
|
202
|
-
fork_results = _run_in_process()
|
|
203
|
-
upstream_results = _run_upstream(upstream_python)
|
|
204
|
-
|
|
205
|
-
with capsys.disabled():
|
|
206
|
-
print()
|
|
207
|
-
print("=" * 100)
|
|
208
|
-
print(f"3-way Benchmark — Python {_py()}")
|
|
209
|
-
print("=" * 100)
|
|
210
|
-
print(f"Instances: {N_INSTANCES:,} | Fields: 20 | Iterations: {ITERATIONS}")
|
|
211
|
-
print()
|
|
212
|
-
print(
|
|
213
|
-
f"{'Scenario':40} | {'Plain':>10} | {'Fork':>10} | {'Upstream':>10} | {'Fork Δ':>10} | {'Upstream Δ':>12}",
|
|
214
|
-
)
|
|
215
|
-
print("-" * 110)
|
|
216
|
-
for key, name, _func in SCENARIOS:
|
|
217
|
-
plain_ms, fork_ms = fork_results[key]
|
|
218
|
-
_up_plain_ms, upstream_ms = upstream_results[key]
|
|
219
|
-
fork_overhead = fork_ms - plain_ms
|
|
220
|
-
upstream_overhead = upstream_ms - plain_ms
|
|
221
|
-
print(
|
|
222
|
-
f"{name:40} | {plain_ms:7.1f} ms | {fork_ms:7.1f} ms | {upstream_ms:7.1f} ms "
|
|
223
|
-
f"| {fork_overhead:+7.1f} ms | {upstream_overhead:+9.1f} ms",
|
|
224
|
-
)
|
|
225
|
-
print()
|