Skip to content

Commit ad4c390

Browse files
Fix tests
* Make metadata field tests more permissive - Add CldTestCase.assertObjectContainsSubset for recursive dict-subset assertions, allowing the server to return extra keys. - Use it in metadata field assertions so added restriction defaults (hidden_ui, excluded_from_search) no longer fail the test. - Drop the conflicting mandatory+readonly combination in the update test (server rejects it) while keeping restrictions coverage. - Reuse the helper across metadata reorder and uploader tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * CI: refresh Python and Django version matrix - Add Python 3.14 and Django 5.2 LTS / 6.0. - Drop EOL Python 3.9 and Django 3.2. - Pin django60 to Python 3.12+ (its minimum) in the matrix. - Update classifiers in setup.py and pyproject.toml to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Update python version in CI --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6d77606 commit ad4c390

8 files changed

Lines changed: 68 additions & 48 deletions

File tree

.github/workflows/test.yml

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ jobs:
99
strategy:
1010
matrix:
1111
include:
12-
- python-version: "3.9"
13-
toxenv: py39-core
1412
- python-version: "3.10"
1513
toxenv: py310-core
1614
- python-version: "3.11"
@@ -19,16 +17,24 @@ jobs:
1917
toxenv: py312-core
2018
- python-version: "3.13"
2119
toxenv: py313-core
22-
- python-version: "3.9"
23-
toxenv: py39-django32
20+
- python-version: "3.14"
21+
toxenv: py314-core
2422
- python-version: "3.10"
2523
toxenv: py310-django42
26-
- python-version: "3.11"
27-
toxenv: py311-django42
28-
- python-version: "3.12"
29-
toxenv: py312-django50
24+
- python-version: "3.10"
25+
toxenv: py310-django50
26+
- python-version: "3.10"
27+
toxenv: py310-django51
28+
- python-version: "3.10"
29+
toxenv: py310-django52
30+
- python-version: "3.13"
31+
toxenv: py313-django42
3032
- python-version: "3.13"
31-
toxenv: py313-django51
33+
toxenv: py313-django52
34+
- python-version: "3.12"
35+
toxenv: py312-django60
36+
- python-version: "3.14"
37+
toxenv: py314-django60
3238

