Skip to content

Commit 9469915

Browse files
committed
Merge branch 'main' into 228_filter_migration_iteration
2 parents d45e422 + 32289a5 commit 9469915

File tree

8 files changed

+50
-26
lines changed

8 files changed

+50
-26
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,13 @@ jobs:
4646
strategy:
4747
matrix:
4848
python-version:
49-
- "3.9"
5049
- "3.10"
50+
- "3.11"
5151
- "3.12"
5252
- "3.13"
53+
- "3.14"
5354
django-version:
54-
- "4.2" # LTS
55+
- "5.2" # LTS
5556
runs-on: ubuntu-latest
5657
steps:
5758
- uses: actions/checkout@v6
@@ -71,11 +72,11 @@ jobs:
7172
strategy:
7273
matrix:
7374
python-version:
74-
- "3.12"
75+
- "3.10"
7576
django-version:
7677
# LTS gets tested on all OS
78+
- "4.2"
7779
- "5.1"
78-
- "5.2"
7980
runs-on: ubuntu-latest
8081
steps:
8182
- uses: actions/checkout@v6

README.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ from pictures.models import PictureField
3333

3434
class Profile(models.Model):
3535
title = models.CharField(max_length=255)
36-
picture = PictureField(upload_to="avatars")
36+
picture = PictureField(
37+
upload_to="avatars", width_field="picture_width", height_field="picture_height"
38+
)
39+
picture_width = models.PositiveIntegerField(editable=False)
40+
picture_height = models.PositiveIntegerField(editable=False)
3741
```
3842

3943
```html
@@ -157,7 +161,11 @@ class Profile(models.Model):
157161
picture = PictureField(
158162
upload_to="avatars",
159163
aspect_ratios=[None, "1/1", "3/2", "16/9"],
164+
width_field="picture_width",
165+
height_field="picture_height",
160166
)
167+
picture_width = models.PositiveIntegerField(editable=False)
168+
picture_height = models.PositiveIntegerField(editable=False)
161169
```
162170

163171
```html
@@ -235,10 +243,15 @@ class Profile(models.Model):
235243
validators=[
236244
MinSizeValidator(400, 300), # At least 400x300 pixels
237245
MaxSizeValidator(4096, 4096), # At most 4096x4096 pixels
238-
]
246+
],
247+
width_field="picture_width",
248+
height_field="picture_height",
239249
)
250+
picture_width = models.PositiveIntegerField(editable=False)
251+
picture_height = models.PositiveIntegerField(editable=False)
252+
240253

