diff --git a/.github/workflows/test_js.yaml b/.github/workflows/test_js.yaml new file mode 100644 index 0000000..21661d4 --- /dev/null +++ b/.github/workflows/test_js.yaml @@ -0,0 +1,20 @@ +name: JS tests + +on: [push] + +jobs: + test: + name: Test + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install + run: | + corepack enable + make nodejs + + - name: Run tests + run: make wtr diff --git a/.github/workflows/test_py.yaml b/.github/workflows/test_py.yaml new file mode 100644 index 0000000..9ba97b1 --- /dev/null +++ b/.github/workflows/test_py.yaml @@ -0,0 +1,50 @@ +name: Python tests + +on: [push] + +jobs: + test: + name: Test ${{ matrix.python }} - ${{ matrix.os }} + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - windows-latest + - macos-latest + + python: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + + - name: Install + run: | + pip install wheel + pip install coverage + pip install https://github.com/conestack/webresource/archive/master.zip + pip install https://github.com/conestack/yafowil/archive/master.zip + pip install -e .[test] + + - name: Run tests + run: | + python --version + python -m pytest src/yafowil/widget/image/tests + + - name: Run coverage + run: | + coverage run --source=src/yafowil/widget/image --omit=src/yafowil/widget/image/example.py -m pytest src/yafowil/widget/image/tests + coverage report --fail-under=99 diff --git a/.gitignore b/.gitignore index c42b36c..1b20c06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,20 @@ -*~ -*#* +*.egg-info *.pyc *.pyo -*.egg-info -/develop-eggs/ -/parts/ -/bin/ -/eggs/ -/downloads/ -/var/ +/.coverage +/.mxmake/ +/coverage/ /dist/ -/.installed.cfg -/.pydevproject -/.project -/.DS_Store -/.mr.developer.cfg -/src/yafowil/widget/image/testing/micro.png -/src/yafowil/widget/image/testing/cropped.png -/src/yafowil/widget/image/testing/landscape.png -/src/yafowil/widget/image/testing/portrait.png +/htmlcov/ +/js/karma/ +/node_modules/ +/package-lock.json +/pnpm-lock.yaml +/py2/ +/py3/ +/pypy3/ +/requirements-mxdev.txt +/src/yafowil/widget/image/images_tmp /src/yafowil/widget/image/testing/crop_fitting_ls_18_30.png /src/yafowil/widget/image/testing/crop_fitting_ls_30_18.png /src/yafowil/widget/image/testing/crop_fitting_pt_18_30.png @@ -27,8 +23,12 @@ /src/yafowil/widget/image/testing/crop_fitting_sq_40_50.png /src/yafowil/widget/image/testing/crop_fitting_sq_48_40.png /src/yafowil/widget/image/testing/crop_size_20_20.png -/src/yafowil/widget/image/testing/crop_size_20_40.png /src/yafowil/widget/image/testing/crop_size_20_40_offset_5_3.png +/src/yafowil/widget/image/testing/crop_size_20_40.png /src/yafowil/widget/image/testing/crop_size_40_20.png /src/yafowil/widget/image/testing/crop_size_50_20_offset_5_0.png -/src/yafowil/widget/image/images_tmp +/src/yafowil/widget/image/testing/cropped.png +/src/yafowil/widget/image/testing/landscape.png +/src/yafowil/widget/image/testing/micro.png +/src/yafowil/widget/image/testing/portrait.png +/venv/ diff --git a/CHANGES.rst b/CHANGES.rst index c17fbe4..a40289d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,8 +1,30 @@ Changes ======= -1.6 (unreleased) ----------------- +2.0a3 (unreleased) +------------------ + +- Nothing changed yet. + + +2.0a2 (2024-05-23) +------------------ + +- Fix deprecated imports. + [rnix] + +- Use Image.Resampling.LANCZOS instead of ANTIALIAS. + [rnix] + + +2.0a1 (2023-05-15) +------------------ + +- Add ``webresource`` support. + [rnix] + +- Rewrite JavaScript using ES6. + [rnix] - Introduce ``rounddpi`` flag for image blueprint. Pillow, as of version 6.0, no longer rounds reported DPI values for BMP, JPEG and PNG images, but image diff --git a/LICENSE.rst b/LICENSE.rst index 01c1de3..7eab66e 100644 --- a/LICENSE.rst +++ b/LICENSE.rst @@ -2,7 +2,7 @@ License ======= Copyright (c) 2012-2021, BlueDynamics Alliance, Austria, Germany, Switzerland -Copyright (c) 2021, Yafowil Contributors +Copyright (c) 2021-2024, Yafowil Contributors All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6c69c3b --- /dev/null +++ b/Makefile @@ -0,0 +1,581 @@ +############################################################################## +# THIS FILE IS GENERATED BY MXMAKE +# +# DOMAINS: +#: core.base +#: core.mxenv +#: core.mxfiles +#: core.packages +#: js.nodejs +#: js.rollup +#: js.wtr +#: qa.coverage +#: qa.test +# +# SETTINGS (ALL CHANGES MADE BELOW SETTINGS WILL BE LOST) +############################################################################## + +## core.base + +# `deploy` target dependencies. +# No default value. +DEPLOY_TARGETS?= + +# target to be executed when calling `make run` +# No default value. +RUN_TARGET?= + +# Additional files and folders to remove when running clean target +# No default value. +CLEAN_FS?= + +# Optional makefile to include before default targets. This can +# be used to provide custom targets or hook up to existing targets. +# Default: include.mk +INCLUDE_MAKEFILE?=include.mk + +# Optional additional directories to be added to PATH in format +# `/path/to/dir/:/path/to/other/dir`. Gets inserted first, thus gets searched +# first. +# No default value. +EXTRA_PATH?= + +## js.nodejs + +# The package manager to use. Defaults to `npm`. Possible values +# are `npm` and `pnpm` +# Default: npm +NODEJS_PACKAGE_MANAGER?=pnpm + +# Value for `--prefix` option when installing packages. +# Default: . +NODEJS_PREFIX?=. + +# Packages to install with `--no-save` option. +# No default value. +NODEJS_PACKAGES?= + +# Packages to install with `--save-dev` option. +# No default value. +NODEJS_DEV_PACKAGES?= + +# Packages to install with `--save-prod` option. +# No default value. +NODEJS_PROD_PACKAGES?= + +# Packages to install with `--save-optional` option. +# No default value. +NODEJS_OPT_PACKAGES?= + +# Additional install options. Possible values are `--save-exact` +# and `--save-bundle`. +# No default value. +NODEJS_INSTALL_OPTS?= + +## js.wtr + +# Web test runner config file. +# Default: wtr.config.mjs +WTR_CONFIG?=js/wtr.config.mjs + +# Web test runner additional command line options. +# Default: --coverage +WTR_OPTIONS?=--coverage + +## js.rollup + +# Rollup config file. +# Default: rollup.conf.js +ROLLUP_CONFIG?=js/rollup.conf.js + +## core.mxenv + +# Primary Python interpreter to use. It is used to create the +# virtual environment if `VENV_ENABLED` and `VENV_CREATE` are set to `true`. +# Default: python3 +PRIMARY_PYTHON?=python3 + +# Minimum required Python version. +# Default: 3.9 +PYTHON_MIN_VERSION?=3.9 + +# Install packages using the given package installer method. +# Supported are `pip` and `uv`. If uv is used, its global availability is +# checked. Otherwise, it is installed, either in the virtual environment or +# using the `PRIMARY_PYTHON`, dependent on the `VENV_ENABLED` setting. If +# `VENV_ENABLED` and uv is selected, uv is used to create the virtual +# environment. +# Default: pip +PYTHON_PACKAGE_INSTALLER?=uv + +# Flag whether to use a global installed 'uv' or install +# it in the virtual environment. +# Default: false +MXENV_UV_GLOBAL?=false + +# Flag whether to use virtual environment. If `false`, the +# interpreter according to `PRIMARY_PYTHON` found in `PATH` is used. +# Default: true +VENV_ENABLED?=true + +# Flag whether to create a virtual environment. If set to `false` +# and `VENV_ENABLED` is `true`, `VENV_FOLDER` is expected to point to an +# existing virtual environment. +# Default: true +VENV_CREATE?=true + +# The folder of the virtual environment. +# If `VENV_ENABLED` is `true` and `VENV_CREATE` is true it is used as the +# target folder for the virtual environment. If `VENV_ENABLED` is `true` and +# `VENV_CREATE` is false it is expected to point to an existing virtual +# environment. If `VENV_ENABLED` is `false` it is ignored. +# Default: .venv +VENV_FOLDER?=venv + +# mxdev to install in virtual environment. +# Default: mxdev +MXDEV?=mxdev + +# mxmake to install in virtual environment. +# Default: mxmake +MXMAKE?=mxmake + +## core.mxfiles + +# The config file to use. +# Default: mx.ini +PROJECT_CONFIG?=mx.ini + +## core.packages + +# Allow prerelease and development versions. +# By default, the package installer only finds stable versions. +# Default: false +PACKAGES_ALLOW_PRERELEASES?=false + +## qa.test + +# The command which gets executed. Defaults to the location the +# :ref:`run-tests` template gets rendered to if configured. +# Default: .mxmake/files/run-tests.sh +TEST_COMMAND?=$(VENV_FOLDER)/bin/pytest src/yafowil/widget/image/tests + +# Additional Python requirements for running tests to be +# installed (via pip). +# Default: pytest +TEST_REQUIREMENTS?=pytest + +# Additional make targets the test target depends on. +# No default value. +TEST_DEPENDENCY_TARGETS?= + +## qa.coverage + +# The command which gets executed. Defaults to the location the +# :ref:`run-coverage` template gets rendered to if configured. +# Default: .mxmake/files/run-coverage.sh +COVERAGE_COMMAND?=\ + $(VENV_FOLDER)/bin/coverage run \ + --omit src/yafowil/widget/image/example.py \ + --source src/yafowil/widget/image \ + -m pytest src/yafowil/widget/image/tests \ + && $(VENV_FOLDER)/bin/coverage report --fail-under=99 + + +############################################################################## +# END SETTINGS - DO NOT EDIT BELOW THIS LINE +############################################################################## + +INSTALL_TARGETS?= +DIRTY_TARGETS?= +CLEAN_TARGETS?= +PURGE_TARGETS?= +CHECK_TARGETS?= +TYPECHECK_TARGETS?= +FORMAT_TARGETS?= + +export PATH:=$(if $(EXTRA_PATH),$(EXTRA_PATH):,)$(PATH) + +# Defensive settings for make: https://tech.davis-hansson.com/p/make/ +SHELL:=bash +.ONESHELL: +# for Makefile debugging purposes add -x to the .SHELLFLAGS +.SHELLFLAGS:=-eu -o pipefail -O inherit_errexit -c +.SILENT: +.DELETE_ON_ERROR: +MAKEFLAGS+=--warn-undefined-variables +MAKEFLAGS+=--no-builtin-rules + +# mxmake folder +MXMAKE_FOLDER?=.mxmake + +# Sentinel files +SENTINEL_FOLDER?=$(MXMAKE_FOLDER)/sentinels +SENTINEL?=$(SENTINEL_FOLDER)/about.txt +$(SENTINEL): $(firstword $(MAKEFILE_LIST)) + @mkdir -p $(SENTINEL_FOLDER) + @echo "Sentinels for the Makefile process." > $(SENTINEL) + +############################################################################## +# nodejs +############################################################################## + +export PATH:=$(shell pwd)/$(NODEJS_PREFIX)/node_modules/.bin:$(PATH) + + +NODEJS_TARGET:=$(SENTINEL_FOLDER)/nodejs.sentinel +$(NODEJS_TARGET): $(SENTINEL) + @echo "Install nodejs packages" + @test -z "$(NODEJS_DEV_PACKAGES)" \ + && echo "No dev packages to be installed" \ + || $(NODEJS_PACKAGE_MANAGER) --prefix $(NODEJS_PREFIX) install \ + --save-dev \ + $(NODEJS_INSTALL_OPTS) \ + $(NODEJS_DEV_PACKAGES) + @test -z "$(NODEJS_PROD_PACKAGES)" \ + && echo "No prod packages to be installed" \ + || $(NODEJS_PACKAGE_MANAGER) --prefix $(NODEJS_PREFIX) install \ + --save-prod \ + $(NODEJS_INSTALL_OPTS) \ + $(NODEJS_PROD_PACKAGES) + @test -z "$(NODEJS_OPT_PACKAGES)" \ + && echo "No opt packages to be installed" \ + || $(NODEJS_PACKAGE_MANAGER) --prefix $(NODEJS_PREFIX) install \ + --save-optional \ + $(NODEJS_INSTALL_OPTS) \ + $(NODEJS_OPT_PACKAGES) + @test -z "$(NODEJS_PACKAGES)" \ + && echo "No packages to be installed" \ + || $(NODEJS_PACKAGE_MANAGER) --prefix $(NODEJS_PREFIX) install \ + --no-save \ + $(NODEJS_PACKAGES) + @touch $(NODEJS_TARGET) + +.PHONY: nodejs +nodejs: $(NODEJS_TARGET) + +.PHONY: nodejs-dirty +nodejs-dirty: + @rm -f $(NODEJS_TARGET) + +.PHONY: nodejs-clean +nodejs-clean: nodejs-dirty + @rm -rf $(NODEJS_PREFIX)/node_modules + +INSTALL_TARGETS+=nodejs +DIRTY_TARGETS+=nodejs-dirty +CLEAN_TARGETS+=nodejs-clean + +############################################################################## +# web test runner +############################################################################## + +NODEJS_DEV_PACKAGES+=\ + @web/test-runner \ + @web/dev-server-import-maps + +.PHONY: wtr +wtr: $(NODEJS_TARGET) + @web-test-runner $(WTR_OPTIONS) --config $(WTR_CONFIG) + +############################################################################## +# rollup +############################################################################## + +NODEJS_DEV_PACKAGES+=\ + rollup \ + rollup-plugin-cleanup \ + @rollup/plugin-terser + +.PHONY: rollup +rollup: $(NODEJS_TARGET) + @rollup --config $(ROLLUP_CONFIG) + +############################################################################## +# mxenv +############################################################################## + +export OS:=$(OS) + +# Determine the executable path +ifeq ("$(VENV_ENABLED)", "true") +export VIRTUAL_ENV=$(abspath $(VENV_FOLDER)) +ifeq ("$(OS)", "Windows_NT") +VENV_EXECUTABLE_FOLDER=$(VIRTUAL_ENV)/Scripts +else +VENV_EXECUTABLE_FOLDER=$(VIRTUAL_ENV)/bin +endif +export PATH:=$(VENV_EXECUTABLE_FOLDER):$(PATH) +MXENV_PYTHON=python +else +MXENV_PYTHON=$(PRIMARY_PYTHON) +endif + +# Determine the package installer +ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") +PYTHON_PACKAGE_COMMAND=uv pip +else +PYTHON_PACKAGE_COMMAND=$(MXENV_PYTHON) -m pip +endif + +MXENV_TARGET:=$(SENTINEL_FOLDER)/mxenv.sentinel +$(MXENV_TARGET): $(SENTINEL) + @$(PRIMARY_PYTHON) -c "import sys; vi = sys.version_info; sys.exit(1 if (int(vi[0]), int(vi[1])) >= tuple(map(int, '$(PYTHON_MIN_VERSION)'.split('.'))) else 0)" \ + && echo "Need Python >= $(PYTHON_MIN_VERSION)" && exit 1 || : + @[[ "$(VENV_ENABLED)" == "true" && "$(VENV_FOLDER)" == "" ]] \ + && echo "VENV_FOLDER must be configured if VENV_ENABLED is true" && exit 1 || : + @[[ "$(VENV_ENABLED)$(PYTHON_PACKAGE_INSTALLER)" == "falseuv" ]] \ + && echo "Package installer uv does not work with a global Python interpreter." && exit 1 || : +ifeq ("$(VENV_ENABLED)", "true") +ifeq ("$(VENV_CREATE)", "true") +ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvtrue") + @echo "Setup Python Virtual Environment using package 'uv' at '$(VENV_FOLDER)'" + @uv venv -p $(PRIMARY_PYTHON) --seed $(VENV_FOLDER) +else + @echo "Setup Python Virtual Environment using module 'venv' at '$(VENV_FOLDER)'" + @$(PRIMARY_PYTHON) -m venv $(VENV_FOLDER) + @$(MXENV_PYTHON) -m ensurepip -U +endif +endif +else + @echo "Using system Python interpreter" +endif +ifeq ("$(PYTHON_PACKAGE_INSTALLER)$(MXENV_UV_GLOBAL)","uvfalse") + @echo "Install uv" + @$(MXENV_PYTHON) -m pip install uv +endif + @$(PYTHON_PACKAGE_COMMAND) install -U pip setuptools wheel + @echo "Install/Update MXStack Python packages" + @$(PYTHON_PACKAGE_COMMAND) install -U $(MXDEV) $(MXMAKE) + @touch $(MXENV_TARGET) + +.PHONY: mxenv +mxenv: $(MXENV_TARGET) + +.PHONY: mxenv-dirty +mxenv-dirty: + @rm -f $(MXENV_TARGET) + +.PHONY: mxenv-clean +mxenv-clean: mxenv-dirty +ifeq ("$(VENV_ENABLED)", "true") +ifeq ("$(VENV_CREATE)", "true") + @rm -rf $(VENV_FOLDER) +endif +else + @$(PYTHON_PACKAGE_COMMAND) uninstall -y $(MXDEV) + @$(PYTHON_PACKAGE_COMMAND) uninstall -y $(MXMAKE) +endif + +INSTALL_TARGETS+=mxenv +DIRTY_TARGETS+=mxenv-dirty +CLEAN_TARGETS+=mxenv-clean + +############################################################################## +# mxfiles +############################################################################## + +# case `core.sources` domain not included +SOURCES_TARGET?= + +# File generation target +MXMAKE_FILES?=$(MXMAKE_FOLDER)/files + +# set environment variables for mxmake +define set_mxfiles_env + @export MXMAKE_FILES=$(1) +endef + +# unset environment variables for mxmake +define unset_mxfiles_env + @unset MXMAKE_FILES +endef + +$(PROJECT_CONFIG): +ifneq ("$(wildcard $(PROJECT_CONFIG))","") + @touch $(PROJECT_CONFIG) +else + @echo "[settings]" > $(PROJECT_CONFIG) +endif + +LOCAL_PACKAGE_FILES:=$(wildcard pyproject.toml setup.cfg setup.py requirements.txt constraints.txt) + +FILES_TARGET:=requirements-mxdev.txt +$(FILES_TARGET): $(PROJECT_CONFIG) $(MXENV_TARGET) $(SOURCES_TARGET) $(LOCAL_PACKAGE_FILES) + @echo "Create project files" + @mkdir -p $(MXMAKE_FILES) + $(call set_mxfiles_env,$(MXMAKE_FILES)) + @mxdev -n -c $(PROJECT_CONFIG) + $(call unset_mxfiles_env) + @test -e $(MXMAKE_FILES)/pip.conf && cp $(MXMAKE_FILES)/pip.conf $(VENV_FOLDER)/pip.conf || : + @touch $(FILES_TARGET) + +.PHONY: mxfiles +mxfiles: $(FILES_TARGET) + +.PHONY: mxfiles-dirty +mxfiles-dirty: + @touch $(PROJECT_CONFIG) + +.PHONY: mxfiles-clean +mxfiles-clean: mxfiles-dirty + @rm -rf constraints-mxdev.txt requirements-mxdev.txt $(MXMAKE_FILES) + +INSTALL_TARGETS+=mxfiles +DIRTY_TARGETS+=mxfiles-dirty +CLEAN_TARGETS+=mxfiles-clean + +############################################################################## +# packages +############################################################################## + +# additional sources targets which requires package re-install on change +-include $(MXMAKE_FILES)/additional_sources_targets.mk +ADDITIONAL_SOURCES_TARGETS?= + +INSTALLED_PACKAGES=$(MXMAKE_FILES)/installed.txt + +ifeq ("$(PACKAGES_ALLOW_PRERELEASES)","true") +ifeq ("$(PYTHON_PACKAGE_INSTALLER)","uv") +PACKAGES_PRERELEASES=--prerelease=allow +else +PACKAGES_PRERELEASES=--pre +endif +else +PACKAGES_PRERELEASES= +endif + +PACKAGES_TARGET:=$(INSTALLED_PACKAGES) +$(PACKAGES_TARGET): $(FILES_TARGET) $(ADDITIONAL_SOURCES_TARGETS) + @echo "Install python packages" + @$(PYTHON_PACKAGE_COMMAND) install $(PACKAGES_PRERELEASES) -r $(FILES_TARGET) + @$(PYTHON_PACKAGE_COMMAND) freeze > $(INSTALLED_PACKAGES) + @touch $(PACKAGES_TARGET) + +.PHONY: packages +packages: $(PACKAGES_TARGET) + +.PHONY: packages-dirty +packages-dirty: + @rm -f $(PACKAGES_TARGET) + +.PHONY: packages-clean +packages-clean: + @test -e $(FILES_TARGET) \ + && test -e $(MXENV_PYTHON) \ + && $(MXENV_PYTHON) -m pip uninstall -y -r $(FILES_TARGET) \ + || : + @rm -f $(PACKAGES_TARGET) + +INSTALL_TARGETS+=packages +DIRTY_TARGETS+=packages-dirty +CLEAN_TARGETS+=packages-clean + +############################################################################## +# test +############################################################################## + +TEST_TARGET:=$(SENTINEL_FOLDER)/test.sentinel +$(TEST_TARGET): $(MXENV_TARGET) + @echo "Install $(TEST_REQUIREMENTS)" + @$(PYTHON_PACKAGE_COMMAND) install $(TEST_REQUIREMENTS) + @touch $(TEST_TARGET) + +.PHONY: test +test: $(FILES_TARGET) $(SOURCES_TARGET) $(PACKAGES_TARGET) $(TEST_TARGET) $(TEST_DEPENDENCY_TARGETS) + @test -z "$(TEST_COMMAND)" && echo "No test command defined" && exit 1 || : + @echo "Run tests using $(TEST_COMMAND)" + @/usr/bin/env bash -c "$(TEST_COMMAND)" + +.PHONY: test-dirty +test-dirty: + @rm -f $(TEST_TARGET) + +.PHONY: test-clean +test-clean: test-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y $(TEST_REQUIREMENTS) || : + @rm -rf .pytest_cache + +INSTALL_TARGETS+=$(TEST_TARGET) +CLEAN_TARGETS+=test-clean +DIRTY_TARGETS+=test-dirty + +############################################################################## +# coverage +############################################################################## + +COVERAGE_TARGET:=$(SENTINEL_FOLDER)/coverage.sentinel +$(COVERAGE_TARGET): $(TEST_TARGET) + @echo "Install Coverage" + @$(PYTHON_PACKAGE_COMMAND) install -U coverage + @touch $(COVERAGE_TARGET) + +.PHONY: coverage +coverage: $(FILES_TARGET) $(SOURCES_TARGET) $(PACKAGES_TARGET) $(COVERAGE_TARGET) + @test -z "$(COVERAGE_COMMAND)" && echo "No coverage command defined" && exit 1 || : + @echo "Run coverage using $(COVERAGE_COMMAND)" + @/usr/bin/env bash -c "$(COVERAGE_COMMAND)" + +.PHONY: coverage-dirty +coverage-dirty: + @rm -f $(COVERAGE_TARGET) + +.PHONY: coverage-clean +coverage-clean: coverage-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y coverage || : + @rm -rf .coverage htmlcov + +INSTALL_TARGETS+=$(COVERAGE_TARGET) +DIRTY_TARGETS+=coverage-dirty +CLEAN_TARGETS+=coverage-clean + +############################################################################## +# Custom includes +############################################################################## + +-include $(INCLUDE_MAKEFILE) + +############################################################################## +# Default targets +############################################################################## + +INSTALL_TARGET:=$(SENTINEL_FOLDER)/install.sentinel +$(INSTALL_TARGET): $(INSTALL_TARGETS) + @touch $(INSTALL_TARGET) + +.PHONY: install +install: $(INSTALL_TARGET) + @touch $(INSTALL_TARGET) + +.PHONY: run +run: $(RUN_TARGET) + +.PHONY: deploy +deploy: $(DEPLOY_TARGETS) + +.PHONY: dirty +dirty: $(DIRTY_TARGETS) + @rm -f $(INSTALL_TARGET) + +.PHONY: clean +clean: dirty $(CLEAN_TARGETS) + @rm -rf $(CLEAN_TARGETS) $(MXMAKE_FOLDER) $(CLEAN_FS) + +.PHONY: purge +purge: clean $(PURGE_TARGETS) + +.PHONY: runtime-clean +runtime-clean: + @echo "Remove runtime artifacts, like byte-code and caches." + @find . -name '*.py[c|o]' -delete + @find . -name '*~' -exec rm -f {} + + @find . -name '__pycache__' -exec rm -fr {} + + +.PHONY: check +check: $(CHECK_TARGETS) + +.PHONY: typecheck +typecheck: $(TYPECHECK_TARGETS) + +.PHONY: format +format: $(FORMAT_TARGETS) diff --git a/README.rst b/README.rst index b3b8a6e..2d892ee 100644 --- a/README.rst +++ b/README.rst @@ -20,3 +20,5 @@ Contributors - Robert Niederreiter - Georg Bernhard + +- Lena Daxenbichler diff --git a/bootstrap.py b/bootstrap.py deleted file mode 100644 index f78186f..0000000 --- a/bootstrap.py +++ /dev/null @@ -1,170 +0,0 @@ -############################################################################## -# -# Copyright (c) 2006 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -"""Bootstrap a buildout-based project - -Simply run this script in a directory containing a buildout.cfg. -The script accepts buildout command-line options, so you can -use the -c option to specify an alternate configuration file. -""" - -import os -import shutil -import sys -import tempfile - -from optparse import OptionParser - -tmpeggs = tempfile.mkdtemp() - -usage = '''\ -[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options] - -Bootstraps a buildout-based project. - -Simply run this script in a directory containing a buildout.cfg, using the -Python that you want bin/buildout to use. - -Note that by using --find-links to point to local resources, you can keep -this script from going over the network. -''' - -parser = OptionParser(usage=usage) -parser.add_option("-v", "--version", help="use a specific zc.buildout version") - -parser.add_option("-t", "--accept-buildout-test-releases", - dest='accept_buildout_test_releases', - action="store_true", default=False, - help=("Normally, if you do not specify a --version, the " - "bootstrap script and buildout gets the newest " - "*final* versions of zc.buildout and its recipes and " - "extensions for you. If you use this flag, " - "bootstrap and buildout will get the newest releases " - "even if they are alphas or betas.")) -parser.add_option("-c", "--config-file", - help=("Specify the path to the buildout configuration " - "file to be used.")) -parser.add_option("-f", "--find-links", - help=("Specify a URL to search for buildout releases")) - - -options, args = parser.parse_args() - -###################################################################### -# load/install setuptools - -to_reload = False -try: - import pkg_resources - import setuptools -except ImportError: - ez = {} - - try: - from urllib.request import urlopen - except ImportError: - from urllib2 import urlopen - - # XXX use a more permanent ez_setup.py URL when available. - exec(urlopen('https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py' - ).read(), ez) - setup_args = dict(to_dir=tmpeggs, download_delay=0) - ez['use_setuptools'](**setup_args) - - if to_reload: - reload(pkg_resources) - import pkg_resources - # This does not (always?) update the default working set. We will - # do it. - for path in sys.path: - if path not in pkg_resources.working_set.entries: - pkg_resources.working_set.add_entry(path) - -###################################################################### -# Install buildout - -ws = pkg_resources.working_set - -cmd = [sys.executable, '-c', - 'from setuptools.command.easy_install import main; main()', - '-mZqNxd', tmpeggs] - -find_links = os.environ.get( - 'bootstrap-testing-find-links', - options.find_links or - ('http://downloads.buildout.org/' - if options.accept_buildout_test_releases else None) - ) -if find_links: - cmd.extend(['-f', find_links]) - -setuptools_path = ws.find( - pkg_resources.Requirement.parse('setuptools')).location - -requirement = 'zc.buildout' -version = options.version -if version is None and not options.accept_buildout_test_releases: - # Figure out the most recent final version of zc.buildout. - import setuptools.package_index - _final_parts = '*final-', '*final' - - def _final_version(parsed_version): - for part in parsed_version: - if (part[:1] == '*') and (part not in _final_parts): - return False - return True - index = setuptools.package_index.PackageIndex( - search_path=[setuptools_path]) - if find_links: - index.add_find_links((find_links,)) - req = pkg_resources.Requirement.parse(requirement) - if index.obtain(req) is not None: - best = [] - bestv = None - for dist in index[req.project_name]: - distv = dist.parsed_version - if _final_version(distv): - if bestv is None or distv > bestv: - best = [dist] - bestv = distv - elif distv == bestv: - best.append(dist) - if best: - best.sort() - version = best[-1].version -if version: - requirement = '=='.join((requirement, version)) -cmd.append(requirement) - -import subprocess -if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0: - raise Exception( - "Failed to execute command:\n%s", - repr(cmd)[1:-1]) - -###################################################################### -# Import and run buildout - -ws.add_entry(tmpeggs) -ws.require(requirement) -import zc.buildout.buildout - -if not [a for a in args if '=' not in a]: - args.append('bootstrap') - -# if -c was provided, we push it back into args for buildout' main function -if options.config_file is not None: - args[0:0] = ['-c', options.config_file] - -zc.buildout.buildout.main(args) -shutil.rmtree(tmpeggs) diff --git a/buildout.cfg b/buildout.cfg deleted file mode 100644 index 3499b44..0000000 --- a/buildout.cfg +++ /dev/null @@ -1,39 +0,0 @@ -[buildout] -parts = pil test coverage py -develop = . - -extensions = mr.developer -sources-dir = ${buildout:directory}/devsrc -always-checkout = force -auto-checkout = * - -[remotes] -cs = git://github.com/conestack -cs_push = git@github.com:conestack - -[sources] -yafowil = git ${remotes:cs}/yafowil.git pushurl=${remotes:cs_push}/yafowil.git - -[pil] -recipe = zc.recipe.egg:custom -egg = Pillow -# Ubuntu development -library-dirs = /usr/lib/x86_64-linux-gnu - -[test] -recipe = zc.recipe.testrunner -eggs = - ${pil:egg} - yafowil[test] - yafowil.widget.image[test] -defaults = ['--auto-color', '--auto-progress'] - -[coverage] -recipe = zc.recipe.testrunner -eggs = ${test:eggs} -defaults = ['--coverage', '../../coverage', '-v', '--auto-progress'] - -[py] -recipe = zc.recipe.egg -eggs = ${test:eggs} -interpreter = py diff --git a/i18n.sh b/i18n.sh deleted file mode 100755 index c814ec1..0000000 --- a/i18n.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash -# Author: Robert Niederreiter -# License: no license -# Date: 2014-07-20 -# -# Requirements: -# - python lingua (sudo pip install lingua) -# - gettext -# -# Usage: -# Initial catalog creation (lang is the language identifier): -# ./i18n.sh lang -# Updating translation and compile catalog: -# ./i18n.sh - -# configuration -DOMAIN="yafowil.widget.image" -SEACH_PATH=src/yafowil/widget/image -LOCALES_PATH=src/yafowil/widget/image/locales -# end configuration - -# create locales folder if not exists -if [ ! -d "$LOCALES_PATH" ]; then - echo "Locales directory not exists, create" - mkdir -p $LOCALES_PATH -fi - -# create pot if not exists -if [ ! -f $LOCALES_PATH/$DOMAIN.pot ]; then - echo "Create pot file" - touch $LOCALES_PATH/$DOMAIN.pot -fi - -# no arguments, extract and update -if [ $# -eq 0 ]; then - echo "Extract messages" - pot-create $SEACH_PATH -o $LOCALES_PATH/$DOMAIN.pot - - echo "Update translations" - for po in $LOCALES_PATH/*/LC_MESSAGES/$DOMAIN.po; do - msgmerge -o $po $po $LOCALES_PATH/$DOMAIN.pot - done - - echo "Compile message catalogs" - for po in $LOCALES_PATH/*/LC_MESSAGES/*.po; do - msgfmt -o ${po%.*}.mo $po - done - -# first argument represents language identifier, create catalog -else - cd $LOCALES_PATH - mkdir -p $1/LC_MESSAGES - msginit -i $DOMAIN.pot -o $1/LC_MESSAGES/$DOMAIN.po -l $1 -fi diff --git a/js/rollup.conf.js b/js/rollup.conf.js new file mode 100644 index 0000000..f61b2ac --- /dev/null +++ b/js/rollup.conf.js @@ -0,0 +1,65 @@ +import cleanup from 'rollup-plugin-cleanup'; +import postcss from 'rollup-plugin-postcss'; +import terser from '@rollup/plugin-terser'; + +const out_dir = 'src/yafowil/widget/image/resources'; + +const outro = ` +window.yafowil = window.yafowil || {}; +window.yafowil.image = exports; +`; + +export default args => { + let conf = { + input: 'js/src/bundle.js', + plugins: [ + cleanup() + ], + output: [{ + name: 'yafowil_image', + file: `${out_dir}/widget.js`, + format: 'iife', + outro: outro, + globals: { + jquery: 'jQuery' + }, + interop: 'default' + }], + external: [ + 'jquery' + ] + }; + if (args.configDebug !== true) { + conf.output.push({ + name: 'yafowil_image', + file: `${out_dir}/widget.min.js`, + format: 'iife', + plugins: [ + terser() + ], + outro: outro, + globals: { + jquery: 'jQuery' + }, + interop: 'default' + }); + } + let scss = { + input: ['scss/styles.scss'], + output: [{ + file: `${out_dir}/widget.css`, + format: 'es', + plugins: [terser()], + }], + plugins: [ + postcss({ + extract: true, + minimize: true, + use: [ + ['sass', { outputStyle: 'compressed' }], + ], + }), + ], + }; + return [conf, scss]; +}; diff --git a/js/src/bundle.js b/js/src/bundle.js new file mode 100644 index 0000000..efcc51c --- /dev/null +++ b/js/src/bundle.js @@ -0,0 +1,15 @@ +import $ from 'jquery'; + +import {ImageWidget} from './widget.js'; + +export * from './widget.js'; + +$(function() { + if (window.ts !== undefined) { + ts.ajax.register(ImageWidget.initialize, true); + } else if (window.bdajax !== undefined) { + bdajax.register(ImageWidget.initialize, true); + } else { + ImageWidget.initialize(); + } +}); diff --git a/js/src/widget.js b/js/src/widget.js new file mode 100644 index 0000000..6fdb8fe --- /dev/null +++ b/js/src/widget.js @@ -0,0 +1,30 @@ +import $ from 'jquery'; + +export class ImageWidget { + + static initialize(context) { + $('input.image', context).each(function (event) { + new ImageWidget($(this)); + }); + } + + constructor(elem) { + elem.data('yafowil-image', this); + this.elem = elem; + // XXX: file needs anyway, provide in yafowil directly? + $('input.file').on('change', function (evt) { + let elem = $(this); + if (elem.attr('type') === 'radio') { + return true; + } + $('input.file[value="replace"]').trigger('click'); + }); + $('input.image').on('change', function (evt) { + let elem = $(this); + if (elem.attr('type') === 'radio') { + return true; + } + $('input.image[value="replace"]').trigger('click'); + }); + } +} diff --git a/js/tests/test_image.js b/js/tests/test_image.js new file mode 100644 index 0000000..ceeaa64 --- /dev/null +++ b/js/tests/test_image.js @@ -0,0 +1,10 @@ +import {ImageWidget} from '../src/widget.js'; +import $ from 'jquery'; + +QUnit.test('initialize', assert => { + let el = $('').addClass('image').appendTo('body'); + ImageWidget.initialize(); + let wid = el.data('yafowil-image'); + + assert.ok(wid); +}); \ No newline at end of file diff --git a/js/wtr.config.mjs b/js/wtr.config.mjs new file mode 100644 index 0000000..f333d83 --- /dev/null +++ b/js/wtr.config.mjs @@ -0,0 +1,25 @@ +import {importMapsPlugin} from '@web/dev-server-import-maps'; + +export default { + nodeResolve: true, + testFramework: { + path: './node_modules/web-test-runner-qunit/dist/autorun.js', + config: { + noglobals: false + } + }, + files: [ + 'js/tests/**/test_*.js' + ], + plugins: [ + importMapsPlugin({ + inject: { + importMap: { + imports: { + 'jquery': './node_modules/jquery/dist-module/jquery.module.js' + }, + }, + }, + }), + ], +} diff --git a/mx.ini b/mx.ini new file mode 100644 index 0000000..098ead2 --- /dev/null +++ b/mx.ini @@ -0,0 +1,2 @@ +[settings] +main-package = -e .[test] diff --git a/mxmake.yaml b/mxmake.yaml new file mode 100644 index 0000000..a95d561 --- /dev/null +++ b/mxmake.yaml @@ -0,0 +1,30 @@ +topics: + core: + mxenv: + VENV_FOLDER: venv + PYTHON_PACKAGE_INSTALLER: uv + + qa: + test: + TEST_COMMAND: $(VENV_FOLDER)/bin/pytest src/yafowil/widget/image/tests + + coverage: + COVERAGE_COMMAND: | + \ + $(VENV_FOLDER)/bin/coverage run \ + --omit src/yafowil/widget/image/example.py \ + --source src/yafowil/widget/image \ + -m pytest src/yafowil/widget/image/tests \ + && $(VENV_FOLDER)/bin/coverage report --fail-under=99 + + js: + nodejs: + NODEJS_PACKAGE_MANAGER: pnpm + + rollup: + ROLLUP_CONFIG: js/rollup.conf.js + + wtr: + WTR_CONFIG: js/wtr.config.mjs + +mx-ini: true diff --git a/package.json b/package.json new file mode 100644 index 0000000..62314a1 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "devDependencies": { + "@rollup/plugin-terser": "^0.4.4", + "@web/dev-server-import-maps": "^0.2.1", + "@web/test-runner": "^0.18.3", + "@web/test-runner-core": "^0.13.2", + "install": "^0.13.0", + "jquery": "^4.0.0-beta", + "qunit": "^2.20.1", + "rollup": "^2.79.2", + "rollup-plugin-cleanup": "^3.2.1", + "rollup-plugin-postcss": "^4.0.2", + "sass": "^1.80.4", + "web-test-runner-qunit": "^2.0.0" + }, + "packageManager": "pnpm@9.3.0+sha512.ee7b93e0c2bd11409c6424f92b866f31d3ea1bef5fbe47d3c7500cdc3c9668833d2e55681ad66df5b640c61fa9dc25d546efa54d76d7f8bf54b13614ac293631" +} diff --git a/scss/styles.scss b/scss/styles.scss new file mode 100644 index 0000000..9412ce7 --- /dev/null +++ b/scss/styles.scss @@ -0,0 +1,7 @@ +input.image { + margin-top:6px; +} +form img.image-preview { + display:block; + clear:both; +} \ No newline at end of file diff --git a/setup.py b/setup.py index 830fa47..ab646a8 100644 --- a/setup.py +++ b/setup.py @@ -8,14 +8,13 @@ def read_file(name): return f.read() -version = '1.6.dev0' +version = '2.0a3.dev0' shortdesc = 'Image Widget for YAFOWIL' longdesc = '\n\n'.join([read_file(name) for name in [ 'README.rst', 'CHANGES.rst', 'LICENSE.rst' ]]) -tests_require = ['yafowil[test]'] setup( @@ -45,14 +44,14 @@ def read_file(name): include_package_data=True, zip_safe=False, install_requires=[ + 'Pillow', 'setuptools', - 'yafowil>2.1.99', + 'yafowil>2.1.99' ], - tests_require=tests_require, - extras_require=dict( - test=tests_require, - ), - test_suite="yafowil.widget.image.tests", + extras_require=dict(test=[ + 'lxml', + 'pytest' + ]), entry_points=""" [yafowil.plugin] register = yafowil.widget.image:register diff --git a/src/yafowil/widget/image/__init__.py b/src/yafowil/widget/image/__init__.py index 1ce4ad3..6b76f54 100644 --- a/src/yafowil/widget/image/__init__.py +++ b/src/yafowil/widget/image/__init__.py @@ -1,9 +1,36 @@ from yafowil.base import factory from yafowil.utils import entry_point import os +import webresource as wr -resourcedir = os.path.join(os.path.dirname(__file__), 'resources') +resources_dir = os.path.join(os.path.dirname(__file__), 'resources') + + +############################################################################## +# Default +############################################################################## + +# webresource ################################################################ + +resources = wr.ResourceGroup( + name='yafowil.widget.image', + directory=resources_dir, + path='yafowil-image' +) +resources.add(wr.ScriptResource( + name='yafowil-image-js', + depends='jquery-js', + resource='widget.js', + compressed='widget.min.js' +)) +resources.add(wr.StyleResource( + name='yafowil-image-css', + resource='widget.css' +)) + +# B/C resources ############################################################## + js = [{ 'group': 'yafowil.widget.image.common', 'resource': 'widget.js', @@ -16,8 +43,22 @@ }] +############################################################################## +# Registration +############################################################################## + @entry_point(order=10) def register(): from yafowil.widget.image import widget # noqa - factory.register_theme('default', 'yafowil.widget.image', - resourcedir, js=js, css=css) + + widget_name = 'yafowil.widget.image' + + # Default + factory.register_theme( + 'default', + widget_name, + resources_dir, + js=js, + css=css + ) + factory.register_resources('default', widget_name, resources) diff --git a/src/yafowil/widget/image/resources/widget.css b/src/yafowil/widget/image/resources/widget.css index 9412ce7..995e848 100644 --- a/src/yafowil/widget/image/resources/widget.css +++ b/src/yafowil/widget/image/resources/widget.css @@ -1,7 +1 @@ -input.image { - margin-top:6px; -} -form img.image-preview { - display:block; - clear:both; -} \ No newline at end of file +input.image{margin-top:6px}form img.image-preview{clear:both;display:block} \ No newline at end of file diff --git a/src/yafowil/widget/image/resources/widget.js b/src/yafowil/widget/image/resources/widget.js index 2c37067..c497a27 100644 --- a/src/yafowil/widget/image/resources/widget.js +++ b/src/yafowil/widget/image/resources/widget.js @@ -1,45 +1,51 @@ -/* - * yafowil image widget - */ - -if (typeof(window['yafowil']) == "undefined") yafowil = {}; - -(function($) { - - $(document).ready(function() { - // initial binding - yafowil.image.binder(); - - // add after ajax binding if bdajax present - if (typeof(window['bdajax']) != "undefined") { - $.extend(bdajax.binders, { - imagewidget_binder: yafowil.image.binder +var yafowil_image = (function (exports, $) { + 'use strict'; + + class ImageWidget { + static initialize(context) { + $('input.image', context).each(function (event) { + new ImageWidget($(this)); }); } - }); - - $.extend(yafowil, { - - image: { - - binder: function(context) { - // XXX: file needs anyway, provide in yafowil directly? - $('input.file').bind('change', function(evt) { - var elem = $(this); - if (elem.attr('type') == 'radio') { - return true; - } - $('input.file[value="replace"]').trigger('click'); - }); - $('input.image').bind('change', function(evt) { - var elem = $(this); - if (elem.attr('type') == 'radio') { - return true; - } - $('input.image[value="replace"]').trigger('click'); - }); - } + constructor(elem) { + elem.data('yafowil-image', this); + this.elem = elem; + $('input.file').on('change', function (evt) { + let elem = $(this); + if (elem.attr('type') === 'radio') { + return true; + } + $('input.file[value="replace"]').trigger('click'); + }); + $('input.image').on('change', function (evt) { + let elem = $(this); + if (elem.attr('type') === 'radio') { + return true; + } + $('input.image[value="replace"]').trigger('click'); + }); + } + } + + $(function() { + if (window.ts !== undefined) { + ts.ajax.register(ImageWidget.initialize, true); + } else if (window.bdajax !== undefined) { + bdajax.register(ImageWidget.initialize, true); + } else { + ImageWidget.initialize(); } }); -})(jQuery); \ No newline at end of file + exports.ImageWidget = ImageWidget; + + Object.defineProperty(exports, '__esModule', { value: true }); + + + window.yafowil = window.yafowil || {}; + window.yafowil.image = exports; + + + return exports; + +})({}, jQuery); diff --git a/src/yafowil/widget/image/resources/widget.min.js b/src/yafowil/widget/image/resources/widget.min.js new file mode 100644 index 0000000..4fd6666 --- /dev/null +++ b/src/yafowil/widget/image/resources/widget.min.js @@ -0,0 +1 @@ +var yafowil_image=function(i,e){"use strict";class t{static initialize(i){e("input.image",i).each((function(i){new t(e(this))}))}constructor(i){i.data("yafowil-image",this),this.elem=i,e("input.file").on("change",(function(i){if("radio"===e(this).attr("type"))return!0;e('input.file[value="replace"]').trigger("click")})),e("input.image").on("change",(function(i){if("radio"===e(this).attr("type"))return!0;e('input.image[value="replace"]').trigger("click")}))}}return e((function(){void 0!==window.ts?ts.ajax.register(t.initialize,!0):void 0!==window.bdajax?bdajax.register(t.initialize,!0):t.initialize()})),i.ImageWidget=t,Object.defineProperty(i,"__esModule",{value:!0}),window.yafowil=window.yafowil||{},window.yafowil.image=i,i}({},jQuery); diff --git a/src/yafowil/widget/image/tests/__init__.py b/src/yafowil/widget/image/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/yafowil/widget/image/tests.py b/src/yafowil/widget/image/tests/test_widget.py similarity index 94% rename from src/yafowil/widget/image/tests.py rename to src/yafowil/widget/image/tests/test_widget.py index d29cfcd..cd05d9a 100644 --- a/src/yafowil/widget/image/tests.py +++ b/src/yafowil/widget/image/tests/test_widget.py @@ -8,10 +8,10 @@ from yafowil.widget.image.utils import aspect_ratio_approximate from yafowil.widget.image.utils import same_aspect_ratio from yafowil.widget.image.utils import scale_size +import os import PIL import pkg_resources import unittest -import yafowil.loader # noqa if IS_PY2: @@ -21,6 +21,10 @@ from io import BytesIO as StringIO +def np(path): + return path.replace('/', os.path.sep) + + class TestUtils(unittest.TestCase): def test_aspect_ratio_approximate(self): @@ -46,8 +50,10 @@ class TestImageWidget(YafowilTestCase): def setUp(self): super(TestImageWidget, self).setUp() + from yafowil.widget import image from yafowil.widget.image import widget reload(widget) + image.register() def dummy_file_data(self, filename): path = pkg_resources.resource_filename( @@ -89,7 +95,7 @@ def test_render_empty(self): 'action': 'myaction' }) form['image'] = factory('image') - self.check_output(""" + self.checkOutput("""
Alternative text Alternative text Alternative text Alternative text """, fxml(form())) @@ -282,7 +288,7 @@ def test_extract_new(self): }) self.assertEqual(data.errors, []) self.assertEqual(data.value, UNSET) - self.check_output(""" + self.checkOutput(""" [('action', 'new'), ('file', <... at ...>), ('image', ), @@ -306,13 +312,13 @@ def test_extract_keep(self): 'image-action': 'keep' }) self.assertEqual(data.errors, []) - self.check_output(""" + self.checkOutput(""" [('action', 'keep'), ('file', <... at ...>), ('image', ), ('mimetype', 'image/png')] """, str(sorted(data.value.items()))) - self.check_output(""" + self.checkOutput(""" [('action', 'keep'), ('file', <... at ...>), ('image', ), @@ -339,12 +345,12 @@ def test_extract_replace(self): 'image-action': 'replace' }) self.assertEqual(data.errors, []) - self.check_output(""" + self.checkOutput(""" [('action', 'replace'), ('file', <... at ...>), ('mimetype', 'image/png')] """, str(sorted(data.value.items()))) - self.check_output(""" + self.checkOutput(""" [('action', 'replace'), ('file', <... at ...>), ('image', ), @@ -371,12 +377,12 @@ def test_extract_delete(self): 'image-action': 'delete' }) self.assertEqual(data.errors, []) - self.check_output(""" + self.checkOutput(""" [('action', 'delete'), ('file', ), ('mimetype', 'image/png')] """, str(sorted(data.value.items()))) - self.check_output(""" + self.checkOutput(""" [('action', 'delete'), ('file', ), ('mimetype', 'image/png')] @@ -396,7 +402,7 @@ def test_extract_mimetype_empty(self): 'mimetype': 'image/jpg' } }) - self.check_output(""" + self.checkOutput(""" [('action', 'new'), ('file', <... at ...>), ('image', ), ('image', ), @@ -484,7 +487,7 @@ def test_extract_mimtype_not_image(self): def test_extract_from_image_foundations(self): buffer = StringIO(self.dummy_png) image = PIL.Image.open(buffer) - self.check_output(""" + self.checkOutput(""" """, str(image)) buffer.seek(0) @@ -688,13 +691,13 @@ def test_extract_scales(self): ) self.assertEqual(data.extracted['action'], 'new') self.assertEqual(data.extracted['mimetype'], 'image/png') - self.check_output(""" + self.checkOutput(""" <... at ...> """, str(data.extracted['file'])) - self.check_output(""" + self.checkOutput(""" """, str(data.extracted['image'])) - self.check_output(""" + self.checkOutput(""" [('landscape', ), ('micro', ), ('portrait', )] @@ -770,7 +773,7 @@ def test_extract_crop_as_is_without_offset_20_20(self): } } data = image.extract(request) - self.check_output(""" + self.checkOutput(""" [('action', 'new'), ('cropped', ), ('file', <... at ...>), @@ -799,7 +802,7 @@ def test_extract_crop_as_is_without_offset_40_20(self): } } data = image.extract(request) - self.check_output(""" + self.checkOutput(""" [('action', 'new'), ('cropped', ), ('file', <... at ...>), @@ -832,7 +835,7 @@ def test_extract_crop_as_is_without_offset_20_40(self): } } data = image.extract(request) - self.check_output(""" + self.checkOutput(""" [('action', 'new'), ('cropped', ), ('file', <... at ...>), @@ -866,7 +869,7 @@ def test_extract_crop_with_offset_5_3_size_20_40(self): } } data = image.extract(request) - self.check_output(""" + self.checkOutput(""" [('action', 'new'), ('cropped', ), ('file', <... at ...>), @@ -896,7 +899,7 @@ def test_crop_with_offset_5_0_size_50_20(self): } } data = image.extract(request) - self.check_output(""" + self.checkOutput(""" [('action', 'new'), ('cropped', ), ('file', <... at ...>), @@ -931,7 +934,7 @@ def test_extract_crop_fitting_from_lanscape_30_18(self): } } data = image.extract(request) - self.check_output(""" + self.checkOutput(""" [('action', 'new'), ('cropped', ), ('file', <... at ...>), @@ -966,7 +969,7 @@ def test_extract_crop_fitting_from_landscape_18_30(self): }, } data = image.extract(request) - self.check_output(""" + self.checkOutput(""" [('action', 'new'), ('cropped', ), ('file', <... at ...>), @@ -1001,7 +1004,7 @@ def test_extract_crop_fitting_from_portrait_30_18(self): } } data = image.extract(request) - self.check_output(""" + self.checkOutput(""" [('action', 'new'), ('cropped', ), ('file', <... at ...>), @@ -1036,7 +1039,7 @@ def test_extract_crop_fitting_from_portrait_18_30(self): } } data = image.extract(request) - self.check_output(""" + self.checkOutput(""" [('action', 'new'), ('cropped', ), ('file', <... at ...>), @@ -1066,7 +1069,7 @@ def test_extract_crop_fitting_square(self): } } data = image.extract(request) - self.check_output(""" + self.checkOutput(""" [('action', 'new'), ('cropped', ), ('file', <... at ...>), @@ -1096,7 +1099,7 @@ def test_extract_crop_fitting_portrait_from_square(self): } } data = image.extract(request) - self.check_output(""" + self.checkOutput(""" [('action', 'new'), ('cropped', ), ('file', <... at ...>), @@ -1126,7 +1129,7 @@ def test_extract_crop_fitting_lanscape_from_square(self): } } data = image.extract(request) - self.check_output(""" + self.checkOutput(""" [('action', 'new'), ('cropped', ), ('file', <... at ...>), @@ -1149,6 +1152,29 @@ def test_saving_image_data(self): self.assertTrue(data.startswith(b'\x89PNG\r\n')) self.assertTrue(data.endswith(b'\x00IEND\xaeB`\x82')) + def test_resources(self): + factory.theme = 'default' + resources = factory.get_resources('yafowil.widget.image') + self.assertTrue(resources.directory.endswith(np('/image/resources'))) + self.assertEqual(resources.name, 'yafowil.widget.image') + self.assertEqual(resources.path, 'yafowil-image') + + scripts = resources.scripts + self.assertEqual(len(scripts), 1) + + self.assertTrue(scripts[0].directory.endswith(np('/image/resources'))) + self.assertEqual(scripts[0].path, 'yafowil-image') + self.assertEqual(scripts[0].file_name, 'widget.min.js') + self.assertTrue(os.path.exists(scripts[0].file_path)) + + styles = resources.styles + self.assertEqual(len(styles), 1) + + self.assertTrue(styles[0].directory.endswith(np('/image/resources'))) + self.assertEqual(styles[0].path, 'yafowil-image') + self.assertEqual(styles[0].file_name, 'widget.css') + self.assertTrue(os.path.exists(styles[0].file_path)) + if __name__ == '__main__': unittest.main() diff --git a/src/yafowil/widget/image/widget.py b/src/yafowil/widget/image/widget.py index 6944535..e51f9e2 100644 --- a/src/yafowil/widget/image/widget.py +++ b/src/yafowil/widget/image/widget.py @@ -2,10 +2,10 @@ from node.utils import UNSET from yafowil.base import ExtractionError from yafowil.base import factory -from yafowil.common import file_extractor -from yafowil.common import file_options_renderer from yafowil.common import generic_required_extractor -from yafowil.common import input_file_edit_renderer +from yafowil.file import file_extractor +from yafowil.file import file_options_renderer +from yafowil.file import input_file_edit_renderer from yafowil.tsf import TSF from yafowil.utils import attr_value from yafowil.utils import css_managed_props @@ -180,7 +180,7 @@ def scales_extractor(widget, data): # scale y if image_appr < scale_appr: image_size = scale_size(image.size, (None, size[1])) - scaled_images[name] = image.resize(image_size, Image.ANTIALIAS) + scaled_images[name] = image.resize(image_size, Image.Resampling.LANCZOS) data.extracted['scales'] = scaled_images return data.extracted @@ -206,17 +206,17 @@ def crop_extractor(widget, data): crop_appr = aspect_ratio_approximate(size) if fitting: if same_aspect_ratio(size, image.size): - image = image.resize(size, Image.ANTIALIAS) + image = image.resize(size, Image.Resampling.LANCZOS) offset = (0, 0) # scale x if image_appr < crop_appr: image_size = scale_size(image.size, (size[0], None)) - image = image.resize(image_size, Image.ANTIALIAS) + image = image.resize(image_size, Image.Resampling.LANCZOS) offset = (0, (image_size[1] - size[1]) / 2) # scale y if image_appr > crop_appr: image_size = scale_size(image.size, (None, size[1])) - image = image.resize(image_size, Image.ANTIALIAS) + image = image.resize(image_size, Image.Resampling.LANCZOS) offset = ((image_size[0] - size[0]) / 2, 0) image = image.crop( (offset[0], offset[1], size[0] + offset[0], size[1] + offset[1]))