diff --git a/.checks.yml b/.checks.yml
deleted file mode 100644
index 288e631..0000000
--- a/.checks.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-- bandit
-- flake8
-- pydocstyle
diff --git a/.fussyfox.yml b/.fussyfox.yml
deleted file mode 100644
index 600df19..0000000
--- a/.fussyfox.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-- bandit
-- flake8
-- isort
-- pydocstyle
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 71607d0..d232a23 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -1,5 +1,9 @@
version: 2
updates:
+- package-ecosystem: pip
+ directory: "/"
+ schedule:
+ interval: daily
- package-ecosystem: github-actions
directory: "/"
schedule:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..9ad71f1
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,97 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - master
+ pull_request:
+
+jobs:
+
+ msgcheck:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/setup-python@v4
+ with:
+ python-version: "3.10"
+ - uses: actions/checkout@v4
+ - run: sudo apt install -y gettext aspell libenchant-2-dev
+ - uses: actions/cache@v3
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ hashFiles('lint-requirements.txt') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+ - run: python -m pip install -r lint-requirements.txt
+ - run: msgcheck -n stdimage/locale/*/LC_MESSAGES/*.po
+
+ lint:
+ strategy:
+ fail-fast: false
+ matrix:
+ lint-command:
+ - "bandit -r . -x ./tests"
+ - "black --check --diff ."
+ - "flake8 ."
+ - "isort --check-only --diff ."
+ - "pydocstyle ."
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/setup-python@v4
+ with:
+ python-version: "3.10"
+ - uses: actions/checkout@v4
+ - uses: actions/cache@v3
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ hashFiles('lint-requirements.txt') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+ - run: python -m pip install -r lint-requirements.txt
+ - run: ${{ matrix.lint-command }}
+
+ dist:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v4
+ with:
+ python-version: "3.10"
+ - run: sudo apt install gettext -y
+ - run: python -m pip install --upgrade pip build wheel twine readme-renderer
+ - run: python -m build --sdist --wheel
+ - run: python -m twine check dist/*
+ - uses: actions/upload-artifact@v3
+ with:
+ path: dist/*
+
+ pytest:
+ runs-on: ubuntu-latest
+ needs:
+ - lint
+ - msgcheck
+ strategy:
+ matrix:
+ python-version:
+ - "3.8"
+ - "3.9"
+ - "3.10"
+ django-version:
+ - "3.2"
+ - "4.0"
+ extra:
+ - "test"
+ - "test,progressbar"
+ steps:
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ matrix.python-version }}
+ - run: sudo apt install gettext -y
+ - uses: actions/checkout@v4
+ - run: python -m pip install --upgrade pip codecov
+ - run: python -m pip install -e .[${{ matrix.extra }}]
+ if: ${{ matrix.extra }}
+ - run: python -m pip install django~=${{ matrix.django-version }}a
+ - run: python -m pytest
+ - run: codecov
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 02fcd4c..a71d600 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -1,25 +1,22 @@
-name: PyPi Release
+name: Release
-on: [release]
+on:
+ release:
+ types: [published]
jobs:
- build:
+ PyPi:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v1
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v1
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v4
with:
- python-version: ${{ matrix.python-version }}
- - name: Install gettext
- run: sudo apt-get install gettext -y
- - name: Install dependencies
- run: python -m pip install --upgrade pip setuptools wheel twine
- - name: Build dist packages
- run: python setup.py sdist bdist_wheel
- - name: Upload packages
- run: python -m twine upload dist/*
+ python-version: "3.10"
+ - run: sudo apt-get install gettext -y
+ - run: python -m pip install --upgrade pip build wheel twine
+ - run: python -m build --sdist --wheel
+ - run: python -m twine upload dist/*
env:
- TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }}
+ TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
deleted file mode 100644
index 026496f..0000000
--- a/.github/workflows/tests.yml
+++ /dev/null
@@ -1,45 +0,0 @@
-name: CI
-
-on:
- push:
- branches:
- - master
- pull_request:
-
-jobs:
-
- dist:
- runs-on: ubuntu-latest
- steps:
- - name: Install gettext
- run: sudo apt-get install gettext -y
- - uses: actions/setup-python@v2
- - run: python -m pip install --upgrade pip setuptools wheel twine readme-renderer
- - uses: actions/checkout@v2
- - run: python setup.py sdist bdist_wheel
- - run: python -m twine check dist/*
-
- pytest:
- runs-on: ubuntu-latest
- strategy:
- matrix:
- python-version:
- - "3.7"
- - "3.8"
- - "3.9"
- django-version:
- - "2.2"
- - "3.1"
- steps:
- - name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v1
- with:
- python-version: ${{ matrix.python-version }}
- - uses: actions/checkout@v1
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip setuptools codecov
- pip install django~=${{ matrix.django-version }}
- - name: Test with pytest
- run: python setup.py test
- - run: codecov
diff --git a/MANIFEST.in b/MANIFEST.in
index 6f39718..8fb7fc9 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -3,3 +3,4 @@ include stdimage/locale/*/LC_MESSAGES/django.mo
prune tests
prune .github
exclude .*
+exclude lint-requirements.txt
diff --git a/README.md b/README.md
index cc70313..77fe5f6 100644
--- a/README.md
+++ b/README.md
@@ -4,19 +4,110 @@
# Django Standardized Image Field
-Django Field that implement the following features:
+This package has been deprecated in favor of [django-pictures][django-pictures].
-* Django-Storages compatible (S3)
-* Resize images to different sizes
+## Migration Instructions
+
+First, make sure you understand the differences between the two packages and
+how to serve images in a modern web application via the [picture][picture-tag]-Element.
+
+Next, follow the setup instructions for [django-pictures][django-pictures].
+
+Once you are set up, change your models to use the new `PictureField` and provide the
+ `aspect_ratios` you'd like to serve. Do create migrations just yet.
+
+This step should be followed by changing your templates and frontend.
+The new placeholders feature for local development should help you
+to do this almost effortlessly.
+
+Finally, run `makemigrations` and replace the `AlterField` operation with
+`AlterPictureField`.
+
+We highly recommend to use Django's `image_width` and `image_height` fields, to avoid
+unnecessary IO. If you can add these fields to your model, you can use the following
+snippet to populate them:
+
+```python
+import django.core.files.storage
+from django.db import migrations, models
+import pictures.models
+from pictures.migrations import AlterPictureField
+
+def forward(apps, schema_editor):
+ for obj in apps.get_model("my-app.MyModel").objects.all().iterator():
+ obj.image_width = obj.logo.width
+ obj.image_height = obj.logo.height
+ obj.save(update_fields=["image_height", "image_width"])
+
+def backward(apps, schema_editor):
+ apps.get_model("my-app.MyModel").objects.all().update(
+ image_width=None,
+ image_height=None,
+ )
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('my-app', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="mymodel",
+ name="image_height",
+ field=models.PositiveIntegerField(editable=False, null=True),
+ ),
+ migrations.AddField(
+ model_name="mymodel",
+ name="image_width",
+ field=models.PositiveIntegerField(editable=False, null=True),
+ ),
+ migrations.RunPython(forward, backward),
+ AlterPictureField(
+ model_name="mymodel",
+ name="image",
+ field=pictures.models.PictureField(
+ aspect_ratios=["3/2", "3/1"],
+ breakpoints={"desktop": 1024, "mobile": 576},
+ container_width=1200,
+ file_types=["WEBP"],
+ grid_columns=12,
+ height_field="image_height",
+ pixel_densities=[1, 2],
+ storage=django.core.files.storage.FileSystemStorage(),
+ upload_to="pictures/",
+ verbose_name="image",
+ width_field="image_width",
+ ),
+ ),
+ ]
+```
+
+[django-pictures]: https://github.com/codingjoe/django-pictures
+[picture-tag]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/picture
+
+## Why would I want this?
+
+This is a drop-in replacement for the [Django ImageField](https://docs.djangoproject.com/en/1.8/ref/models/fields/#django.db.models.ImageField) that provides a standardized way to handle image uploads.
+It is designed to be as easy to use as possible, and to provide a consistent interface for all image fields.
+It allows images to be presented in various size variants (eg:thumbnails, mid, and hi-res versions)
+and it provides a way to handle images that are too large with validators.
+
+
+## Features
+
+Django Standardized Image Field implements the following features:
+
+* [Django-Storages](https://django-storages.readthedocs.io/en/latest/) compatible (eg: S3, Azure, Google Cloud Storage, etc)
+* Resizes images to different sizes
* Access thumbnails on model level, no template tags required
-* Preserves original image
-* Asynchronous rendering (Celery & Co)
-* Restrict accepted image dimensions
-* Rename files to a standardized name (using a callable upload_to)
+* Preserves original images
+* Can be rendered asynchronously (ie as a [Celery job](https://realpython.com/asynchronous-tasks-with-django-and-celery/))
+* Restricts acceptable image dimensions
+* Renames a file to a standardized name format (using a callable `upload_to` function, see below)
## Installation
-Simply install the latest stable package using the command
+Simply install the latest stable package using the following command:
```bash
pip install django-stdimage
@@ -28,11 +119,13 @@ and add `'stdimage'` to `INSTALLED_APP`s in your settings.py, that's it!
## Usage
+Now it's instally you can use either: `StdImageField` or `JPEGField`.
+
`StdImageField` works just like Django's own
[ImageField](https://docs.djangoproject.com/en/dev/ref/models/fields/#imagefield)
-except that you can specify different sized variations.
+except that you can specify different size variations.
-The `JPEGField` works similar to the `StdImageField` but all size variations are
+The `JPEGField` is identical to the `StdImageField` but all images are
converted to JPEGs, no matter what type the original file is.
### Variations
@@ -58,7 +151,7 @@ class MyModel(models.Model):
# is the same as dictionary-style call
image = StdImageField(upload_to='path/to/img', variations={'thumbnail': (100, 75)})
- # variations are converted to JPEGs
+ # JPEGField variations are converted to JPEGs.
jpeg = JPEGField(
upload_to='path/to/img',
variations={'full': (None, None), 'thumbnail': (100, 75)},
@@ -77,7 +170,7 @@ class MyModel(models.Model):
}, delete_orphans=True)
```
-For using generated variations in templates use `myimagefield.variation_name`.
+To use these variations in templates use `myimagefield.variation_name`.
Example:
@@ -85,15 +178,32 @@ Example:
```
-### Utils
+### Upload to function
-Since version 4 the custom `upload_to` utils have been dropped in favor of
-[Django Dynamic Filenames][dynamic_filenames].
+You can use a function for the `upload_to` argument. Using [Django Dynamic Filenames][dynamic_filenames].[dynamic_filenames]: https://github.com/codingjoe/django-dynamic-filenames
-[dynamic_filenames]: https://github.com/codingjoe/django-dynamic-filenames
+This allows images to be given unique paths and filenames based on the model instance.
+
+Example
+
+```python
+from django.db import models
+from stdimage import StdImageField
+from dynamic_filenames import FilePattern
+
+upload_to_pattern = FilePattern(
+ filename_pattern='my_model/{app_label:.25}/{model_name:.30}/{uuid:base32}{ext}',
+)
+
+
+class MyModel(models.Model):
+ # works just like django's ImageField
+ image = StdImageField(upload_to=upload_to_pattern)
+```
### Validators
-The `StdImageField` doesn't implement any size validation. Validation can be specified using the validator attribute
+The `StdImageField` doesn't implement any size validation out-of-the-box.
+However, Validation can be specified using the validator attribute
and using a set of validators shipped with this package.
Validators can be used for both Forms and Models.
@@ -120,9 +230,9 @@ Django [dropped support](https://docs.djangoproject.com/en/dev/releases/1.3/#del
for automated deletions in version 1.3.
Since version 5, this package supports a `delete_orphans` argument. It will delete
-orphaned files, should a file be delete or replaced via Django form or and object with
-a `StdImageField` be deleted. It will not be deleted if the field value is changed or
-reassigned programatically. In those rare cases, you will need to handle proper deletion
+orphaned files, should a file be deleted or replaced via a Django form and the object with
+the `StdImageField` be deleted. It will not delete files if the field value is changed or
+reassigned programatically. In these rare cases, you will need to handle proper deletion
yourself.
```python
@@ -141,10 +251,10 @@ class MyModel(models.Model):
### Async image processing
Tools like celery allow to execute time-consuming tasks outside of the request. If you don't want
-to wait for your variations to be rendered in request, StdImage provides your the option to pass a
-async keyword and a util.
-Note that the callback is not transaction save, but the file will be there.
-This example is based on celery.
+to wait for your variations to be rendered in request, StdImage provides you the option to pass an
+async keyword and a 'render_variations' function that triggers the async task.
+Note that the callback is not transaction save, but the file variations will be present.
+The example below is based on celery.
`tasks.py`:
```python
@@ -177,19 +287,18 @@ def image_processor(file_name, variations, storage):
class AsyncImageModel(models.Model):
image = StdImageField(
# above task definition can only handle one model object per image filename
- upload_to='path/to/file/',
+ upload_to='path/to/file/', # or use a function
render_variations=image_processor # pass boolean or callable
)
processed = models.BooleanField(default=False) # flag that could be used for view querysets
```
### Re-rendering variations
-You might want to add new variations to a field. That means you need to render new variations for missing fields.
+You might have added or changed variations to an existing field. That means you will need to render new variations.
This can be accomplished using a management command.
```bash
python manage.py rendervariations 'app_name.model_name.field_name' [--replace] [-i/--ignore-missing]
```
The `replace` option will replace all existing files.
-The `ignore-missing` option will suspend missing source file errors and keep
-rendering variations for other files. Othervise command will stop on first
-missing file.
+The `ignore-missing` option will suspend 'missing source file' errors and keep
+rendering variations for other files. Otherwise, the command will stop on first missing file.
diff --git a/lint-requirements.txt b/lint-requirements.txt
new file mode 100644
index 0000000..e0936be
--- /dev/null
+++ b/lint-requirements.txt
@@ -0,0 +1,6 @@
+bandit==1.7.5
+black==24.3.0
+flake8==6.1.0
+isort==5.12.0
+msgcheck==4.0.0
+pydocstyle==6.3.0
diff --git a/setup.cfg b/setup.cfg
index 4255691..5262f42 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -9,7 +9,7 @@ url = https://github.com/codingjoe/django-stdimage
license = MIT
license_file = LICENSE
classifier =
- Development Status :: 5 - Production/Stable
+ Development Status :: 7 - Inactive
Environment :: Web Environment
Framework :: Django
Topic :: Multimedia :: Graphics :: Graphics Conversion
@@ -20,7 +20,14 @@ classifier =
Topic :: Software Development
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
+ Programming Language :: Python :: 3.8
+ Programming Language :: Python :: 3.9
+ Programming Language :: Python :: 3.10
Framework :: Django
+ Framework :: Django :: 3.2
+ Framework :: Django :: 4.0
+
+python_requires = >=3.8
[options]
include_package_data = True
@@ -28,14 +35,9 @@ packages = stdimage
install_requires =
Django>=2.2
pillow>=2.5
- progressbar2>=3.0.0
+
setup_requires =
setuptools_scm
- pytest-runner
-tests_require =
- pytest
- pytest-cov
- pytest-django
[options.package_data]
* = *.txt, *.rst, *.html, *.po
@@ -44,36 +46,24 @@ tests_require =
exclude =
tests
+[options.extras_require]
+test =
+ pytest
+ pytest-cov
+ pytest-django
+progressbar = progressbar2>=3.0.0
+
[bdist_wheel]
universal = 1
-[aliases]
-test = pytest
-
[tool:pytest]
+testpaths =
+ tests
norecursedirs=venv env .eggs
DJANGO_SETTINGS_MODULE=tests.settings
-addopts = --cov=stdimage --cov-report xml --tb=short -rxs --nomigrations
-
-[tox:tox]
-envlist = py{36,37}-dj{111,22,master}
-
-[testenv]
-deps =
- dj111: https://github.com/django/django/archive/stable/1.11.x.tar.gz#egg=django
- dj22: https://github.com/django/django/archive/stable/2.2.x.tar.gz#egg=django
- djmaster: https://github.com/django/django/archive/master.tar.gz#egg=django
-commands = python setup.py test
-
-[flake8]
-max-line-length = 88
-statistics = true
-show-source = true
-exclude = */migrations/*,docs/*,env/*,venv/*,.tox/*,.eggs
-
-[pydocstyle]
-add-ignore = D1
-match-dir = (?!tests|env|docs|\.).*
+addopts = --cov=stdimage --nomigrations --tb=short
+filterwarnings =
+ ignore::DeprecationWarning
[coverage:run]
source = .
@@ -87,10 +77,22 @@ omit =
ignore_errors = True
show_missing = True
+[flake8]
+max_line_length=88
+select = C,E,F,W,B,B950
+ignore = E203, E501, W503
+
+[pydocstyle]
+add-ignore = D1
+match-dir = (?!tests|env|docs|\.).*
+
[isort]
atomic = true
line_length = 88
known_first_party = stdimage, tests
include_trailing_comma = True
+multi_line_output = 3
+force_grid_wrap = 0
+use_parentheses = True
default_section=THIRDPARTY
combine_as_imports = true
diff --git a/setup.py b/setup.py
index abf1228..e19500b 100755
--- a/setup.py
+++ b/setup.py
@@ -5,53 +5,54 @@
import subprocess # nosec
from distutils.cmd import Command
from distutils.command.build import build as _build
+from distutils.command.install import install as _install
from setuptools import setup
-from setuptools.command.install_lib import install_lib as _install_lib
BASE_DIR = os.path.dirname((os.path.abspath(__file__)))
class compile_translations(Command):
- description = 'Compile i18n translations using gettext.'
+ description = "Compile i18n translations using gettext."
user_options = []
def initialize_options(self):
- pass
+ self.build_lib = None
def finalize_options(self):
- pass
+ self.set_undefined_options("build", ("build_lib", "build_lib"))
def run(self):
- pattern = 'stdimage/locale/*/LC_MESSAGES/django.po'
+ pattern = "stdimage/locale/*/LC_MESSAGES/django.po"
for file in glob.glob(pattern):
- cmd = ['msgfmt', '-c']
name, ext = os.path.splitext(file)
-
- cmd += ['-o', '%s.mo' % name]
- cmd += ['%s.po' % name]
+ cmd = ["msgfmt", "-c", "-o", f"{self.build_lib}/{name}.mo", file]
self.announce(
- 'running command: %s' % ' '.join(cmd),
- level=distutils.log.INFO)
+ "running command: %s" % " ".join(cmd), level=distutils.log.INFO
+ )
subprocess.check_call(cmd, cwd=BASE_DIR) # nosec
class build(_build):
- sub_commands = [('compile_translations', None)] + _build.sub_commands
+ sub_commands = [
+ *_build.sub_commands,
+ ("compile_translations", None),
+ ]
-class install_lib(_install_lib):
- def run(self):
- self.run_command('compile_translations')
- _install_lib.run(self)
+class install(_install):
+ sub_commands = [
+ *_install.sub_commands,
+ ("compile_translations", None),
+ ]
setup(
- name='django-stdimage',
+ name="django-stdimage",
use_scm_version=True,
cmdclass={
- 'build': build,
- 'install_lib': install_lib,
- 'compile_translations': compile_translations,
+ "build": build,
+ "install": install,
+ "compile_translations": compile_translations,
},
)
diff --git a/stdimage/management/commands/rendervariations.py b/stdimage/management/commands/rendervariations.py
index 2ba3df3..bcc852e 100644
--- a/stdimage/management/commands/rendervariations.py
+++ b/stdimage/management/commands/rendervariations.py
@@ -1,4 +1,3 @@
-import progressbar
from django.apps import apps
from django.core.files.storage import get_storage_class
from django.core.management import BaseCommand, CommandError
@@ -7,44 +6,48 @@
class Command(BaseCommand):
- help = 'Renders all variations of a StdImageField.'
- args = ''
+ help = "Renders all variations of a StdImageField."
+ args = ""
def add_arguments(self, parser):
- parser.add_argument('field_path',
- nargs='+',
- type=str,
- help='')
- parser.add_argument('--replace',
- action='store_true',
- dest='replace',
- default=False,
- help='Replace existing files.')
+ parser.add_argument(
+ "field_path", nargs="+", type=str, help=""
+ )
+ parser.add_argument(
+ "--replace",
+ action="store_true",
+ dest="replace",
+ default=False,
+ help="Replace existing files.",
+ )
- parser.add_argument('-i', '--ignore-missing',
- action='store_true',
- dest='ignore_missing',
- default=False,
- help='Ignore missing source file error and '
- 'skip render for that file')
+ parser.add_argument(
+ "-i",
+ "--ignore-missing",
+ action="store_true",
+ dest="ignore_missing",
+ default=False,
+ help="Ignore missing source file error and " "skip render for that file",
+ )
def handle(self, *args, **options):
- replace = options.get('replace', False)
- ignore_missing = options.get('ignore_missing', False)
- routes = options.get('field_path', [])
+ replace = options.get("replace", False)
+ ignore_missing = options.get("ignore_missing", False)
+ routes = options.get("field_path", [])
for route in routes:
try:
- app_label, model_name, field_name = route.rsplit('.')
+ app_label, model_name, field_name = route.rsplit(".")
except ValueError:
- raise CommandError("Error parsing field_path '{}'. Use format "
- "."
- .format(route))
+ raise CommandError(
+ "Error parsing field_path '{}'. Use format "
+ ".".format(route)
+ )
model_class = apps.get_model(app_label, model_name)
field = model_class._meta.get_field(field_name)
- queryset = model_class._default_manager \
- .exclude(**{'%s__isnull' % field_name: True}) \
- .exclude(**{field_name: ''})
+ queryset = model_class._default_manager.exclude(
+ **{"%s__isnull" % field_name: True}
+ ).exclude(**{field_name: ""})
obj = queryset.first()
do_render = True
if obj:
@@ -53,11 +56,9 @@ def handle(self, *args, **options):
images = queryset.values_list(field_name, flat=True).iterator()
count = queryset.count()
- self.render(field, images, count, replace, ignore_missing,
- do_render)
+ self.render(field, images, count, replace, ignore_missing, do_render)
- @staticmethod
- def render(field, images, count, replace, ignore_missing, do_render):
+ def render(self, field, images, count, replace, ignore_missing, do_render):
kwargs_list = (
dict(
file_name=file_name,
@@ -70,28 +71,43 @@ def render(field, images, count, replace, ignore_missing, do_render):
)
for file_name in images
)
- with progressbar.ProgressBar(max_value=count, widgets=(
- progressbar.RotatingMarker(),
- ' | ', progressbar.AdaptiveETA(),
- ' | ', progressbar.Percentage(),
- ' ', progressbar.Bar(),
- )) as bar:
- for _ in map(render_field_variations, kwargs_list):
- bar += 1
+ try:
+ import progressbar
+ except ImportError:
+ for file_name in map(render_field_variations, kwargs_list):
+ self.stdout.write(f"Processing: {file_name}", self.style.NOTICE)
+ else:
+ with progressbar.ProgressBar(
+ max_value=count,
+ widgets=(
+ progressbar.RotatingMarker(),
+ " | ",
+ progressbar.AdaptiveETA(),
+ " | ",
+ progressbar.Percentage(),
+ " ",
+ progressbar.Bar(),
+ ),
+ ) as bar:
+ for _ in map(render_field_variations, kwargs_list):
+ bar += 1
def render_field_variations(kwargs):
- kwargs['storage'] = get_storage_class(kwargs['storage'])()
- ignore_missing = kwargs.pop('ignore_missing')
- do_render = kwargs.pop('do_render')
+ kwargs["storage"] = get_storage_class(kwargs["storage"])()
+ ignore_missing = kwargs.pop("ignore_missing")
+ do_render = kwargs.pop("do_render")
try:
if callable(do_render):
- kwargs.pop('field_class')
+ kwargs.pop("field_class")
do_render = do_render(**kwargs)
if do_render:
render_variations(**kwargs)
except FileNotFoundError as e:
if not ignore_missing:
+ print(ignore_missing)
raise CommandError(
- 'Source file was not found, terminating. '
- 'Use -i/--ignore-missing to skip this error.') from e
+ "Source file was not found, terminating. "
+ "Use -i/--ignore-missing to skip this error."
+ ) from e
+ return kwargs["file_name"]
diff --git a/stdimage/models.py b/stdimage/models.py
index e1528b6..dd429d8 100644
--- a/stdimage/models.py
+++ b/stdimage/models.py
@@ -1,19 +1,32 @@
import logging
import os
+import warnings
from io import BytesIO
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.db.models import signals
-from django.db.models.fields.files import (ImageField, ImageFieldFile,
- ImageFileDescriptor,)
+from django.db.models.fields.files import (
+ ImageField,
+ ImageFieldFile,
+ ImageFileDescriptor,
+)
from PIL import Image, ImageFile, ImageOps
+from PIL.Image import Resampling
from .validators import MinSizeValidator
logger = logging.getLogger()
+warnings.warn(
+ "The django-stdimage is deprecated in favor of django-pictures.\n"
+ "Migration instructions are available in the README:\n"
+ "https://github.com/codingjoe/django-stdimage#migration-instructions",
+ DeprecationWarning,
+)
+
+
class StdImageFileDescriptor(ImageFileDescriptor):
"""The variation property of the field is accessible in instance cases."""
@@ -37,16 +50,15 @@ def save(self, name, content, save=True):
if not isinstance(render_variations, bool):
msg = (
'"render_variations" callable expects a boolean return value,'
- ' but got %s'
- ) % type(render_variations)
+ " but got %s"
+ ) % type(render_variations)
raise TypeError(msg)
if render_variations:
self.render_variations()
@staticmethod
def is_smaller(img, variation):
- return img.size[0] > variation['width'] \
- or img.size[1] > variation['height']
+ return img.size[0] > variation["width"] or img.size[1] > variation["height"]
def render_variations(self, replace=True):
"""Render all image variations and saves them to the storage."""
@@ -54,11 +66,12 @@ def render_variations(self, replace=True):
self.render_variation(self.name, variation, replace, self.storage)
@classmethod
- def render_variation(cls, file_name, variation, replace=True,
- storage=default_storage):
+ def render_variation(
+ cls, file_name, variation, replace=True, storage=default_storage
+ ):
"""Render an image variation and saves it to the storage."""
- variation_name = cls.get_variation_name(file_name, variation['name'])
- file_overwrite = getattr(storage, 'file_overwrite', False)
+ variation_name = cls.get_variation_name(file_name, variation["name"])
+ file_overwrite = getattr(storage, "file_overwrite", False)
if not replace and storage.exists(variation_name):
logger.info('File "%s" already exists.', variation_name)
return variation_name
@@ -83,47 +96,38 @@ def process_variation(cls, variation, image):
"""Process variation before actual saving."""
save_kargs = {}
file_format = image.format
- save_kargs['format'] = file_format
+ save_kargs["format"] = file_format
- resample = variation['resample']
+ resample = variation["resample"]
if cls.is_smaller(image, variation):
factor = 1
- while image.size[0] / factor \
- > 2 * variation['width'] \
- and image.size[1] * 2 / factor \
- > 2 * variation['height']:
+ while (
+ image.size[0] / factor > 2 * variation["width"]
+ and image.size[1] * 2 / factor > 2 * variation["height"]
+ ):
factor *= 2
if factor > 1:
image.thumbnail(
- (int(image.size[0] / factor),
- int(image.size[1] / factor)),
- resample=resample
+ (int(image.size[0] / factor), int(image.size[1] / factor)),
+ resample=resample,
)
- size = variation['width'], variation['height']
- size = tuple(int(i) if i is not None else i
- for i in size)
+ size = variation["width"], variation["height"]
+ size = tuple(int(i) if i is not None else i for i in size)
- if file_format == 'JPEG':
+ if file_format == "JPEG":
# http://stackoverflow.com/a/21669827
- image = image.convert('RGB')
- save_kargs['optimize'] = True
- save_kargs['quality'] = 'web_high'
+ image = image.convert("RGB")
+ save_kargs["optimize"] = True
+ save_kargs["quality"] = "web_high"
if size[0] * size[1] > 10000: # roughly <10kb
- save_kargs['progressive'] = True
+ save_kargs["progressive"] = True
- if variation['crop']:
- image = ImageOps.fit(
- image,
- size,
- method=resample
- )
+ if variation["crop"]:
+ image = ImageOps.fit(image, size, method=resample)
else:
- image.thumbnail(
- size,
- resample=resample
- )
+ image.thumbnail(size, resample=resample)
return image, save_kargs
@@ -132,11 +136,13 @@ def get_variation_name(cls, file_name, variation_name):
"""Return the variation file name based on the variation."""
path, ext = os.path.splitext(file_name)
path, file_name = os.path.split(path)
- file_name = '{file_name}.{variation_name}{extension}'.format(**{
- 'file_name': file_name,
- 'variation_name': variation_name,
- 'extension': ext,
- })
+ file_name = "{file_name}.{variation_name}{extension}".format(
+ **{
+ "file_name": file_name,
+ "variation_name": variation_name,
+ "extension": ext,
+ }
+ )
return os.path.join(path, file_name)
def delete(self, save=True):
@@ -148,6 +154,25 @@ def delete_variations(self):
variation_name = self.get_variation_name(self.name, variation)
self.storage.delete(variation_name)
+ def __getstate__(self):
+ state = super().__getstate__()
+ state["variations"] = {}
+ for variation_name in self.field.variations:
+ if variation := getattr(self, variation_name, None):
+ variation_state = variation.__getstate__()
+ state["variations"][variation_name] = variation_state
+ return state
+
+ def __setstate__(self, state):
+ variations = state["variations"]
+ state.pop("variations")
+ super().__setstate__(state)
+ for key, value in variations.items():
+ cls = ImageFieldFile
+ field = cls.__new__(cls)
+ setattr(self, key, field)
+ getattr(self, key).__setstate__(value)
+
class StdImageField(ImageField):
"""
@@ -168,15 +193,22 @@ class StdImageField(ImageField):
descriptor_class = StdImageFileDescriptor
attr_class = StdImageFieldFile
def_variation = {
- 'width': None,
- 'height': None,
- 'crop': False,
- 'resample': Image.ANTIALIAS,
+ "width": None,
+ "height": None,
+ "crop": False,
+ "resample": Resampling.LANCZOS,
}
- def __init__(self, verbose_name=None, name=None, variations=None,
- render_variations=True, force_min_size=False, delete_orphans=False,
- **kwargs):
+ def __init__(
+ self,
+ verbose_name=None,
+ name=None,
+ variations=None,
+ render_variations=True,
+ force_min_size=False,
+ delete_orphans=False,
+ **kwargs,
+ ):
"""
Standardized ImageField for Django.
@@ -207,13 +239,12 @@ def __init__(self, verbose_name=None, name=None, variations=None,
if not variations:
variations = {}
if not isinstance(variations, dict):
- msg = ('"variations" expects a dict,'
- ' but got %s') % type(variations)
+ msg = ('"variations" expects a dict,' " but got %s") % type(variations)
raise TypeError(msg)
- if not (isinstance(render_variations, bool) or
- callable(render_variations)):
- msg = ('"render_variations" excepts a boolean or callable,'
- ' but got %s') % type(render_variations)
+ if not (isinstance(render_variations, bool) or callable(render_variations)):
+ msg = (
+ '"render_variations" excepts a boolean or callable,' " but got %s"
+ ) % type(render_variations)
raise TypeError(msg)
self._variations = variations
@@ -227,14 +258,15 @@ def __init__(self, verbose_name=None, name=None, variations=None,
if self.variations and self.force_min_size:
self.min_size = (
- max(self.variations.values(),
- key=lambda x: x["width"])["width"],
- max(self.variations.values(),
- key=lambda x: x["height"])["height"]
+ max(self.variations.values(), key=lambda x: x["width"])["width"],
+ max(self.variations.values(), key=lambda x: x["height"])["height"],
)
super().__init__(verbose_name=verbose_name, name=name, **kwargs)
+ # The attribute name of the old file to use on the model object
+ self._old_attname = "_old_%s" % name
+
def add_variation(self, name, params):
variation = self.def_variation.copy()
variation["kwargs"] = {}
@@ -260,12 +292,9 @@ def set_variations(self, instance=None, **kwargs):
if field._committed:
for name, variation in list(self.variations.items()):
variation_name = self.attr_class.get_variation_name(
- field.name,
- variation['name']
+ field.name, variation["name"]
)
- variation_field = ImageFieldFile(instance,
- self,
- variation_name)
+ variation_field = ImageFieldFile(instance, self, variation_name)
setattr(field, name, variation_field)
def post_delete_callback(self, sender, instance, **kwargs):
@@ -284,73 +313,83 @@ def validate(self, value, model_instance):
MinSizeValidator(self.min_size[0], self.min_size[1])(value)
def save_form_data(self, instance, data):
- if self.delete_orphans and self.blank and (data is False or data is not None):
+ if self.delete_orphans and (data is False or data is not None):
file = getattr(instance, self.name)
if file and file._committed and file != data:
- file.delete(save=False)
+ # Store the old file which should be deleted if the new one is valid
+ setattr(instance, self._old_attname, file)
super().save_form_data(instance, data)
+ def pre_save(self, model_instance, add):
+ if hasattr(model_instance, self._old_attname):
+ # Delete the old file and its variations from the storage
+ old_file = getattr(model_instance, self._old_attname)
+ old_file.delete_variations()
+ old_file.storage.delete(old_file.name)
+ delattr(model_instance, self._old_attname)
+ return super().pre_save(model_instance, add)
+
+ def deconstruct(self):
+ name, path, args, kwargs = super().deconstruct()
+ return (
+ name,
+ path,
+ args,
+ {
+ **kwargs,
+ "variations": self._variations,
+ "force_min_size": self.force_min_size,
+ },
+ )
-class JPEGFieldFile(StdImageFieldFile):
+class JPEGFieldFile(StdImageFieldFile):
@classmethod
def get_variation_name(cls, file_name, variation_name):
path = super().get_variation_name(file_name, variation_name)
path, ext = os.path.splitext(path)
- return '%s.jpeg' % path
+ return "%s.jpeg" % path
@classmethod
def process_variation(cls, variation, image):
"""Process variation before actual saving."""
save_kargs = {}
- file_format = 'JPEG'
- save_kargs['format'] = file_format
-
- resample = variation['resample']
+ file_format = "JPEG"
+ save_kargs["format"] = file_format
- if variation['width'] is None:
- variation['width'] = image.size[0]
+ resample = variation["resample"]
- if variation['height'] is None:
- variation['height'] = image.size[1]
+ width = image.size[0] if variation["width"] is None else variation["width"]
+ height = image.size[1] if variation["height"] is None else variation["height"]
factor = 1
- while image.size[0] / factor \
- > 2 * variation['width'] \
- and image.size[1] * 2 / factor \
- > 2 * variation['height']:
+ while (
+ image.size[0] / factor > 2 * width
+ and image.size[1] * 2 / factor > 2 * height
+ ):
factor *= 2
if factor > 1:
image.thumbnail(
- (int(image.size[0] / factor),
- int(image.size[1] / factor)),
- resample=resample
+ (int(image.size[0] / factor), int(image.size[1] / factor)),
+ resample=resample,
)
- size = variation['width'], variation['height']
- size = tuple(int(i) if i is not None else i
- for i in size)
+ size = width, height
+ size = tuple(int(i) if i is not None else i for i in size)
# http://stackoverflow.com/a/21669827
- image = image.convert('RGB')
- save_kargs['optimize'] = True
- save_kargs['quality'] = 'web_high'
+ image = image.convert("RGB")
+ save_kargs["optimize"] = True
+ save_kargs["quality"] = "web_high"
if size[0] * size[1] > 10000: # roughly <10kb
- save_kargs['progressive'] = True
+ save_kargs["progressive"] = True
- if variation['crop']:
- image = ImageOps.fit(
- image,
- size,
- method=resample
- )
+ if variation["crop"]:
+ image = ImageOps.fit(image, size, method=resample)
else:
- image.thumbnail(
- size,
- resample=resample
- )
+ image.thumbnail(size, resample=resample)
- save_kargs.update(variation['kwargs'])
+ save_kargs.update(variation["kwargs"])
return image, save_kargs
diff --git a/stdimage/utils.py b/stdimage/utils.py
index 667ab74..183289a 100644
--- a/stdimage/utils.py
+++ b/stdimage/utils.py
@@ -3,10 +3,13 @@
from .models import StdImageFieldFile
-def render_variations(file_name, variations, replace=False,
- storage=default_storage, field_class=StdImageFieldFile):
+def render_variations(
+ file_name,
+ variations,
+ replace=False,
+ storage=default_storage,
+ field_class=StdImageFieldFile,
+):
"""Render all variations for a given field."""
for key, variation in variations.items():
- field_class.render_variation(
- file_name, variation, replace, storage
- )
+ field_class.render_variation(file_name, variation, replace, storage)
diff --git a/stdimage/validators.py b/stdimage/validators.py
index b609c55..257374a 100644
--- a/stdimage/validators.py
+++ b/stdimage/validators.py
@@ -13,14 +13,14 @@ def compare(self, x):
return True
def __init__(self, width, height):
- self.limit_value = width or float('inf'), height or float('inf')
+ self.limit_value = width or float("inf"), height or float("inf")
def __call__(self, value):
cleaned = self.clean(value)
if self.compare(cleaned, self.limit_value):
params = {
- 'width': self.limit_value[0],
- 'height': self.limit_value[1],
+ "width": self.limit_value[0],
+ "height": self.limit_value[1],
}
raise ValidationError(self.message, code=self.code, params=params)
@@ -42,10 +42,13 @@ class MaxSizeValidator(BaseSizeValidator):
def compare(self, img_size, max_size):
return img_size[0] > max_size[0] or img_size[1] > max_size[1]
- message = _('The image you uploaded is too large.'
- ' The required maximum resolution is:'
- ' %(width)sx%(height)s px.')
- code = 'max_resolution'
+
+ message = _(
+ "The image you uploaded is too large."
+ " The required maximum resolution is:"
+ " %(width)sx%(height)s px."
+ )
+ code = "max_resolution"
class MinSizeValidator(BaseSizeValidator):
@@ -57,6 +60,9 @@ class MinSizeValidator(BaseSizeValidator):
def compare(self, img_size, min_size):
return img_size[0] < min_size[0] or img_size[1] < min_size[1]
- message = _('The image you uploaded is too small.'
- ' The required minimum resolution is:'
- ' %(width)sx%(height)s px.')
+
+ message = _(
+ "The image you uploaded is too small."
+ " The required minimum resolution is:"
+ " %(width)sx%(height)s px."
+ )
diff --git a/tests/admin.py b/tests/admin.py
index 0abab8b..da8f70a 100644
--- a/tests/admin.py
+++ b/tests/admin.py
@@ -3,6 +3,7 @@
from . import models
admin.site.register(models.AdminDeleteModel)
+admin.site.register(models.AdminUpdateModel)
admin.site.register(models.ResizeCropModel)
admin.site.register(models.ResizeModel)
admin.site.register(models.SimpleModel)
diff --git a/tests/conftest.py b/tests/conftest.py
index 33ebae0..bcce969 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -7,17 +7,14 @@
@pytest.fixture
def imagedata():
- img = Image.new('RGB', (250, 250), (255, 55, 255))
+ img = Image.new("RGB", (250, 250), (255, 55, 255))
output = io.BytesIO()
- img.save(output, format='JPEG')
+ img.save(output, format="JPEG")
return output
@pytest.fixture
def image_upload_file(imagedata):
- return SimpleUploadedFile(
- 'image.jpg',
- imagedata.getvalue()
- )
+ return SimpleUploadedFile("image.jpg", imagedata.getvalue())
diff --git a/tests/forms.py b/tests/forms.py
index 0e660fd..ff3927f 100644
--- a/tests/forms.py
+++ b/tests/forms.py
@@ -4,7 +4,12 @@
class ThumbnailModelForm(forms.ModelForm):
-
class Meta:
model = models.ThumbnailModel
- fields = '__all__'
+ fields = "__all__"
+
+
+class MinSizeModelForm(forms.ModelForm):
+ class Meta:
+ model = models.MinSizeModel
+ fields = "__all__"
diff --git a/tests/models.py b/tests/models.py
index 34a4255..4e91627 100644
--- a/tests/models.py
+++ b/tests/models.py
@@ -10,93 +10,109 @@
from stdimage.utils import render_variations
from stdimage.validators import MaxSizeValidator, MinSizeValidator
-upload_to = 'img/'
+upload_to = "img/"
class SimpleModel(models.Model):
"""works as ImageField"""
+
image = StdImageField(upload_to=upload_to)
class AdminDeleteModel(models.Model):
"""can be deleted through admin"""
+
image = StdImageField(
upload_to=upload_to,
variations={
- 'thumbnail': (100, 75),
+ "thumbnail": (100, 75),
},
blank=True,
delete_orphans=True,
)
+class AdminUpdateModel(models.Model):
+ """can be updated through admin, image not optional"""
+
+ image = StdImageField(
+ upload_to=upload_to,
+ variations={
+ "thumbnail": (100, 75),
+ },
+ blank=False,
+ delete_orphans=True,
+ )
+
+
class ResizeModel(models.Model):
"""resizes image to maximum size to fit a 640x480 area"""
+
image = StdImageField(
upload_to=upload_to,
variations={
- 'medium': {'width': 400, 'height': 400},
- 'thumbnail': (100, 75),
- }
+ "medium": {"width": 400, "height": 400},
+ "thumbnail": (100, 75),
+ },
)
class ResizeCropModel(models.Model):
"""resizes image to 640x480 cropping if necessary"""
+
image = StdImageField(
- upload_to=upload_to,
- variations={'thumbnail': (150, 150, True)}
+ upload_to=upload_to, variations={"thumbnail": (150, 150, True)}
)
class ThumbnailModel(models.Model):
"""creates a thumbnail resized to maximum size to fit a 100x75 area"""
+
image = StdImageField(
upload_to=upload_to,
blank=True,
- variations={'thumbnail': (100, 75)},
+ variations={"thumbnail": (100, 75)},
delete_orphans=True,
)
class JPEGModel(models.Model):
"""creates a thumbnail resized to maximum size to fit a 100x75 area"""
+
image = JPEGField(
upload_to=upload_to,
blank=True,
variations={
- 'full': (None, None),
- 'thumbnail': (100, 75, True),
- },
+ "full": (None, None),
+ "thumbnail": (100, 75, True),
+ },
delete_orphans=True,
)
class MaxSizeModel(models.Model):
- image = StdImageField(
- upload_to=upload_to,
- validators=[MaxSizeValidator(16, 16)]
- )
+ image = StdImageField(upload_to=upload_to, validators=[MaxSizeValidator(16, 16)])
class MinSizeModel(models.Model):
image = StdImageField(
upload_to=upload_to,
- validators=[MinSizeValidator(200, 200)]
+ delete_orphans=True,
+ validators=[MinSizeValidator(200, 200)],
)
class ForceMinSizeModel(models.Model):
"""creates a thumbnail resized to maximum size to fit a 100x75 area"""
+
image = StdImageField(
- upload_to=upload_to,
- force_min_size=True,
- variations={'thumbnail': (600, 600)}
+ upload_to=upload_to, force_min_size=True, variations={"thumbnail": (600, 600)}
)
class CustomManager(models.Manager):
"""Just like Django's default, but a different class."""
+
pass
@@ -109,18 +125,20 @@ class Meta:
class ManualVariationsModel(CustomManagerModel):
"""delays creation of 150x150 thumbnails until it is called manually"""
+
image = StdImageField(
upload_to=upload_to,
- variations={'thumbnail': (150, 150, True)},
- render_variations=False
+ variations={"thumbnail": (150, 150, True)},
+ render_variations=False,
)
class MyStorageModel(CustomManagerModel):
"""delays creation of 150x150 thumbnails until it is called manually"""
+
image = StdImageField(
upload_to=upload_to,
- variations={'thumbnail': (150, 150, True)},
+ variations={"thumbnail": (150, 150, True)},
storage=FileSystemStorage(),
)
@@ -132,18 +150,20 @@ def render_job(**kwargs):
class UtilVariationsModel(models.Model):
"""delays creation of 150x150 thumbnails until it is called manually"""
+
image = StdImageField(
upload_to=upload_to,
- variations={'thumbnail': (150, 150, True)},
- render_variations=render_job
+ variations={"thumbnail": (150, 150, True)},
+ render_variations=render_job,
)
class ThumbnailWithoutDirectoryModel(models.Model):
"""Save into a generated filename that does not contain any '/' char"""
+
image = StdImageField(
- upload_to=lambda instance, filename: 'custom.gif',
- variations={'thumbnail': {'width': 150, 'height': 150}},
+ upload_to=lambda instance, filename: "custom.gif",
+ variations={"thumbnail": {"width": 150, "height": 150}},
)
@@ -151,8 +171,7 @@ def custom_render_variations(file_name, variations, storage, replace=False):
"""Resize image to 100x100."""
for _, variation in variations.items():
variation_name = StdImageFieldFile.get_variation_name(
- file_name,
- variation['name']
+ file_name, variation["name"]
)
if storage.exists(variation_name):
storage.delete(variation_name)
@@ -163,7 +182,7 @@ def custom_render_variations(file_name, variations, storage, replace=False):
img = img.resize(size)
with BytesIO() as file_buffer:
- img.save(file_buffer, 'JPEG')
+ img.save(file_buffer, "JPEG")
f = ContentFile(file_buffer.getvalue())
storage.save(variation_name, f)
@@ -175,6 +194,6 @@ class CustomRenderVariationsModel(models.Model):
image = StdImageField(
upload_to=upload_to,
- variations={'thumbnail': (150, 150)},
+ variations={"thumbnail": (150, 150)},
render_variations=custom_render_variations,
)
diff --git a/tests/settings.py b/tests/settings.py
index 81d782a..480a744 100644
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -7,42 +7,42 @@
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': ':memory:',
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": ":memory:",
}
}
INSTALLED_APPS = (
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.sites',
- 'stdimage',
- 'tests'
+ "django.contrib.admin",
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.sites",
+ "stdimage",
+ "tests",
)
-DEFAULT_FILE_STORAGE = 'tests.storage.MyFileSystemStorage'
+DEFAULT_FILE_STORAGE = "tests.storage.MyFileSystemStorage"
TEMPLATES = [
{
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'APP_DIRS': True,
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "APP_DIRS": True,
}
]
MIDDLEWARE = MIDDLEWARE_CLASSES = (
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
)
MEDIA_ROOT = tempfile.mkdtemp()
SITE_ID = 1
-ROOT_URLCONF = 'tests.urls'
+ROOT_URLCONF = "tests.urls"
-SECRET_KEY = 'foobar'
+SECRET_KEY = "foobar"
-USE_L10N = True
+USE_TZ = True
diff --git a/tests/test_commands.py b/tests/test_commands.py
index 1e0b8b0..0ef69b1 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -11,45 +11,30 @@
@pytest.mark.django_db
class TestRenderVariations:
-
@pytest.fixture(autouse=True)
def _swap_concurrent_executor(self, monkeypatch):
"""Use ThreadPoolExecutor for coverage reports."""
monkeypatch.setattr(
- 'concurrent.futures.ProcessPoolExecutor',
+ "concurrent.futures.ProcessPoolExecutor",
ThreadPoolExecutor,
)
def test_no_options(self, image_upload_file):
- obj = ThumbnailModel.objects.create(
- image=image_upload_file
- )
+ obj = ThumbnailModel.objects.create(image=image_upload_file)
file_path = obj.image.thumbnail.path
obj.image.delete_variations()
- call_command(
- 'rendervariations',
- 'tests.ThumbnailModel.image'
- )
+ call_command("rendervariations", "tests.ThumbnailModel.image")
assert os.path.exists(file_path)
def test_multiprocessing(self, image_upload_file):
objs = [
- ThumbnailModel.objects.create(
- image=image_upload_file
- )
- for _ in range(100)
+ ThumbnailModel.objects.create(image=image_upload_file) for _ in range(100)
]
- file_names = [
- obj.image.thumbnail.path
- for obj in objs
- ]
+ file_names = [obj.image.thumbnail.path for obj in objs]
for obj in objs:
obj.image.delete_variations()
assert not any([os.path.exists(f) for f in file_names])
- call_command(
- 'rendervariations',
- 'tests.ThumbnailModel.image'
- )
+ call_command("rendervariations", "tests.ThumbnailModel.image")
assert any([os.path.exists(f) for f in file_names])
def test_no_replace(self, image_upload_file):
@@ -59,8 +44,8 @@ def test_no_replace(self, image_upload_file):
before = os.path.getmtime(file_path)
time.sleep(0.1)
call_command(
- 'rendervariations',
- 'tests.ThumbnailModel.image',
+ "rendervariations",
+ "tests.ThumbnailModel.image",
)
assert os.path.exists(file_path)
after = os.path.getmtime(file_path)
@@ -72,11 +57,7 @@ def test_replace(self, image_upload_file):
assert os.path.exists(file_path)
before = os.path.getmtime(file_path)
time.sleep(0.1)
- call_command(
- 'rendervariations',
- 'tests.ThumbnailModel.image',
- replace=True
- )
+ call_command("rendervariations", "tests.ThumbnailModel.image", replace=True)
assert os.path.exists(file_path)
after = os.path.getmtime(file_path)
assert before != after
@@ -89,9 +70,9 @@ def test_ignore_missing(self, image_upload_file):
assert not os.path.exists(file_path)
time.sleep(0.1)
call_command(
- 'rendervariations',
- 'tests.ThumbnailModel.image',
- '--ignore-missing',
+ "rendervariations",
+ "tests.ThumbnailModel.image",
+ "--ignore-missing",
replace=True,
)
@@ -103,9 +84,9 @@ def test_short_ignore_missing(self, image_upload_file):
assert not os.path.exists(file_path)
time.sleep(0.1)
call_command(
- 'rendervariations',
- 'tests.ThumbnailModel.image',
- '-i',
+ "rendervariations",
+ "tests.ThumbnailModel.image",
+ "-i",
replace=True,
)
@@ -118,48 +99,40 @@ def test_no_ignore_missing(self, image_upload_file):
time.sleep(0.1)
with pytest.raises(CommandError):
call_command(
- 'rendervariations',
- 'tests.ThumbnailModel.image',
+ "rendervariations",
+ "tests.ThumbnailModel.image",
replace=True,
)
def test_none_default_storage(self, image_upload_file):
- obj = MyStorageModel.customer_manager.create(
- image=image_upload_file
- )
+ obj = MyStorageModel.customer_manager.create(image=image_upload_file)
file_path = obj.image.thumbnail.path
obj.image.delete_variations()
- call_command(
- 'rendervariations',
- 'tests.MyStorageModel.image'
- )
+ call_command("rendervariations", "tests.MyStorageModel.image")
assert os.path.exists(file_path)
def test_invalid_field_path(self):
with pytest.raises(CommandError) as exc_info:
- call_command(
- 'rendervariations',
- 'MyStorageModel.image'
- )
+ call_command("rendervariations", "MyStorageModel.image")
- error_message = "Error parsing field_path 'MyStorageModel.image'. "\
- "Use format ."
+ error_message = (
+ "Error parsing field_path 'MyStorageModel.image'. "
+ "Use format ."
+ )
assert str(exc_info.value) == error_message
def test_custom_render_variations(self, image_upload_file):
- obj = CustomRenderVariationsModel.objects.create(
- image=image_upload_file
- )
+ obj = CustomRenderVariationsModel.objects.create(image=image_upload_file)
file_path = obj.image.thumbnail.path
assert os.path.exists(file_path)
- with open(file_path, 'rb') as f:
+ with open(file_path, "rb") as f:
before = hashlib.md5(f.read()).hexdigest()
call_command(
- 'rendervariations',
- 'tests.CustomRenderVariationsModel.image',
+ "rendervariations",
+ "tests.CustomRenderVariationsModel.image",
replace=True,
)
assert os.path.exists(file_path)
- with open(file_path, 'rb') as f:
+ with open(file_path, "rb") as f:
after = hashlib.md5(f.read()).hexdigest()
assert before == after
diff --git a/tests/test_forms.py b/tests/test_forms.py
index 1420d2c..03629b0 100644
--- a/tests/test_forms.py
+++ b/tests/test_forms.py
@@ -6,27 +6,26 @@
class TestStdImageField(TestStdImage):
-
def test_save_form_data__new(self, db):
- instance = models.ThumbnailModel.objects.create(image=self.fixtures['100.gif'])
+ instance = models.ThumbnailModel.objects.create(image=self.fixtures["100.gif"])
org_path = instance.image.path
assert os.path.exists(org_path)
form = forms.ThumbnailModelForm(
- files=dict(image=self.fixtures['600x400.jpg']),
+ files=dict(image=self.fixtures["600x400.jpg"]),
instance=instance,
)
assert form.is_valid()
obj = form.save()
- assert obj.image.name == 'img/600x400.jpg'
+ assert obj.image.name == "img/600x400.jpg"
assert os.path.exists(instance.image.path)
assert not os.path.exists(org_path)
def test_save_form_data__false(self, db):
- instance = models.ThumbnailModel.objects.create(image=self.fixtures['100.gif'])
+ instance = models.ThumbnailModel.objects.create(image=self.fixtures["100.gif"])
org_path = instance.image.path
assert os.path.exists(org_path)
form = forms.ThumbnailModelForm(
- data={'image-clear': '1'},
+ data={"image-clear": "1"},
instance=instance,
)
assert form.is_valid()
@@ -35,14 +34,27 @@ def test_save_form_data__false(self, db):
assert not os.path.exists(org_path)
def test_save_form_data__none(self, db):
- instance = models.ThumbnailModel.objects.create(image=self.fixtures['100.gif'])
+ instance = models.ThumbnailModel.objects.create(image=self.fixtures["100.gif"])
org_path = instance.image.path
assert os.path.exists(org_path)
form = forms.ThumbnailModelForm(
- data={'image': None},
+ data={"image": None},
instance=instance,
)
assert form.is_valid()
obj = form.save()
assert obj.image
assert os.path.exists(org_path)
+
+ def test_save_form_data__invalid(self, db):
+ instance = models.MinSizeModel.objects.create(
+ image=self.fixtures["600x400.jpg"]
+ )
+ org_path = instance.image.path
+ assert os.path.exists(org_path)
+ form = forms.MinSizeModelForm(
+ files={"image": self.fixtures["100.gif"]},
+ instance=instance,
+ )
+ assert not form.is_valid()
+ assert os.path.exists(org_path)
diff --git a/tests/test_models.py b/tests/test_models.py
index f681e7a..e234521 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,25 +1,37 @@
import io
import os
import time
+from copy import deepcopy
import pytest
from django.conf import settings
from django.core.files.storage import default_storage
from django.core.files.uploadedfile import SimpleUploadedFile
+from django.db.models.fields.files import ImageFieldFile
from PIL import Image
-from . import models
-from .models import (AdminDeleteModel, CustomRenderVariationsModel, ResizeCropModel,
- ResizeModel, SimpleModel, ThumbnailModel,
- ThumbnailWithoutDirectoryModel, UtilVariationsModel,)
+from stdimage.models import StdImageFieldFile
-IMG_DIR = os.path.join(settings.MEDIA_ROOT, 'img')
+from . import models
+from .models import (
+ AdminDeleteModel,
+ AdminUpdateModel,
+ CustomRenderVariationsModel,
+ ResizeCropModel,
+ ResizeModel,
+ SimpleModel,
+ ThumbnailModel,
+ ThumbnailWithoutDirectoryModel,
+ UtilVariationsModel,
+)
+
+IMG_DIR = os.path.join(settings.MEDIA_ROOT, "img")
FIXTURES = [
- ('100.gif', 'GIF', 100, 100),
- ('600x400.gif', 'GIF', 600, 400),
- ('600x400.jpg', 'JPEG', 600, 400),
- ('600x400.jpg', 'PNG', 600, 400),
+ ("100.gif", "GIF", 100, 100),
+ ("600x400.gif", "GIF", 600, 400),
+ ("600x400.jpg", "JPEG", 600, 400),
+ ("600x400.jpg", "PNG", 600, 400),
]
@@ -30,7 +42,7 @@ class TestStdImage:
def setup(self):
for fixture_filename, img_format, width, height in FIXTURES:
with io.BytesIO() as f:
- img = Image.new('RGB', (width, height), (255, 55, 255))
+ img = Image.new("RGB", (width, height), (255, 55, 255))
img.save(f, format=img_format)
suf = SimpleUploadedFile(fixture_filename, f.getvalue())
self.fixtures[fixture_filename] = suf
@@ -49,105 +61,100 @@ class TestModel(TestStdImage):
def test_simple(self, db):
"""Tests if Field behaves just like Django's ImageField."""
- instance = SimpleModel.objects.create(image=self.fixtures['100.gif'])
- target_file = os.path.join(IMG_DIR, '100.gif')
- source_file = self.fixtures['100.gif']
+ instance = SimpleModel.objects.create(image=self.fixtures["100.gif"])
+ target_file = os.path.join(IMG_DIR, "100.gif")
+ source_file = self.fixtures["100.gif"]
assert SimpleModel.objects.count() == 1
assert SimpleModel.objects.get(pk=1) == instance
assert os.path.exists(target_file)
- with open(target_file, 'rb') as f:
+ with open(target_file, "rb") as f:
source_file.seek(0)
assert source_file.read() == f.read()
def test_variations(self, db):
"""Adds image and checks filesystem as well as width and height."""
- instance = ResizeModel.objects.create(
- image=self.fixtures['600x400.jpg']
- )
+ instance = ResizeModel.objects.create(image=self.fixtures["600x400.jpg"])
- source_file = self.fixtures['600x400.jpg']
+ source_file = self.fixtures["600x400.jpg"]
- assert os.path.exists(os.path.join(IMG_DIR, '600x400.jpg'))
+ assert os.path.exists(os.path.join(IMG_DIR, "600x400.jpg"))
assert instance.image.width == 600
assert instance.image.height == 400
- path = os.path.join(IMG_DIR, '600x400.jpg')
+ path = os.path.join(IMG_DIR, "600x400.jpg")
- with open(path, 'rb') as f:
+ with open(path, "rb") as f:
source_file.seek(0)
assert source_file.read() == f.read()
- path = os.path.join(IMG_DIR, '600x400.medium.jpg')
+ path = os.path.join(IMG_DIR, "600x400.medium.jpg")
assert os.path.exists(path)
assert instance.image.medium.width == 400
assert instance.image.medium.height <= 400
- with open(os.path.join(IMG_DIR, '600x400.medium.jpg'), 'rb') as f:
+ with open(os.path.join(IMG_DIR, "600x400.medium.jpg"), "rb") as f:
source_file.seek(0)
assert source_file.read() != f.read()
- assert os.path.exists(os.path.join(IMG_DIR, '600x400.thumbnail.jpg'))
+ assert os.path.exists(os.path.join(IMG_DIR, "600x400.thumbnail.jpg"))
assert instance.image.thumbnail.width == 100
assert instance.image.thumbnail.height <= 75
- with open(os.path.join(IMG_DIR, '600x400.thumbnail.jpg'), 'rb') as f:
+ with open(os.path.join(IMG_DIR, "600x400.thumbnail.jpg"), "rb") as f:
source_file.seek(0)
assert source_file.read() != f.read()
def test_cropping(self, db):
- instance = ResizeCropModel.objects.create(
- image=self.fixtures['600x400.jpg']
- )
+ instance = ResizeCropModel.objects.create(image=self.fixtures["600x400.jpg"])
assert instance.image.thumbnail.width == 150
assert instance.image.thumbnail.height == 150
def test_variations_override(self, db):
- source_file = self.fixtures['600x400.jpg']
- target_file = os.path.join(IMG_DIR, 'image.thumbnail.jpg')
+ source_file = self.fixtures["600x400.jpg"]
+ target_file = os.path.join(IMG_DIR, "image.thumbnail.jpg")
os.mkdir(IMG_DIR)
default_storage.save(target_file, source_file)
- ResizeModel.objects.create(
- image=self.fixtures['600x400.jpg']
- )
- thumbnail_path = os.path.join(IMG_DIR, 'image.thumbnail.jpg')
+ ResizeModel.objects.create(image=self.fixtures["600x400.jpg"])
+ thumbnail_path = os.path.join(IMG_DIR, "image.thumbnail.jpg")
assert os.path.exists(thumbnail_path)
- thumbnail_path = os.path.join(IMG_DIR, 'image.thumbnail_1.jpg')
+ thumbnail_path = os.path.join(IMG_DIR, "image.thumbnail_1.jpg")
assert not os.path.exists(thumbnail_path)
def test_delete_thumbnail(self, db):
"""Delete an image with thumbnail"""
- obj = ThumbnailModel.objects.create(
- image=self.fixtures['100.gif']
- )
+ obj = ThumbnailModel.objects.create(image=self.fixtures["100.gif"])
obj.image.delete()
- path = os.path.join(IMG_DIR, 'image.gif')
+ path = os.path.join(IMG_DIR, "image.gif")
assert not os.path.exists(path)
- path = os.path.join(IMG_DIR, 'image.thumbnail.gif')
+ path = os.path.join(IMG_DIR, "image.thumbnail.gif")
assert not os.path.exists(path)
def test_fore_min_size(self, admin_client):
- admin_client.post('/admin/tests/forceminsizemodel/add/', {
- 'image': self.fixtures['100.gif'],
- })
- path = os.path.join(IMG_DIR, 'image.gif')
+ admin_client.post(
+ "/admin/tests/forceminsizemodel/add/",
+ {
+ "image": self.fixtures["100.gif"],
+ },
+ )
+ path = os.path.join(IMG_DIR, "image.gif")
assert not os.path.exists(path)
def test_thumbnail_save_without_directory(self, db):
obj = ThumbnailWithoutDirectoryModel.objects.create(
- image=self.fixtures['100.gif']
+ image=self.fixtures["100.gif"]
)
obj.save()
# Our model saves the images directly into the MEDIA_ROOT directory
# not IMG_DIR, under a custom name
- original = os.path.join(settings.MEDIA_ROOT, 'custom.gif')
- thumbnail = os.path.join(settings.MEDIA_ROOT, 'custom.thumbnail.gif')
+ original = os.path.join(settings.MEDIA_ROOT, "custom.gif")
+ thumbnail = os.path.join(settings.MEDIA_ROOT, "custom.thumbnail.gif")
assert os.path.exists(original)
assert os.path.exists(thumbnail)
def test_custom_render_variations(self, db):
instance = CustomRenderVariationsModel.objects.create(
- image=self.fixtures['600x400.jpg']
+ image=self.fixtures["600x400.jpg"]
)
# Image size must be 100x100 despite variations settings
assert instance.image.thumbnail.width == 100
@@ -160,54 +167,123 @@ def test_defer(self, db, django_assert_num_queries):
Accessing a deferred field would cause Django to do
a second implicit database query.
"""
- instance = ResizeModel.objects.create(image=self.fixtures['100.gif'])
+ instance = ResizeModel.objects.create(image=self.fixtures["100.gif"])
with django_assert_num_queries(1):
- deferred = ResizeModel.objects.only('pk').get(pk=instance.pk)
+ deferred = ResizeModel.objects.only("pk").get(pk=instance.pk)
with django_assert_num_queries(1):
deferred.image
assert instance.image.thumbnail == deferred.image.thumbnail
+ @pytest.mark.django_db
+ def test_variations_deepcopy_unsaved(self):
+ instance_original = ResizeModel(image=self.fixtures["600x400.jpg"])
+ instance = deepcopy(instance_original)
+ assert isinstance(instance.image, StdImageFieldFile)
+ assert instance.image == instance_original.image
+
+ @pytest.mark.django_db
+ def test_variations_deepcopy_without_image(self):
+ instance_original = ThumbnailModel.objects.create(image=None)
+ instance = deepcopy(instance_original)
+ assert isinstance(instance.image, StdImageFieldFile)
+ assert instance.image == instance_original.image
+
+ @pytest.mark.django_db
+ def test_variations_deepcopy(self):
+ """Tests test_variations() with a deep copied object"""
+ instance_original = ResizeModel.objects.create(
+ image=self.fixtures["600x400.jpg"]
+ )
+ instance = deepcopy(instance_original)
+ assert isinstance(instance.image, StdImageFieldFile)
+
+ assert hasattr(instance.image, "thumbnail")
+ assert hasattr(instance.image, "medium")
+
+ assert isinstance(instance.image.thumbnail, ImageFieldFile)
+ assert isinstance(instance.image.medium, ImageFieldFile)
+
+ source_file = self.fixtures["600x400.jpg"]
+
+ assert os.path.exists(os.path.join(IMG_DIR, "600x400.jpg"))
+ assert instance.image.width == 600
+ assert instance.image.height == 400
+ path = os.path.join(IMG_DIR, "600x400.jpg")
+
+ with open(path, "rb") as f:
+ source_file.seek(0)
+ assert source_file.read() == f.read()
+
+ path = os.path.join(IMG_DIR, "600x400.medium.jpg")
+ assert os.path.exists(path)
+ assert instance.image.medium.width == 400
+ assert instance.image.medium.height <= 400
+ with open(os.path.join(IMG_DIR, "600x400.medium.jpg"), "rb") as f:
+ source_file.seek(0)
+ assert source_file.read() != f.read()
+
+ assert os.path.exists(os.path.join(IMG_DIR, "600x400.thumbnail.jpg"))
+ assert instance.image.thumbnail.width == 100
+ assert instance.image.thumbnail.height <= 75
+ with open(os.path.join(IMG_DIR, "600x400.thumbnail.jpg"), "rb") as f:
+ source_file.seek(0)
+ assert source_file.read() != f.read()
+
class TestUtils(TestStdImage):
"""Tests Utils"""
def test_deletion_singnal_receiver(self, db):
- obj = AdminDeleteModel.objects.create(
- image=self.fixtures['100.gif']
- )
+ obj = AdminDeleteModel.objects.create(image=self.fixtures["100.gif"])
path = obj.image.path
obj.delete()
assert not os.path.exists(path)
def test_deletion_singnal_receiver_many(self, db):
- obj = AdminDeleteModel.objects.create(
- image=self.fixtures['100.gif']
- )
+ obj = AdminDeleteModel.objects.create(image=self.fixtures["100.gif"])
path = obj.image.path
AdminDeleteModel.objects.all().delete()
assert not os.path.exists(path)
def test_pre_save_delete_callback_clear(self, admin_client):
- obj = AdminDeleteModel.objects.create(
- image=self.fixtures['100.gif']
- )
+ obj = AdminDeleteModel.objects.create(image=self.fixtures["100.gif"])
path = obj.image.path
- admin_client.post('/admin/tests/admindeletemodel/1/change/', {
- 'image-clear': 'checked',
- })
+ admin_client.post(
+ "/admin/tests/admindeletemodel/1/change/",
+ {
+ "image-clear": "checked",
+ },
+ )
assert not os.path.exists(path)
def test_pre_save_delete_callback_new(self, admin_client):
- AdminDeleteModel.objects.create(
- image=self.fixtures['100.gif']
+ obj = AdminDeleteModel.objects.create(image=self.fixtures["100.gif"])
+ path = obj.image.path
+ assert os.path.exists(path)
+ admin_client.post(
+ "/admin/tests/admindeletemodel/1/change/",
+ {
+ "image": self.fixtures["600x400.jpg"],
+ },
)
- admin_client.post('/admin/tests/admindeletemodel/1/change/', {
- 'image': self.fixtures['600x400.jpg'],
- })
- assert not os.path.exists(os.path.join(IMG_DIR, 'image.gif'))
+ assert not os.path.exists(path)
+ assert os.path.exists(os.path.join(IMG_DIR, "600x400.jpg"))
+
+ def test_pre_save_delete_callback_update(self, admin_client):
+ obj = AdminUpdateModel.objects.create(image=self.fixtures["100.gif"])
+ path = obj.image.path
+ assert os.path.exists(path)
+ admin_client.post(
+ "/admin/tests/adminupdatemodel/1/change/",
+ {
+ "image": self.fixtures["600x400.jpg"],
+ },
+ )
+ assert not os.path.exists(path)
+ assert os.path.exists(os.path.join(IMG_DIR, "600x400.jpg"))
def test_render_variations_callback(self, db):
- obj = UtilVariationsModel.objects.create(image=self.fixtures['100.gif'])
+ obj = UtilVariationsModel.objects.create(image=self.fixtures["100.gif"])
file_path = obj.image.thumbnail.path
assert os.path.exists(file_path)
@@ -227,23 +303,39 @@ def test_render_variations_overwrite(self, db, image_upload_file):
class TestValidators(TestStdImage):
def test_max_size_validator(self, admin_client):
- response = admin_client.post('/admin/tests/maxsizemodel/add/', {
- 'image': self.fixtures['600x400.jpg'],
- })
- assert 'too large' in response.context['adminform'].form.errors['image'][0]
- assert not os.path.exists(os.path.join(IMG_DIR, '800x600.jpg'))
+ response = admin_client.post(
+ "/admin/tests/maxsizemodel/add/",
+ {
+ "image": self.fixtures["600x400.jpg"],
+ },
+ )
+ assert "too large" in response.context["adminform"].form.errors["image"][0]
+ assert not os.path.exists(os.path.join(IMG_DIR, "800x600.jpg"))
def test_min_size_validator(self, admin_client):
- response = admin_client.post('/admin/tests/minsizemodel/add/', {
- 'image': self.fixtures['100.gif'],
- })
- assert 'too small' in response.context['adminform'].form.errors['image'][0]
- assert not os.path.exists(os.path.join(IMG_DIR, '100.gif'))
+ response = admin_client.post(
+ "/admin/tests/minsizemodel/add/",
+ {
+ "image": self.fixtures["100.gif"],
+ },
+ )
+ assert "too small" in response.context["adminform"].form.errors["image"][0]
+ assert not os.path.exists(os.path.join(IMG_DIR, "100.gif"))
class TestJPEGField(TestStdImage):
def test_convert(self, db):
- obj = models.JPEGModel.objects.create(image=self.fixtures['100.gif'])
- assert obj.image.thumbnail.path.endswith('img/100.thumbnail.jpeg')
+ obj = models.JPEGModel.objects.create(image=self.fixtures["100.gif"])
+ assert obj.image.thumbnail.path.endswith("img/100.thumbnail.jpeg")
assert obj.image.full.width == 100
assert obj.image.full.height == 100
+
+ def test_convert_multiple(self, db):
+ large = models.JPEGModel.objects.create(image=self.fixtures["600x400.gif"])
+ small = models.JPEGModel.objects.create(image=self.fixtures["100.gif"])
+
+ assert large.image.field._variations["full"] == (None, None)
+ assert small.image.field._variations["full"] == (None, None)
+
+ assert large.image.full.width == 600
+ assert small.image.full.width == 100
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 047a651..7180204 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,7 +1,7 @@
import os
import pytest
-from PIL import Image
+from PIL.Image import Resampling
from stdimage.utils import render_variations
from tests.models import ManualVariationsModel
@@ -14,18 +14,18 @@ def test_render_variations(self, image_upload_file):
instance = ManualVariationsModel.customer_manager.create(
image=image_upload_file
)
- path = os.path.join(IMG_DIR, 'image.thumbnail.jpg')
+ path = os.path.join(IMG_DIR, "image.thumbnail.jpg")
assert not os.path.exists(path)
render_variations(
file_name=instance.image.name,
variations={
- 'thumbnail': {
- 'name': 'thumbnail',
- 'width': 150,
- 'height': 150,
- 'crop': True,
- 'resample': Image.ANTIALIAS
+ "thumbnail": {
+ "name": "thumbnail",
+ "width": 150,
+ "height": 150,
+ "crop": True,
+ "resample": Resampling.LANCZOS,
}
- }
+ },
)
assert os.path.exists(path)
diff --git a/tests/test_validators.py b/tests/test_validators.py
index 4a3b4ac..5ccdd93 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -4,7 +4,8 @@
class TestBaseSizeValidator:
def test_init__none(self):
assert validators.MinSizeValidator(None, None).limit_value == (
- float('inf'), float('inf')
+ float("inf"),
+ float("inf"),
)
diff --git a/tests/urls.py b/tests/urls.py
index d69c573..f8aa21b 100644
--- a/tests/urls.py
+++ b/tests/urls.py
@@ -1,8 +1,8 @@
-from django.conf.urls import url
from django.contrib import admin
+from django.urls import path
admin.autodiscover()
urlpatterns = [
- url(r'^admin/', admin.site.urls),
+ path("admin/", admin.site.urls),
]