diff --git a/.github/workflows/lockThreads.yml b/.github/workflows/lockThreads.yml index 2767a3c..ed5c04b 100644 --- a/.github/workflows/lockThreads.yml +++ b/.github/workflows/lockThreads.yml @@ -2,7 +2,7 @@ name: 'Lock Threads' on: schedule: - - cron: '0 0 * * *' + - cron: '25 4 * * *' workflow_dispatch: permissions: @@ -16,7 +16,7 @@ jobs: action: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v3 + - uses: dessant/lock-threads@v5 with: issue-inactive-days: '30' issue-comment: > diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5065a5b..2c58994 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,64 +7,21 @@ on: workflow_dispatch: jobs: - build: - runs-on: ${{ matrix.os || 'ubuntu-latest' }} - strategy: - matrix: - include: - - python: "3.6" - toxenv: py36-django32 - os: ubuntu-20.04 - - python: "3.7" - toxenv: py37-django32 - - python: "3.8" - toxenv: py38-django32 - - python: "3.9" - toxenv: py39-django32 - - python: "3.10" - toxenv: py310-django32 - - python: "pypy-3.10" - toxenv: pypy3-django32 - - - python: "3.8" - toxenv: py38-django41 - - python: "3.9" - toxenv: py39-django41 - - python: "3.10" - toxenv: py310-django41 - - python: "3.11" - toxenv: py311-django41 - - python: "pypy-3.10" - toxenv: pypy3-django41 - - - python: "3.8" - toxenv: py38-django42 - - python: "3.9" - toxenv: py39-django42 - - python: "3.10" - toxenv: py310-django42 - - python: "3.11" - toxenv: py311-django42 - - python: "pypy-3.10" - toxenv: pypy3-django42 - - - python: "3.10" - toxenv: py310-django50 - - python: "3.11" - toxenv: py311-django50 - - python: "3.12" - toxenv: py312-django50 - - python: "pypy-3.10" - toxenv: pypy3-django50 + test: + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Setup Python - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - name: Setup python + uses: actions/setup-python@v5 with: - python-version: ${{ matrix.python }} - - name: Install Test Framework - run: pip install tox-gh-actions - - name: Run Tests - env: - TOXENV: ${{ matrix.toxenv }} - run: tox # setting TOXENV is equivalent to calling `tox -e [ENV]` + python-version: | + 3.8 + 3.9 + 3.10 + pypy-3.10 + 3.11 + 3.12 + - name: Install test framework + run: pip install tox + - name: Run tests + run: tox r diff --git a/.gitignore b/.gitignore index 6792bfc..7bc5150 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ env/* .vscode/* README.html *.txt +.venv/* \ No newline at end of file diff --git a/.isort.cfg b/.isort.cfg deleted file mode 100644 index 7dd4dba..0000000 --- a/.isort.cfg +++ /dev/null @@ -1,14 +0,0 @@ -[settings] -sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -line_length=100 -default_section=THIRDPARTY -known_first_party= - django_cleanup -known_third_party= - easy_thumbnails - sorl - pytest -known_django=django -multi_line_output=4 -lines_after_imports=2 -combine_as_imports=true diff --git a/CHANGELOG.md b/CHANGELOG.md index 2591bcb..9959e6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [9.0.0] - 2024-09-18 +## Added +- pyproject.toml +- Documentation on how to use transaction test case when using pytest. PR [#108] from [@pavel-kalmykov](https://github.com/pavel-kalmykov). + +### Changed +- Update to remove specific version references, since there haven't been significant changes the approach on versioning will change. The version will no longer update when only tests or supported versions are updated. +- Updated lock thread version and update job to not run at contested times to avoid github rate limiting errors. +- Updated ci build action versions. +- Move isort and pytest settings to toml file. +- Simplify tox.ini and github actions CI job. +- Update a getattr call to remove unnecessary default of None so it will fail on an attribute error. +- Change from .format() to f-strings. + +### Removed +- Removed setup.py/setup.cfg + ## [8.1.0] - 2024-01-28 ### Added - Run tests for django 5.0 and python 3.12. @@ -93,7 +110,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.4] - 2012-08-16 ## [0.1.0] - 2012-08-14 -[Unreleased]: https://github.com/un1t/django-cleanup/compare/8.1.0...HEAD +[Unreleased]: https://github.com/un1t/django-cleanup/compare/9.0.0...HEAD +[9.0.0]: https://github.com/un1t/django-cleanup/compare/8.1.0...9.0.0 [8.1.0]: https://github.com/un1t/django-cleanup/compare/8.0.0...8.1.0 [8.0.0]: https://github.com/un1t/django-cleanup/compare/7.0.0...8.0.0 [7.0.0]: https://github.com/un1t/django-cleanup/compare/6.0.0...7.0.0 @@ -131,6 +149,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [0.1.4]: https://github.com/un1t/django-cleanup/compare/0.1.0...0.1.4 [0.1.0]: https://github.com/un1t/django-cleanup/releases/tag/0.1.0 +[#108]: https://github.com/un1t/django-cleanup/pull/108 [#100]: https://github.com/un1t/django-cleanup/pull/100 [#98]: https://github.com/un1t/django-cleanup/issues/98 [#96]: https://github.com/un1t/django-cleanup/issues/96 diff --git a/MANIFEST.in b/MANIFEST.in index bc9ae00..fc49293 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1 @@ -include README.rst -include LICENSE include CHANGELOG.md diff --git a/README.rst b/README.rst index 99f2e5c..6ac8d94 100644 --- a/README.rst +++ b/README.rst @@ -11,8 +11,7 @@ is set as the :code:`FileField`'s default value will not be deleted. Compatibility ------------- -- Django 3.2, 4.1, 4.2, 5.0 (`See Django Supported Versions `_) -- Python 3.6+ +- This app follows Django's `supported versions`_ and `Python version support`_. - Compatible with `sorl-thumbnail `_ - Compatible with `easy-thumbnail `_ @@ -164,10 +163,10 @@ Install, setup and use pyenv_ to install all the required versions of cPython (see the `tox.ini `_). Setup pyenv_ to have all versions of python activated within your local django-cleanup repository. -Ensuring that the python 3.12 that was installed is first priority. +Ensuring that the latest supported python version that was installed is first priority. -Install tox_ on python 3.12 and run the :code:`tox` command from your local django-cleanup -repository. +Install tox_ on the latest supported python version and run the :code:`tox` command from your local +django-cleanup repository. How to write tests ================== @@ -181,6 +180,12 @@ For details on why this is required see `here actually committed, thus your :code:`on_commit()` callbacks will never be run. If you need to test the results of an :code:`on_commit()` callback, use a :code:`TransactionTestCase` instead. +pytest +------ +When writing tests with pytest_ use `@pytest.mark.django_db(transaction=True)`_ with the +:code:`transaction` argument set to :code:`True` to ensure that the behavior will be the same as +using a transaction test case. + License ======= django-cleanup is free software under terms of the: @@ -209,8 +214,12 @@ SOFTWARE. .. _django.test.TransactionTestCase: https://docs.djangoproject.com/en/stable/topics/testing/tools/#django.test.TransactionTestCase +.. _pytest: https://docs.pytest.org .. _pyenv: https://github.com/pyenv/pyenv .. _tox: https://tox.readthedocs.io/en/latest/ +.. _supported versions: https://www.djangoproject.com/download/#supported-versions +.. _Python version support: https://docs.djangoproject.com/en/dev/faq/install/#what-python-version-can-i-use-with-django +.. _@pytest.mark.django_db(transaction=True): https://pytest-django.readthedocs.io/en/latest/helpers.html#pytest.mark.django_db .. |Version| image:: https://img.shields.io/pypi/v/django-cleanup.svg :target: https://pypi.python.org/pypi/django-cleanup/ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..57ab455 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,53 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name='django-cleanup' +authors = [ + {name = "Ilya Shalyapin", email = "ishalyapin@gmail.com"} +] +description = "Deletes old files." +readme = "README.rst" +keywords = ["django"] +classifiers = [ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Web Environment', + 'Framework :: Django', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + 'Topic :: Utilities' +] +requires-python = ">=3.8" +dynamic = ["version"] + +[project.urls] +Homepage = "https://github.com/un1t/django-cleanup" +Changelog = "https://github.com/un1t/django-cleanup/blob/master/CHANGELOG.md" + +[tool.setuptools.dynamic] +version = {attr = "django_cleanup.__version__"} + +[tool.isort] +sections = ["FUTURE", "STDLIB", "DJANGO", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] +line_length = 100 +default_section = "THIRDPARTY" +known_first_party = ["django_cleanup"] +known_third_party = ["easy_thumbnails", "sorl", "pytest"] +known_django = "django" +multi_line_output = 4 +lines_after_imports = 2 +combine_as_imports = true + +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "test.settings" +pythonpath = [".", "src"] +addopts = ["-v", "--cov-report=term-missing", "--cov=django_cleanup"] +markers = [ + "cleanup_selected_config: marks test as using the CleanupSelectedConfig app config", + "django_storage: change django storage backends" +] diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index a4ce5c5..0000000 --- a/pytest.ini +++ /dev/null @@ -1,6 +0,0 @@ -[pytest] -DJANGO_SETTINGS_MODULE = test.settings -pythonpath = . src -addopts = -n auto -v --cov-report=term-missing --cov=django_cleanup --forked -markers = - CleanupSelectedConfig: marks test as using the CleanupSelectedConfig app config diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 2a9acf1..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal = 1 diff --git a/setup.py b/setup.py deleted file mode 100644 index 691837a..0000000 --- a/setup.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# Copyright (c) 2012 Ilya Shalyapin -# -# django-cleanup is free software under terms of the MIT License. -# - -import os -import re -from codecs import open as codecs_open - -from setuptools import find_packages, setup - - -def read(*parts): - file_path = os.path.join(os.path.dirname(__file__), *parts) - return codecs_open(file_path, encoding='utf-8').read() - - -def find_version(*parts): - version_file = read(*parts) - version_match = re.search( - r'''^__version__ = ['"]([^'"]*)['"]''', version_file, re.M) - if version_match: - return str(version_match.group(1)) - raise RuntimeError('Unable to find version string.') - - -setup( - name='django-cleanup', - version=find_version('src/django_cleanup', '__init__.py'), - packages=['django_cleanup'], - package_dir={'': 'src'}, - include_package_data=True, - requires=['python (>=3.6)', 'django (>=3.2)'], - description='Deletes old files.', - long_description=read('README.rst'), - long_description_content_type='text/x-rst', - author='Ilya Shalyapin', - author_email='ishalyapin@gmail.com', - url='https://github.com/un1t/django-cleanup', - download_url='https://github.com/un1t/django-cleanup/tarball/master', - license='MIT License', - keywords='django', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Framework :: Django', - 'Framework :: Django :: 3.2', - 'Framework :: Django :: 4.1', - 'Framework :: Django :: 4.2', - 'Framework :: Django :: 5.0', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Topic :: Utilities', - ], -) diff --git a/src/django_cleanup/__init__.py b/src/django_cleanup/__init__.py index 567c9f3..2615c32 100644 --- a/src/django_cleanup/__init__.py +++ b/src/django_cleanup/__init__.py @@ -4,4 +4,4 @@ will delete files on model instance deletion. ''' -__version__ = '8.1.0' +__version__ = '9.0.0' diff --git a/src/django_cleanup/cache.py b/src/django_cleanup/cache.py index a2e3423..8800c0a 100644 --- a/src/django_cleanup/cache.py +++ b/src/django_cleanup/cache.py @@ -18,7 +18,6 @@ def fields_dict_default(): return {} FIELDS_FIELDS = defaultdict(fields_dict_default) FIELDS_STORAGE = defaultdict(fields_dict_default) -DOTTED_PATH = '{klass.__module__}.{klass.__qualname__}' # cache init ## @@ -82,7 +81,7 @@ def fields_for_model_instance(instance, using=None): deferred_fields = instance.get_deferred_fields() for field_name in get_fields_for_model(model_name, exclude=deferred_fields): - fieldfile = getattr(instance, field_name, None) + fieldfile = getattr(instance, field_name) yield field_name, fieldfile.__class__(using, fieldfile.field, fieldfile.name) @@ -104,22 +103,26 @@ def get_field_storage(model_name, field_name): def get_dotted_path(object_): '''get the dotted path for an object''' - return DOTTED_PATH.format(klass=object_.__class__) + klass = object_.__class__ + return f'{klass.__module__}.{klass.__qualname__}' def get_model_name(model): '''returns a unique model name''' - return '{opt.app_label}.{opt.model_name}'.format(opt=model._meta) + opt = model._meta + return f'{opt.app_label}.{opt.model_name}' def get_mangled_ignore(model): '''returns a mangled attribute name specific to the model for ignore functionality''' - return '_{opt.model_name}__{opt.app_label}_cleanup_ignore'.format(opt=model._meta) + opt = model._meta + return f'_{opt.model_name}__{opt.app_label}_cleanup_ignore' def get_mangled_select(model): '''returns a mangled attribute name specific to the model for select functionality''' - return '_{opt.model_name}__{opt.app_label}_cleanup_select'.format(opt=model._meta) + opt = model._meta + return f'_{opt.model_name}__{opt.app_label}_cleanup_select' # booleans ## @@ -132,7 +135,8 @@ def model_has_filefields(model_name): def ignore_model(model, select_mode): '''Check if a model should be ignored''' - return (not hasattr(model, get_mangled_select(model))) if select_mode else hasattr(model, get_mangled_ignore(model)) + return ((not hasattr(model, get_mangled_select(model))) + if select_mode else hasattr(model, get_mangled_ignore(model))) # instance functions ## diff --git a/src/django_cleanup/handlers.py b/src/django_cleanup/handlers.py index aa6a8c5..c8d9eed 100644 --- a/src/django_cleanup/handlers.py +++ b/src/django_cleanup/handlers.py @@ -123,12 +123,12 @@ def run_on_commit(): def connect(): '''Connect signals to the cleanup models''' for model in cache.cleanup_models(): - key = '{{}}_django_cleanup_{}'.format(cache.get_model_name(model)) + suffix = f'_django_cleanup_{cache.get_model_name(model)}' post_init.connect(cache_original_post_init, sender=model, - dispatch_uid=key.format('post_init')) + dispatch_uid=f'post_init{suffix}') pre_save.connect(fallback_pre_save, sender=model, - dispatch_uid=key.format('pre_save')) + dispatch_uid=f'pre_save{suffix}') post_save.connect(delete_old_post_save, sender=model, - dispatch_uid=key.format('post_save')) + dispatch_uid=f'post_save{suffix}') post_delete.connect(delete_all_post_delete, sender=model, - dispatch_uid=key.format('post_delete')) + dispatch_uid=f'post_delete{suffix}') diff --git a/test/conftest.py b/test/conftest.py index 9407485..88015e2 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,7 +1,8 @@ +import copy import os import shutil -from django.conf import settings +from django.conf import settings as django_settings from django.db.models.signals import post_delete, post_init, post_save, pre_save import pytest @@ -11,29 +12,38 @@ from .testing_helpers import get_random_pic_name -pytest_plugins = ("test.pytest_plugin",) +pytest_plugins = () + +def pytest_collection_modifyitems(items): + for item in items: + item.add_marker(pytest.mark.django_db(transaction=True)) @pytest.fixture(autouse=True) -def setup_django_cleanup_state(request): +def setup_django_cleanup_state(request, settings): for model in cache.cleanup_models(): - key = '{{}}_django_cleanup_{}'.format(cache.get_model_name(model)) + suffix = f'_django_cleanup_{cache.get_model_name(model)}' post_init.disconnect(None, sender=model, - dispatch_uid=key.format('post_init')) + dispatch_uid=f'post_init{suffix}') pre_save.disconnect(None, sender=model, - dispatch_uid=key.format('pre_save')) + dispatch_uid=f'pre_save{suffix}') post_save.disconnect(None, sender=model, - dispatch_uid=key.format('post_save')) + dispatch_uid=f'post_save{suffix}') post_delete.disconnect(None, sender=model, - dispatch_uid=key.format('post_delete')) + dispatch_uid=f'post_delete{suffix}') cache.FIELDS.clear() - selectedConfig = any(m.name == 'CleanupSelectedConfig' for m in request.node.iter_markers()) - - cache.prepare(selectedConfig) + cache.prepare(request.node.get_closest_marker('cleanup_selected_config') is not None) handlers.connect() + stroage_marker = request.node.get_closest_marker('django_storage') + if stroage_marker is not None: + storages = copy.deepcopy(settings.STORAGES) + for key, value in stroage_marker.kwargs.items(): + storages[key]['BACKEND'] = value + settings.STORAGES = storages + -@pytest.fixture(params=[settings.MEDIA_ROOT]) +@pytest.fixture(params=[django_settings.MEDIA_ROOT]) def picture(request): src = os.path.join(request.param, 'pic.jpg') dst = os.path.join(request.param, get_random_pic_name()) diff --git a/test/pytest_plugin/__init__.py b/test/pytest_plugin/__init__.py deleted file mode 100644 index cf7f78e..0000000 --- a/test/pytest_plugin/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -import sys - -import pytest - - -@pytest.hookimpl(tryfirst=True) -def pytest_load_initial_conftests(early_config, parser, args): - if hasattr(sys, 'gettrace') and sys.gettrace() is not None and '--forked' in args: - args.remove('--forked') diff --git a/test/requirements.txt b/test/requirements.txt index 7716de7..9f7dc51 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -3,8 +3,4 @@ easy-thumbnails pillow pytest pytest-django -pytest-pythonpath pytest-cov -pytest-xdist -pytest-forked -asgiref<3.7.0 diff --git a/test/settings.py b/test/settings.py index 9debf63..95e09fb 100644 --- a/test/settings.py +++ b/test/settings.py @@ -30,9 +30,9 @@ except (django.core.exceptions.AppRegistryNotReady, django.core.exceptions.ImproperlyConfigured): INSTALLED_APPS = INSTALLED_APPS + INSTALLED_APPS_INTEGRATION - MIDDLEWARE_CLASSES = [] - SECRET_KEY = '123' - MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + +## workaround: https://github.com/SmileyChris/easy-thumbnails/issues/641#issuecomment-2291098096 +THUMBNAIL_DEFAULT_STORAGE_ALIAS = 'default' diff --git a/test/test_all.py b/test/test_all.py index 1f1692a..e5d64d0 100644 --- a/test/test_all.py +++ b/test/test_all.py @@ -4,8 +4,9 @@ import re import tempfile -from django.conf import settings +from django.conf import settings as django_settings from django.core.files import File +from django.core.files.base import ContentFile from django.db import transaction from django.db.models.fields import NOT_PROVIDED, files @@ -33,15 +34,18 @@ -def getTraceback(): +def get_traceback(picture): fileabspath = os.path.abspath error = 'FileNotFoundError' - return TB.format( - handlers=fileabspath(handlers.__file__), - files=fileabspath(files.__file__), - storage=fileabspath(storage.__file__), - error=error) + return f'''Traceback (most recent call last): + File "{fileabspath(handlers.__file__)}", line xxx, in run_on_commit + file_.delete(save=False) + File "{fileabspath(files.__file__)}", line xxx, in delete + self.storage.delete(self.name) + File "{fileabspath(storage.__file__)}", line xxx, in delete + os.remove(name) +{error}: [Errno 2] No such file or directory: '{picture}\'''' def _raise(message): @@ -50,7 +54,6 @@ def _func(x): # pragma: no cover return _func -@pytest.mark.django_db(transaction=True) def test_refresh_from_db_without_refresh(picture): product = Product.objects.create(image=picture['filename']) assert os.path.exists(picture['path']) @@ -62,7 +65,6 @@ def test_refresh_from_db_without_refresh(picture): assert not os.path.exists(picture['path']) -@pytest.mark.django_db(transaction=True) def test_cache_gone(picture): product = Product.objects.create(image=picture['filename']) assert os.path.exists(picture['path']) @@ -73,7 +75,6 @@ def test_cache_gone(picture): assert not os.path.exists(picture['path']) -@pytest.mark.django_db(transaction=True) def test_storage_gone(picture): product = Product.objects.create(image=picture['filename']) assert os.path.exists(picture['path']) @@ -85,7 +86,6 @@ def test_storage_gone(picture): assert not os.path.exists(picture['path']) -@pytest.mark.django_db(transaction=True) def test_replace_file_with_file(picture): product = Product.objects.create(image=picture['filename']) assert os.path.exists(picture['path']) @@ -95,11 +95,10 @@ def test_replace_file_with_file(picture): product.save() assert not os.path.exists(picture['path']) assert product.image - new_image_path = os.path.join(settings.MEDIA_ROOT, random_pic_name) + new_image_path = os.path.join(django_settings.MEDIA_ROOT, random_pic_name) assert product.image.path == new_image_path -@pytest.mark.django_db(transaction=True) def test_replace_file_with_blank(picture): product = Product.objects.create(image=picture['filename']) assert os.path.exists(picture['path']) @@ -111,7 +110,6 @@ def test_replace_file_with_blank(picture): assert product.image.name == '' -@pytest.mark.django_db(transaction=True) def test_replace_file_with_none(picture): product = Product.objects.create(image=picture['filename']) assert os.path.exists(picture['path']) @@ -123,7 +121,6 @@ def test_replace_file_with_none(picture): assert product.image.name is None -@pytest.mark.django_db(transaction=True) def test_replace_file_proxy(picture): product = ProductProxy.objects.create(image=picture['filename']) assert os.path.exists(picture['path']) @@ -133,7 +130,6 @@ def test_replace_file_proxy(picture): assert not os.path.exists(picture['path']) -@pytest.mark.django_db(transaction=True) def test_replace_file_unmanaged(picture): product = ProductUnmanaged.objects.create(image=picture['filename']) assert os.path.exists(picture['path']) @@ -143,7 +139,6 @@ def test_replace_file_unmanaged(picture): assert not os.path.exists(picture['path']) -@pytest.mark.django_db(transaction=True) def test_replace_file_deferred(picture): '''probably shouldn't save from a deferred model but someone might do it''' product = Product.objects.create(image=picture['filename']) @@ -155,7 +150,6 @@ def test_replace_file_deferred(picture): assert not os.path.exists(picture['path']) -@pytest.mark.django_db(transaction=True) def test_remove_model_instance(picture): product = Product.objects.create(image=picture['filename']) assert os.path.exists(picture['path']) @@ -164,7 +158,6 @@ def test_remove_model_instance(picture): assert not os.path.exists(picture['path']) -@pytest.mark.django_db(transaction=True) def test_remove_model_instance_default(picture): product = Product.objects.create() assert product.image_default.path == picture['srcpath'] @@ -175,7 +168,6 @@ def test_remove_model_instance_default(picture): assert os.path.exists(picture['srcpath']) -@pytest.mark.django_db(transaction=True) def test_replace_file_with_file_default(picture): product = Product.objects.create() assert os.path.exists(picture['srcpath']) @@ -188,7 +180,6 @@ def test_replace_file_with_file_default(picture): assert os.path.exists(picture['srcpath']) -@pytest.mark.django_db(transaction=True) def test_remove_model_instance_ignore(picture): product = ProductIgnore.objects.create(image=picture['filename']) assert os.path.exists(picture['path']) @@ -197,7 +188,6 @@ def test_remove_model_instance_ignore(picture): assert os.path.exists(picture['path']) -@pytest.mark.django_db(transaction=True) def test_replace_file_with_file_ignore(picture): product = ProductIgnore.objects.create(image=picture['filename']) assert os.path.exists(picture['path']) @@ -207,11 +197,10 @@ def test_replace_file_with_file_ignore(picture): product.save() assert os.path.exists(picture['path']) assert product.image - new_image_path = os.path.join(settings.MEDIA_ROOT, random_pic_name) + new_image_path = os.path.join(django_settings.MEDIA_ROOT, random_pic_name) assert product.image.path == new_image_path -@pytest.mark.django_db(transaction=True) def test_remove_model_instance_proxy(picture): product = ProductProxy.objects.create(image=picture['filename']) assert os.path.exists(picture['path']) @@ -220,7 +209,6 @@ def test_remove_model_instance_proxy(picture): assert not os.path.exists(picture['path']) -@pytest.mark.django_db(transaction=True) def test_remove_model_instance_unmanaged(picture): product = ProductUnmanaged.objects.create(image=picture['filename']) assert os.path.exists(picture['path']) @@ -229,7 +217,6 @@ def test_remove_model_instance_unmanaged(picture): assert not os.path.exists(picture['path']) -@pytest.mark.django_db(transaction=True) def test_remove_model_instance_deferred(picture): product = Product.objects.create(image=picture['filename']) assert os.path.exists(picture['path']) @@ -239,7 +226,6 @@ def test_remove_model_instance_deferred(picture): assert not os.path.exists(picture['path']) -@pytest.mark.django_db(transaction=True) def test_remove_blank_file(monkeypatch): product = Product.objects.create(image='') monkeypatch.setattr( @@ -250,14 +236,12 @@ def test_remove_blank_file(monkeypatch): product.delete() -@pytest.mark.django_db(transaction=True) def test_remove_not_exists(): product = Product.objects.create(image='no-such-file') with transaction.atomic(get_using(product)): product.delete() -@pytest.mark.django_db(transaction=True) def test_remove_none(monkeypatch): product = Product.objects.create(image=None) monkeypatch.setattr( @@ -268,10 +252,10 @@ def test_remove_none(monkeypatch): product.delete() -@pytest.mark.django_db(transaction=True) -def test_exception_on_save(settings, picture, caplog): - settings.DEFAULT_FILE_STORAGE = 'test.storage.DeleteErrorStorage' - product = Product.objects.create(image=picture['filename']) +@pytest.mark.django_storage(default='test.storage.DeleteErrorStorage') +def test_exception_on_save(picture, caplog): + filename = picture['filename'] + product = Product.objects.create(image=filename) # simulate a fieldfile that has a storage that raises a filenotfounderror on delete assert os.path.exists(picture['path']) product.image.delete(save=False) @@ -282,18 +266,16 @@ def test_exception_on_save(settings, picture, caplog): assert not os.path.exists(picture['path']) for record in caplog.records: - assert LINE.sub('line xxx', record.exc_text) == getTraceback().format(picture=picture['path']) + assert LINE.sub('line xxx', record.exc_text) == get_traceback(picture['path']) assert caplog.record_tuples == [ ( 'django_cleanup.handlers', logging.ERROR, - 'There was an exception deleting the file `{}` on field `test.product.image`'.format( - picture['filename']) + f'There was an exception deleting the file `{filename}` on field `test.product.image`' ) ] -@pytest.mark.django_db(transaction=True) def test_cascade_delete(picture): root = RootProduct.objects.create() branch = BranchProduct.objects.create(root=root, image=picture['filename']) @@ -304,7 +286,6 @@ def test_cascade_delete(picture): assert not os.path.exists(picture['path']) -@pytest.mark.django_db(transaction=True) def test_file_exists_on_create_and_update(): # If a filepath is specified which already exists, # the FileField generates a random suffix to choose a different location. @@ -314,7 +295,7 @@ def test_file_exists_on_create_and_update(): # directly within the same directory as the image would be uploaded to. upload_to = Product._meta.get_field("image").upload_to - dst_directory = os.path.join(settings.MEDIA_ROOT, upload_to) + dst_directory = os.path.join(django_settings.MEDIA_ROOT, upload_to) if not os.path.isdir(dst_directory): os.makedirs(dst_directory) @@ -322,7 +303,8 @@ def test_file_exists_on_create_and_update(): # a file aleady exists so the new file is renamed then saved with tempfile.NamedTemporaryFile(prefix="f1__", dir=dst_directory) as f1: with transaction.atomic(): - product = Product.objects.create(image=File(f1, name=os.path.join(upload_to, os.path.basename(f1.name)))) + product = Product.objects.create( + image=File(f1, name=os.path.join(upload_to, os.path.basename(f1.name)))) assert f1.name != product.image.path assert os.path.exists(f1.name) @@ -353,10 +335,9 @@ def test_file_exists_on_create_and_update(): assert not os.path.isfile(product.image.path) -@pytest.mark.django_db(transaction=True) def test_signals(picture): - prekwargs = None - postkwargs = None + prekwargs = {} + postkwargs = {} def assn_prekwargs(**kwargs): nonlocal prekwargs prekwargs = kwargs @@ -364,56 +345,59 @@ def assn_prekwargs(**kwargs): def assn_postkwargs(**kwargs): nonlocal postkwargs postkwargs = kwargs - - cleanup_pre_delete.connect(assn_prekwargs, dispatch_uid='pre_test_replace_file_with_file_signals') - cleanup_post_delete.connect(assn_postkwargs, dispatch_uid='post_test_replace_file_with_file_signals') + + cleanup_pre_delete.connect( + assn_prekwargs, dispatch_uid='pre_test_replace_file_with_file_signals') + cleanup_post_delete.connect( + assn_postkwargs, dispatch_uid='post_test_replace_file_with_file_signals') product = Product.objects.create(image=picture['filename']) random_pic_name = get_random_pic_name() product.image = random_pic_name with transaction.atomic(get_using(product)): product.save() - assert prekwargs['deleted'] == False - assert prekwargs['updated'] == True + assert prekwargs['deleted'] is False + assert prekwargs['updated'] is True assert prekwargs['instance'] == product assert prekwargs['file'] is not None assert prekwargs['file_name'] == picture['filename'] - assert isinstance(prekwargs['default_file_name'], NOT_PROVIDED) + assert isinstance(prekwargs['default_file_name'], NOT_PROVIDED) assert prekwargs['model_name'] == 'test.product' assert prekwargs['field_name'] == 'image' - - assert postkwargs['deleted'] == False - assert postkwargs['updated'] == True + + assert postkwargs['deleted'] is False + assert postkwargs['updated'] is True assert postkwargs['instance'] == product assert postkwargs['file'] is not None assert postkwargs['file_name'] == picture['filename'] - assert isinstance(postkwargs['default_file_name'], NOT_PROVIDED) + assert isinstance(postkwargs['default_file_name'], NOT_PROVIDED) assert postkwargs['model_name'] == 'test.product' assert postkwargs['field_name'] == 'image' - assert postkwargs['success'] == True + assert postkwargs['success'] is True assert postkwargs['error'] is None with transaction.atomic(get_using(product)): product.delete() - assert prekwargs['deleted'] == True - assert prekwargs['updated'] == False + assert prekwargs['deleted'] is True + assert prekwargs['updated'] is False assert prekwargs['instance'] == product assert prekwargs['file'] is not None assert prekwargs['file_name'] == random_pic_name - assert isinstance(prekwargs['default_file_name'], NOT_PROVIDED) + assert isinstance(prekwargs['default_file_name'], NOT_PROVIDED) assert prekwargs['model_name'] == 'test.product' assert prekwargs['field_name'] == 'image' - - assert postkwargs['deleted'] == True - assert postkwargs['updated'] == False + + assert postkwargs['deleted'] is True + assert postkwargs['updated'] is False assert postkwargs['instance'] == product assert postkwargs['file'] is not None assert postkwargs['file_name'] == random_pic_name - assert isinstance(postkwargs['default_file_name'], NOT_PROVIDED) + assert isinstance(postkwargs['default_file_name'], NOT_PROVIDED) assert postkwargs['model_name'] == 'test.product' assert postkwargs['field_name'] == 'image' - assert postkwargs['success'] == True + print(postkwargs['error']) + assert postkwargs['success'] is True assert postkwargs['error'] is None cleanup_pre_delete.disconnect(None, dispatch_uid='pre_test_replace_file_with_file_signals') @@ -421,8 +405,7 @@ def assn_postkwargs(**kwargs): #region select config -@pytest.mark.CleanupSelectedConfig -@pytest.mark.django_db(transaction=True) +@pytest.mark.cleanup_selected_config def test__select_config__replace_file_with_file(picture): product = Product.objects.create(image=picture['filename']) assert os.path.exists(picture['path']) @@ -432,12 +415,11 @@ def test__select_config__replace_file_with_file(picture): product.save() assert not os.path.exists(picture['path']) assert product.image - new_image_path = os.path.join(settings.MEDIA_ROOT, random_pic_name) + new_image_path = os.path.join(django_settings.MEDIA_ROOT, random_pic_name) assert product.image.path == new_image_path -@pytest.mark.CleanupSelectedConfig -@pytest.mark.django_db(transaction=True) +@pytest.mark.cleanup_selected_config def test__select_config__replace_file_with_file_ignore(picture): product = ProductIgnore.objects.create(image=picture['filename']) assert os.path.exists(picture['path']) @@ -447,6 +429,6 @@ def test__select_config__replace_file_with_file_ignore(picture): product.save() assert os.path.exists(picture['path']) assert product.image - new_image_path = os.path.join(settings.MEDIA_ROOT, random_pic_name) + new_image_path = os.path.join(django_settings.MEDIA_ROOT, random_pic_name) assert product.image.path == new_image_path #endregion diff --git a/test/test_integration.py b/test/test_integration.py index f693a4f..4c2762c 100644 --- a/test/test_integration.py +++ b/test/test_integration.py @@ -1,6 +1,6 @@ import os -from django.conf import settings +from django.conf import settings as django_settings from django.db import transaction import pytest @@ -10,19 +10,18 @@ from .testing_helpers import get_using -@pytest.mark.django_db(transaction=True) -def test_sorlthumbnail_replace(settings, picture): +def test_sorlthumbnail_replace(picture): # https://github.com/mariocesar/sorl-thumbnail - models = pytest.importorskip("test.models.integration") - ProductIntegration = models.ProductIntegration + get_thumbnail = pytest.importorskip('sorl.thumbnail').get_thumbnail + models = pytest.importorskip('test.models.integration') + product_integration = models.ProductIntegration sorl_delete = models.sorl_delete cleanup_pre_delete.connect(sorl_delete) - from sorl.thumbnail import get_thumbnail - product = ProductIntegration.objects.create(sorl_image=picture['filename']) + product = product_integration.objects.create(sorl_image=picture['filename']) assert os.path.exists(picture['path']) im = get_thumbnail( product.sorl_image, '100x100', crop='center', quality=50) - thumbnail_path = os.path.join(settings.MEDIA_ROOT, im.name) + thumbnail_path = os.path.join(django_settings.MEDIA_ROOT, im.name) assert os.path.exists(thumbnail_path) product.sorl_image = 'new.png' with transaction.atomic(get_using(product)): @@ -32,19 +31,18 @@ def test_sorlthumbnail_replace(settings, picture): cleanup_pre_delete.disconnect(sorl_delete) -@pytest.mark.django_db(transaction=True) def test_sorlthumbnail_delete(picture): # https://github.com/mariocesar/sorl-thumbnail - models = pytest.importorskip("test.models.integration") - ProductIntegration = models.ProductIntegration + get_thumbnail = pytest.importorskip('sorl.thumbnail').get_thumbnail + models = pytest.importorskip( 'test.models.integration') + product_integration = models.ProductIntegration sorl_delete = models.sorl_delete cleanup_pre_delete.connect(sorl_delete) - from sorl.thumbnail import get_thumbnail - product = ProductIntegration.objects.create(sorl_image=picture['filename']) + product = product_integration.objects.create(sorl_image=picture['filename']) assert os.path.exists(picture['path']) im = get_thumbnail( product.sorl_image, '100x100', crop='center', quality=50) - thumbnail_path = os.path.join(settings.MEDIA_ROOT, im.name) + thumbnail_path = os.path.join(django_settings.MEDIA_ROOT, im.name) assert os.path.exists(thumbnail_path) with transaction.atomic(get_using(product)): product.delete() @@ -53,17 +51,16 @@ def test_sorlthumbnail_delete(picture): cleanup_pre_delete.disconnect(sorl_delete) -@pytest.mark.django_db(transaction=True) def test_easythumbnails_replace(picture): # https://github.com/SmileyChris/easy-thumbnails - models = pytest.importorskip("test.models.integration") - ProductIntegration = models.ProductIntegration - from easy_thumbnails.files import get_thumbnailer - product = ProductIntegration.objects.create(easy_image=picture['filename']) + get_thumbnailer = pytest.importorskip('easy_thumbnails.files').get_thumbnailer + models = pytest.importorskip( 'test.models.integration') + product_integration = models.ProductIntegration + product = product_integration.objects.create(easy_image=picture['filename']) assert os.path.exists(picture['path']) im = get_thumbnailer(product.easy_image).get_thumbnail( {'size': (100, 100)}) - thumbnail_path = os.path.join(settings.MEDIA_ROOT, im.name) + thumbnail_path = os.path.join(django_settings.MEDIA_ROOT, im.name) assert os.path.exists(thumbnail_path) product.easy_image = 'new.png' with transaction.atomic(get_using(product)): @@ -72,17 +69,16 @@ def test_easythumbnails_replace(picture): assert not os.path.exists(thumbnail_path) -@pytest.mark.django_db(transaction=True) def test_easythumbnails_delete(picture): # https://github.com/SmileyChris/easy-thumbnails - models = pytest.importorskip("test.models.integration") - ProductIntegration = models.ProductIntegration - from easy_thumbnails.files import get_thumbnailer - product = ProductIntegration.objects.create(easy_image=picture['filename']) + get_thumbnailer = pytest.importorskip('easy_thumbnails.files').get_thumbnailer + models = pytest.importorskip( 'test.models.integration') + product_integration = models.ProductIntegration + product = product_integration.objects.create(easy_image=picture['filename']) assert os.path.exists(picture['path']) im = get_thumbnailer(product.easy_image).get_thumbnail( {'size': (100, 100)}) - thumbnail_path = os.path.join(settings.MEDIA_ROOT, im.name) + thumbnail_path = os.path.join(django_settings.MEDIA_ROOT, im.name) assert os.path.exists(thumbnail_path) with transaction.atomic(get_using(product)): product.delete() diff --git a/test/testing_helpers.py b/test/testing_helpers.py index e66cfc8..5190a51 100644 --- a/test/testing_helpers.py +++ b/test/testing_helpers.py @@ -9,5 +9,5 @@ def get_using(instance): def get_random_pic_name(length=20): - return 'pic{}.jpg'.format( - ''.join(random.choice(string.ascii_letters) for m in range(length))) + random_str = ''.join(random.choice(string.ascii_letters) for m in range(length)) + return f'pic{random_str}.jpg' diff --git a/tox.ini b/tox.ini index e37ca54..ebf4415 100644 --- a/tox.ini +++ b/tox.ini @@ -1,19 +1,14 @@ [tox] envlist = - py{36,37,38,39,310,py3}-django32 - py{38,39,310,311,py3}-django{41,42} - py{312}-django42 - py{310,311,312,py3}-django{50,main} + py{38,39}-django{42} + py{310,311,312,py3}-django{42,50,51} [testenv] deps = - djangomain: https://github.com/django/django/tarball/main + # August 2024 - December 2025 + django51: django<5.2 # January 2024 - April 2025 django50: django<5.1 # LTS April 2023 - April 2026 django42: django<4.3 - # August 2022 - December 2023 - django41: django<4.2 - # LTS April 2021 - April 2024 - django32: django<3.3 -rtest/requirements.txt -commands=pytest test #-k "test_name" +commands = pytest test