241-
Use `None` to limit only one dimension: `MaxSizeValidator(2048, None)` limits only width.
254+
# Use `None` to limit only one dimension: `MaxSizeValidator(2048, None)` limits only width.
242255
```
243256

244257
> [!IMPORTANT]

pictures/models.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import math
77
from fractions import Fraction
88
from pathlib import Path
9+
from types import NotImplementedType
910

1011
from django.core import checks
1112
from django.core.files.base import ContentFile
@@ -105,7 +106,7 @@ def name(self) -> str:
105106
def path(self) -> Path:
106107
return Path(self.storage.path(self.name))
107108

108-
def process(self, image) -> Image:
109+
def process(self, image) -> Image.Image:
109110
image = ImageOps.exif_transpose(image) # crates a copy
110111
height = self.height or self.width / Fraction(*image.size)
111112
size = math.floor(self.width), math.floor(height)
@@ -130,7 +131,7 @@ def delete(self):
130131

131132

132133
class PictureFieldFile(ImageFieldFile):
133-
def __xor__(self, other) -> tuple[set[Picture], set[Picture]]:
134+
def __xor__(self, other) -> tuple[set[Picture], set[Picture]] | NotImplementedType:
134135
"""Return the new and obsolete :class:`Picture` instances."""
135136
if not isinstance(other, PictureFieldFile):
136137
return NotImplemented
@@ -192,7 +193,7 @@ def height(self):
192193
return self._get_image_dimensions()[1]
193194

194195
@property
195-
def aspect_ratios(self) -> {Fraction | None: {str: {int: Picture}}}:
196+
def aspect_ratios(self) -> dict[Fraction | None, dict[str, dict[int, Picture]]]:
196197
self._require_file()
197198
return self.get_picture_files(
198199
file_name=self.name,
@@ -210,7 +211,7 @@ def get_picture_files(
210211
img_height: int,
211212
storage: Storage,
212213
field: PictureField,
213-
) -> {Fraction | None: {str: {int: Picture}}}:
214+
) -> dict[Fraction | None, dict[str, dict[int, Picture]]]:
214215
PictureClass = import_string(conf.get_settings().PICTURE_CLASS)
215216
return {
216217
ratio: {
@@ -291,7 +292,7 @@ def _check_aspect_ratios(self):
291292
return errors
292293

293294
def _check_width_height_field(self):
294-
if None in self.aspect_ratios and not (self.width_field and self.height_field):
295+
if not (self.width_field and self.height_field):
295296
return [
296297
checks.Warning(
297298
"width_field and height_field attributes are missing",

pictures/utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def sizes(
6161

6262

6363
def source_set(
64-
size: (int, int), *, ratio: str | Fraction | None, max_width: int, cols: int
64+
size: tuple[int, int], *, ratio: str | Fraction | None, max_width: int, cols: int
6565
) -> set:
6666
ratio = Fraction(ratio) if ratio else None
6767
img_width, img_height = size
@@ -82,8 +82,8 @@ def placeholder(width: int, height: int, alt):
8282
hue = random.randint(0, 360) # NoQA S311
8383
img = Image.new("RGB", (width, height), color=f"hsl({hue}, 40%, 80%)")
8484
draw = ImageDraw.Draw(img)
85-
draw.line(((0, 0, width, height)), width=3, fill=f"hsl({hue}, 60%, 20%)")
86-
draw.line(((0, height, width, 0)), width=3, fill=f"hsl({hue}, 60%, 20%)")
85+
draw.line((0, 0, width, height), width=3, fill=f"hsl({hue}, 60%, 20%)")
86+
draw.line((0, height, width, 0), width=3, fill=f"hsl({hue}, 60%, 20%)")
8787
draw.rectangle(
8888
(width / 4, height / 4, width * 3 / 4, height * 3 / 4),
8989
fill=f"hsl({hue}, 40%, 80%)",

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,17 @@ classifiers = [
2323
"Topic :: Software Development",
2424
"Programming Language :: Python :: 3",
2525
"Programming Language :: Python :: 3 :: Only",
26-
"Programming Language :: Python :: 3.9",
2726
"Programming Language :: Python :: 3.10",
2827
"Programming Language :: Python :: 3.11",
2928
"Programming Language :: Python :: 3.12",
3029
"Programming Language :: Python :: 3.13",
30+
"Programming Language :: Python :: 3.14",
3131
"Framework :: Django",
3232
"Framework :: Django :: 4.2",
3333
"Framework :: Django :: 5.1",
3434
"Framework :: Django :: 5.2",
3535
]
36-
requires-python = ">=3.9"
36+
requires-python = ">=3.10"
3737
dependencies = ["django>=4.2.0", "pillow>=11.3.0"]
3838

3939
[project.optional-dependencies]
@@ -44,7 +44,7 @@ test = [
4444
"redis",
4545
]
4646
dramatiq = [
47-
"dramatiq<2",
47+
"dramatiq",
4848
"django-dramatiq",
4949
]
5050
celery = [

tests/test_models.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -825,9 +825,19 @@ def test_check_aspect_ratios(self):
825825
assert errors
826826
assert errors[0].id == "fields.E100"
827827

828-
def test_check_width_height_field(self):
829-
assert not PictureField(aspect_ratios=["3/2"])._check_width_height_field()
830-
with override_field_aspect_ratios(Profile.picture.field, [None]):
828+
@pytest.mark.parametrize(
829+
"aspect_ratios",
830+
[
831+
("3/2",),
832+
(None,),
833+
(
834+
"3/2",
835+
None,
836+
),
837+
],
838+
)
839+
def test_check_width_height_field(self, aspect_ratios):
840+
with override_field_aspect_ratios(Profile.picture.field, aspect_ratios):
831841
errors = Profile.picture.field._check_width_height_field()
832842
assert errors
833843
assert errors[0].id == "fields.E101"

tests/test_utils.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,14 +145,14 @@ def test_placeholder():
145145
assert img.height == 1200
146146

147147

148-
class TestPicture(Picture):
148+
class SamplePicture(Picture):
149149
@property
150150
def url(self):
151151
return f"/media/{self.parent_name}"
152152

153153

154154
def test_reconstruct(image_upload_file):
155-
picture = TestPicture(
155+
picture = SamplePicture(
156156
image_upload_file.name,
157157
"WEBP",
158158
"16/9",
@@ -164,7 +164,7 @@ def test_reconstruct(image_upload_file):
164164
assert isinstance(reconstructed, Storage)
165165

166166
assert utils.reconstruct(
167-
"tests.test_utils.TestPicture",
167+
"tests.test_utils.SamplePicture",
168168
[],
169169
{
170170
"parent_name": "test.jpg",
@@ -173,7 +173,7 @@ def test_reconstruct(image_upload_file):
173173
"storage": default_storage,
174174
"width": 100,
175175
},
176-
) == TestPicture(
176+
) == SamplePicture(
177177
"test.jpg",
178178
"JPEG",
179179
"16/9",

tests/testapp/settings.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,6 @@
151151
DRAMATIQ_BROKER = {
152152
"BROKER": os.getenv("DRAMATIQ_BROKER", "dramatiq.brokers.redis.RedisBroker"),
153153
"MIDDLEWARE": [
154-
"dramatiq.middleware.Prometheus",
155154
"dramatiq.middleware.AgeLimit",
156155
"dramatiq.middleware.TimeLimit",
157156
"dramatiq.middleware.Callbacks",

0 commit comments

Comments
 (0)