3339
steps:
3440
- uses: actions/checkout@v4

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ For the complete documentation, see the [Python SDK Guide](https://cloudinary.co
4444
|-------------|------------|------------|
4545
| 1.x |||
4646

47-
| SDK Version | Django 1.11 | Django 2.x | Django 3.x | Django 4.x | Django 5.x |
48-
|-------------|-------------|------------|------------|------------|------------|
49-
| 1.x ||||||
47+
| SDK Version | Django 1.11 | Django 2.x | Django 3.x | Django 4.x | Django 5.x | Django 6.x |
48+
|-------------|-------------|------------|------------|------------|------------|------------|
49+
| 1.x |||||||
5050

5151

5252
## Installation

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,22 @@ classifiers = [
1313
"Framework :: Django",
1414
"Framework :: Django :: 1.11",
1515
"Framework :: Django :: 2.2",
16-
"Framework :: Django :: 3.2",
1716
"Framework :: Django :: 4.2",
1817
"Framework :: Django :: 5.0",
1918
"Framework :: Django :: 5.1",
19+
"Framework :: Django :: 5.2",
20+
"Framework :: Django :: 6.0",
2021
"Intended Audience :: Developers",
2122
"License :: OSI Approved :: MIT License",
2223
"Programming Language :: Python",
2324
"Programming Language :: Python :: 2",
2425
"Programming Language :: Python :: 2.7",
2526
"Programming Language :: Python :: 3",
26-
"Programming Language :: Python :: 3.9",
2727
"Programming Language :: Python :: 3.10",
2828
"Programming Language :: Python :: 3.11",
2929
"Programming Language :: Python :: 3.12",
3030
"Programming Language :: Python :: 3.13",
31+
"Programming Language :: Python :: 3.14",
3132
"Topic :: Internet :: WWW/HTTP",
3233
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
3334
"Topic :: Multimedia :: Graphics",

setup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,22 @@
3333
"Framework :: Django",
3434
"Framework :: Django :: 1.11",
3535
"Framework :: Django :: 2.2",
36-
"Framework :: Django :: 3.2",
3736
"Framework :: Django :: 4.2",
3837
"Framework :: Django :: 5.0",
3938
"Framework :: Django :: 5.1",
39+
"Framework :: Django :: 5.2",
40+
"Framework :: Django :: 6.0",
4041
"Intended Audience :: Developers",
4142
"License :: OSI Approved :: MIT License",
4243
"Programming Language :: Python",
4344
"Programming Language :: Python :: 2",
4445
"Programming Language :: Python :: 2.7",
4546
"Programming Language :: Python :: 3",
46-
"Programming Language :: Python :: 3.9",
4747
"Programming Language :: Python :: 3.10",
4848
"Programming Language :: Python :: 3.11",
4949
"Programming Language :: Python :: 3.12",
5050
"Programming Language :: Python :: 3.13",
51+
"Programming Language :: Python :: 3.14",
5152
"Topic :: Internet :: WWW/HTTP",
5253
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
5354
"Topic :: Multimedia :: Graphics",

test/helper_test.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,15 @@ def count_elements(lst):
351351
if count1 != count2:
352352
standard_msg = '%s != %s' % (count1, count2)
353353
self.fail(self._formatMessage(msg, standard_msg))
354+
355+
def assertObjectContainsSubset(self, actual, expected, msg=None):
356+
"""
357+
Fail unless every key/value pair in expected is present in actual.
358+
Nested dicts are compared recursively, so actual may contain extra keys.
359+
"""
360+
for key, value in expected.items():
361+
self.assertIn(key, actual, msg)
362+
if isinstance(value, dict):
363+
self.assertObjectContainsSubset(actual[key], value, msg)
364+
else:
365+
self.assertEqual(actual[key], value, msg)

test/test_metadata.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from cloudinary.exceptions import BadRequest, NotFound
1111
from test.helper_test import (
1212
UNIQUE_TEST_ID, get_uri, get_params, get_method, api_response_mock, ignore_exception, get_json_body,
13-
URLLIB3_REQUEST, patch
13+
URLLIB3_REQUEST, patch, CldTestCase
1414
)
1515

1616
MOCK_RESPONSE = api_response_mock()
@@ -105,7 +105,7 @@
105105
disable_warnings()
106106

107107

108-
class MetadataTest(unittest.TestCase):
108+
class MetadataTest(CldTestCase):
109109
@classmethod
110110
def setUpClass(cls):
111111
cloudinary.reset_config()
@@ -151,9 +151,7 @@ def assert_metadata_field(self, metadata_field, field_type=None, values=None):
151151
if metadata_field["type"] in ["enum", "set"]:
152152
self.assert_metadata_field_datasource(metadata_field["datasource"])
153153

154-
values = values or {}
155-
for key, value in values.items():
156-
self.assertEqual(metadata_field[key], value)
154+
self.assertObjectContainsSubset(metadata_field, values or {})
157155

158156
def assert_metadata_field_datasource(self, datasource):
159157
"""Asserts that a given object fits the generic structure of a metadata field datasource
@@ -308,17 +306,15 @@ def test08_update_metadata_field(self):
308306
"external_id": EXTERNAL_ID_SET,
309307
"label": new_label,
310308
"type": "integer",
311-
"mandatory": True,
312309
"default_value": new_default_value,
313-
"restrictions": {"readonly_ui": True}
310+
"restrictions": {"readonly_ui": True},
314311
})
315312

316313
self.assert_metadata_field(result, "string", {
317314
"external_id": EXTERNAL_ID_GENERAL,
318315
"label": new_label,
319316
"default_value": new_default_value,
320-
"mandatory": True,
321-
"restrictions": {"readonly_ui": True}
317+
"restrictions": {"readonly_ui": True},
322318
})
323319

324320
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
@@ -501,8 +497,7 @@ def test_reorder_metadata_fields_by_label(self, mocker):
501497

502498
self.assertTrue(get_uri(mocker).endswith("/metadata_fields/order"))
503499
self.assertEqual(get_method(mocker), "PUT")
504-
self.assertEqual(get_json_body(mocker)['order_by'], "label")
505-
self.assertEqual(get_json_body(mocker)['direction'], "asc")
500+
self.assertObjectContainsSubset(get_json_body(mocker), {"order_by": "label", "direction": "asc"})
506501

507502
@patch(URLLIB3_REQUEST)
508503
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
@@ -513,8 +508,7 @@ def test_reorder_metadata_fields_by_external_id(self, mocker):
513508

514509
self.assertTrue(get_uri(mocker).endswith("/metadata_fields/order"))
515510
self.assertEqual(get_method(mocker), "PUT")
516-
self.assertEqual(get_json_body(mocker)['order_by'], "external_id")
517-
self.assertEqual(get_json_body(mocker)['direction'], "desc")
511+
self.assertObjectContainsSubset(get_json_body(mocker), {"order_by": "external_id", "direction": "desc"})
518512

519513
@patch(URLLIB3_REQUEST)
520514
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
@@ -525,8 +519,7 @@ def test_reorder_metadata_fields_by_created_at(self, mocker):
525519

526520
self.assertTrue(get_uri(mocker).endswith("/metadata_fields/order"))
527521
self.assertEqual(get_method(mocker), "PUT")
528-
self.assertEqual(get_json_body(mocker)['order_by'], "created_at")
529-
self.assertEqual(get_json_body(mocker)['direction'], "asc")
522+
self.assertObjectContainsSubset(get_json_body(mocker), {"order_by": "created_at", "direction": "asc"})
530523

531524

532525
if __name__ == "__main__":

test/test_uploader.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,12 @@ def test_upload_unicode_filename(self):
196196

197197
result = uploader.upload(TEST_UNICODE_IMAGE, tags=[UNIQUE_TAG], use_filename=True, unique_filename=False)
198198

199-
self.assertEqual(result["width"], TEST_IMAGE_WIDTH)
200-
self.assertEqual(result["height"], TEST_IMAGE_HEIGHT)
201-
202-
self.assertEqual(expected_name, result["public_id"])
203-
self.assertEqual(expected_name, result["original_filename"])
199+
self.assertObjectContainsSubset(result, {
200+
"width": TEST_IMAGE_WIDTH,
201+
"height": TEST_IMAGE_HEIGHT,
202+
"public_id": expected_name,
203+
"original_filename": expected_name,
204+
})
204205

205206
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
206207
def test_upload_file_io_without_filename(self):
@@ -211,9 +212,11 @@ def test_upload_file_io_without_filename(self):
211212

212213
result = uploader.upload(temp_file, tags=[UNIQUE_TAG])
213214

214-
self.assertEqual(result["width"], TEST_IMAGE_WIDTH)
215-
self.assertEqual(result["height"], TEST_IMAGE_HEIGHT)
216-
self.assertEqual('stream', result["original_filename"])
215+
self.assertObjectContainsSubset(result, {
216+
"width": TEST_IMAGE_WIDTH,
217+
"height": TEST_IMAGE_HEIGHT,
218+
"original_filename": "stream",
219+
})
217220

218221
@unittest.skipUnless(cloudinary.config().api_secret, "requires api_key/api_secret")
219222
def test_upload_custom_filename(self):
@@ -794,11 +797,13 @@ def test_upload_large(self):
794797
use_filename=True, unique_filename=False, filename=filename)
795798

796799
self.assertCountEqual(resource2["tags"], ["upload_large_tag", UNIQUE_TAG])
797-
self.assertEqual(resource2["resource_type"], "image")
798-
self.assertEqual(resource2["original_filename"], filename)
800+
self.assertObjectContainsSubset(resource2, {
801+
"resource_type": "image",
802+
"original_filename": filename,
803+
"width": LARGE_FILE_WIDTH,
804+
"height": LARGE_FILE_HEIGHT,
805+
})
799806
self.assertEqual(resource2["original_filename"], resource2["public_id"])
800-
self.assertEqual(resource2["width"], LARGE_FILE_WIDTH)
801-
self.assertEqual(resource2["height"], LARGE_FILE_HEIGHT)
802807

803808
resource3 = uploader.upload_large(temp_file_name, chunk_size=LARGE_FILE_SIZE,
804809
tags=["upload_large_tag", UNIQUE_TAG])

tox.ini

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
[tox]
22
envlist =
3-
py{27,39,310,311,312,313}-core
3+
py{27,310,311,312,313,314}-core
44
py{27}-django{111}
5-
py{39,310,311,312,313}-django{32,42,50,51}
5+
py{310,311,312,313,314}-django{42,50,51,52}
6+
py{312,313,314}-django{60}
67

78
[testenv]
89
usedevelop = True
910
commands =
1011
core: python -m pytest test
11-
django{111,32}: django-admin.py test -v2 django_tests {env:D_ARGS:}
12-
django{42,50,51}: django-admin test -v2 django_tests {env:D_ARGS:}
12+
django{111}: django-admin.py test -v2 django_tests {env:D_ARGS:}
13+
django{42,50,51,52,60}: django-admin test -v2 django_tests {env:D_ARGS:}
1314
passenv = *
1415
deps =
1516
pytest
1617
py27: mock
1718
django111: Django>=1.11,<1.12
18-
django32: Django>=3.2,<3.3
1919
django42: Django>=4.2,<4.3
2020
django50: Django>=5.0,<5.1
2121
django51: Django>=5.1,<5.2
22+
django52: Django>=5.2,<5.3
23+
django60: Django>=6.0,<6.1
2224
setenv =
2325
DJANGO_SETTINGS_MODULE=django_tests.settings

0 commit comments

Comments
 (0)