diff --git a/README.rst b/README.rst index c9ee10c..75ddb1e 100644 --- a/README.rst +++ b/README.rst @@ -31,11 +31,17 @@ binary extension for your Python. If it is unable to build a binary extension, it will use cffi ABI mode instead and only needs the libvips shared library. This takes longer to -start up and is typically ~20% slower in execution. You can find out how -pyvips installed with ``pip show pyvips``. +start up and is typically ~20% slower in execution. You can find out if +API mode is being used with: + +.. code-block:: python + + import pyvips + + print(pyvips.API_mode) This binding passes the vips test suite cleanly and with no leaks under -python2.7 - python3.11, pypy and pypy3 on Windows, macOS and Linux. +python3 and pypy3 on Windows, macOS and Linux. How it works ------------ @@ -246,7 +252,7 @@ Update pypi package: .. code-block:: shell - $ python3 setup.py sdist + $ python3 -m build --sdist $ twine upload --repository pyvips dist/* $ git tag -a v2.2.0 -m "as uploaded to pypi" $ git push origin v2.2.0 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c10eaed --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,89 @@ +[build-system] +requires = [ + # First version of setuptools to support pyproject.toml configuration + "setuptools>=61.0.0", + "wheel", + # Must be kept in sync with `project.dependencies` + "cffi>=1.0.0", + "pkgconfig>=1.5", +] +build-backend = "setuptools.build_meta" + +[project] +name = "pyvips" +authors = [ + {name = "John Cupitt", email = "jcupitt@gmail.com"}, +] +description = "binding for the libvips image processing library" +readme = "README.rst" +keywords = [ + "image processing", +] +license = {text = "MIT"} +requires-python = ">=3.7" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Topic :: Multimedia :: Graphics", + "Topic :: Multimedia :: Graphics :: Graphics Conversion", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dependencies = [ + # Must be kept in sync with `build-system.requires` + "cffi>=1.0.0", +] +dynamic = [ + "version", +] + +[project.urls] +changelog = "https://github.com/libvips/pyvips/blob/master/CHANGELOG.rst" +documentation = "https://libvips.github.io/pyvips/" +funding = "https://opencollective.com/libvips" +homepage = "https://github.com/libvips/pyvips" +issues = "https://github.com/libvips/pyvips/issues" +source = "https://github.com/libvips/pyvips" + +[tool.setuptools] +# We try to compile as part of install, so we can't run in a ZIP +zip-safe = false +include-package-data = false + +[tool.setuptools.dynamic] +version = {attr = "pyvips.version.__version__"} + +[tool.setuptools.packages.find] +exclude = [ + "doc*", + "examples*", + "tests*", +] + +[project.optional-dependencies] +# All the following are used for our own testing +tox = ["tox"] +test = [ + "pytest", + "pyperf", +] +sdist = ["build"] +doc = [ + "sphinx", + "sphinx_rtd_theme", +] + +[tool.pytest.ini_options] +norecursedirs = ["tests/helpers"] diff --git a/pyvips/__init__.py b/pyvips/__init__.py index 39abf8e..323992b 100644 --- a/pyvips/__init__.py +++ b/pyvips/__init__.py @@ -10,7 +10,7 @@ # user code can override this null handler logger.addHandler(logging.NullHandler()) -# pull in our module version number, see also setup.py +# pull in our module version number from .version import __version__ # try to import our binary interface ... if that works, we are in API mode diff --git a/pyvips/error.py b/pyvips/error.py index 359deb6..962a1f6 100644 --- a/pyvips/error.py +++ b/pyvips/error.py @@ -1,23 +1,12 @@ # errors from libvips -import sys import logging +from pathlib import Path from pyvips import ffi, vips_lib, glib_lib logger = logging.getLogger(__name__) -_is_PY3 = sys.version_info[0] == 3 - -if _is_PY3: - # pathlib is not part of Python 2 stdlib - from pathlib import Path - text_type = str, Path - byte_type = bytes -else: - text_type = unicode # noqa: F821 - byte_type = str - def _to_bytes(x): """Convert to a byte string. @@ -26,7 +15,7 @@ def _to_bytes(x): byte string. You must call this on strings you pass to libvips. """ - if isinstance(x, text_type): + if isinstance(x, (str, Path)): # n.b. str also converts pathlib.Path objects x = str(x).encode('utf-8') @@ -44,7 +33,7 @@ def _to_string(x): x = 'NULL' else: x = ffi.string(x) - if isinstance(x, byte_type): + if isinstance(x, bytes): x = x.decode('utf-8') return x diff --git a/pyvips/gobject.py b/pyvips/gobject.py index 3777299..706424c 100644 --- a/pyvips/gobject.py +++ b/pyvips/gobject.py @@ -1,5 +1,3 @@ -from __future__ import division - import logging import pyvips diff --git a/pyvips/gvalue.py b/pyvips/gvalue.py index 13c2a18..88f2360 100644 --- a/pyvips/gvalue.py +++ b/pyvips/gvalue.py @@ -1,9 +1,5 @@ -from __future__ import division -from __future__ import unicode_literals - import logging import numbers -import sys import pyvips from pyvips import ffi, vips_lib, gobject_lib, \ @@ -12,8 +8,6 @@ logger = logging.getLogger(__name__) -_is_PY2 = sys.version_info.major == 2 - class GValue(object): @@ -103,7 +97,7 @@ def to_enum(gtype, value): """ - if isinstance(value, basestring if _is_PY2 else str): # noqa: F821 + if isinstance(value, str): enum_value = vips_lib.vips_enum_from_nick(b'pyvips', gtype, _to_bytes(value)) if enum_value < 0: @@ -132,7 +126,7 @@ def to_flag(gtype, value): """ - if isinstance(value, basestring if _is_PY2 else str): # noqa: F821 + if isinstance(value, str): flag_value = vips_lib.vips_flags_from_nick(b'pyvips', gtype, _to_bytes(value)) if flag_value < 0: diff --git a/pyvips/pyvips_build.py b/pyvips/pyvips_build.py index 4a328f0..788b479 100644 --- a/pyvips/pyvips_build.py +++ b/pyvips/pyvips_build.py @@ -9,19 +9,7 @@ if pkgconfig.installed('vips', '< 8.2'): raise Exception('pkg-config "vips" is too old -- need libvips 8.2 or later') -# pkgconfig 1.5+ has modversion ... otherwise, use a small shim -try: - from pkgconfig import modversion -except ImportError: - def modversion(package): - # will need updating once we hit 8.20 :( - for i in range(20, 3, -1): - if pkgconfig.installed(package, '>= 8.' + str(i)): - # be careful micro version is always set to 0 - return '8.' + str(i) + '.0' - return '8.2.0' - -major, minor, micro = [int(s) for s in modversion('vips').split('.')] +major, minor, micro = [int(s) for s in pkgconfig.modversion('vips').split('.')] ffibuilder = FFI() diff --git a/pyvips/vconnection.py b/pyvips/vconnection.py index b33e093..cd4202c 100644 --- a/pyvips/vconnection.py +++ b/pyvips/vconnection.py @@ -1,5 +1,3 @@ -from __future__ import division - import logging import pyvips diff --git a/pyvips/version.py b/pyvips/version.py index 0a5ae40..391907e 100644 --- a/pyvips/version.py +++ b/pyvips/version.py @@ -1,4 +1,4 @@ -# this is execfile()d into setup.py imported into __init__.py +# this is used in pyproject.toml and imported into __init__.py __version__ = '2.2.3' __all__ = ['__version__'] diff --git a/pyvips/vimage.py b/pyvips/vimage.py index df6a178..46f1ee1 100644 --- a/pyvips/vimage.py +++ b/pyvips/vimage.py @@ -1,7 +1,5 @@ # wrap VipsImage -from __future__ import division - import numbers import struct @@ -83,26 +81,6 @@ def _run_cmplx(fn, image): return image -# https://stackoverflow.com/a/22409540/1480019 -# https://github.com/benjaminp/six/blob/33b584b2c551548021adb92a028ceaf892deb5be/six.py#L846-L861 -def _with_metaclass(metaclass): - """Class decorator for creating a class with a metaclass.""" - def wrapper(cls): - orig_vars = cls.__dict__.copy() - slots = orig_vars.get('__slots__') - if slots is not None: - if isinstance(slots, str): - slots = [slots] - for slots_var in slots: - orig_vars.pop(slots_var) - orig_vars.pop('__dict__', None) - orig_vars.pop('__weakref__', None) - if hasattr(cls, '__qualname__'): - orig_vars['__qualname__'] = cls.__qualname__ - return metaclass(cls.__name__, cls.__bases__, orig_vars) - return wrapper - - # decorator to set docstring def _add_doc(name): try: @@ -258,8 +236,7 @@ def call_function(*args, **kwargs): return call_function -@_with_metaclass(ImageType) -class Image(pyvips.VipsObject): +class Image(pyvips.VipsObject, metaclass=ImageType): """Wrap a VipsImage object. """ @@ -610,12 +587,6 @@ def new_from_memory(data, width, height, bands, format): """ format_value = GValue.to_enum(GValue.format_type, format) pointer = ffi.from_buffer(data) - # py3: - # - memoryview has .nbytes for number of bytes in object - # - len() returns number of elements in top array - # py2: - # - buffer has no nbytes member - # - but len() gives number of bytes in object nbytes = data.nbytes if hasattr(data, 'nbytes') else len(data) vi = vips_lib.vips_image_new_from_memory(pointer, nbytes, diff --git a/pyvips/vinterpolate.py b/pyvips/vinterpolate.py index 88a2080..5b0f2c7 100644 --- a/pyvips/vinterpolate.py +++ b/pyvips/vinterpolate.py @@ -1,5 +1,3 @@ -from __future__ import division - import pyvips from pyvips import ffi, vips_lib, Error, _to_bytes diff --git a/pyvips/vobject.py b/pyvips/vobject.py index 72a036c..bd5c9ff 100644 --- a/pyvips/vobject.py +++ b/pyvips/vobject.py @@ -1,7 +1,5 @@ # wrap VipsObject -from __future__ import division - import logging import pyvips diff --git a/pyvips/voperation.py b/pyvips/voperation.py index ac80511..db66b15 100644 --- a/pyvips/voperation.py +++ b/pyvips/voperation.py @@ -1,5 +1,3 @@ -from __future__ import division, print_function - import logging import pyvips diff --git a/pyvips/vregion.py b/pyvips/vregion.py index 42da7d7..7b79a94 100644 --- a/pyvips/vregion.py +++ b/pyvips/vregion.py @@ -1,7 +1,5 @@ # wrap VipsRegion -from __future__ import division - import pyvips from pyvips import ffi, glib_lib, vips_lib, Error, at_least_libvips diff --git a/pyvips/vsource.py b/pyvips/vsource.py index a3095f0..a1b7d50 100644 --- a/pyvips/vsource.py +++ b/pyvips/vsource.py @@ -1,5 +1,3 @@ -from __future__ import division - import logging import pyvips @@ -81,12 +79,6 @@ def new_from_memory(data): # logger.debug('VipsSource.new_from_memory:') - # py3: - # - memoryview has .nbytes for number of bytes in object - # - len() returns number of elements in top array - # py2: - # - buffer has no nbytes member - # - but len() gives number of bytes in object start = ffi.from_buffer(data) nbytes = data.nbytes if hasattr(data, 'nbytes') else len(data) diff --git a/pyvips/vsourcecustom.py b/pyvips/vsourcecustom.py index d154045..f73dc50 100644 --- a/pyvips/vsourcecustom.py +++ b/pyvips/vsourcecustom.py @@ -1,5 +1,3 @@ -from __future__ import division - import logging import pyvips diff --git a/pyvips/vtarget.py b/pyvips/vtarget.py index 44139d4..9eecfdf 100644 --- a/pyvips/vtarget.py +++ b/pyvips/vtarget.py @@ -1,5 +1,3 @@ -from __future__ import division - import logging import pyvips diff --git a/pyvips/vtargetcustom.py b/pyvips/vtargetcustom.py index 8a5582e..9f6b7d4 100644 --- a/pyvips/vtargetcustom.py +++ b/pyvips/vtargetcustom.py @@ -1,5 +1,3 @@ -from __future__ import division - import logging import pyvips @@ -34,13 +32,7 @@ def on_write(self, handler): """ def interface_handler(buf): - bytes_written = handler(buf) - # py2 will often return None for bytes_written ... replace with - # the length of the string - if bytes_written is None: - bytes_written = len(buf) - - return bytes_written + return handler(buf) self.signal_connect("write", interface_handler) diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 98969c5..0000000 --- a/setup.cfg +++ /dev/null @@ -1,8 +0,0 @@ -[aliases] -test=pytest - -[bdist_wheel] -universal=1 - -[tool:pytest] -norecursedirs=tests/helpers \ No newline at end of file diff --git a/setup.py b/setup.py index 9bebd31..2bebdae 100644 --- a/setup.py +++ b/setup.py @@ -1,126 +1,22 @@ -"""a binding for the libvips image processing library - -See: -https://github.com/libvips/pyvips -""" - -# flake8: noqa - import sys -from codecs import open -from os import path - -from setuptools import setup, find_packages -from distutils import log - -here = path.abspath(path.dirname(__file__)) - -info = {} -with open(path.join(here, 'pyvips', 'version.py'), encoding='utf-8') as f: - exec(f.read(), info) - -with open(path.join(here, 'README.rst'), encoding='utf-8') as f: - long_description = f.read() - -# See https://pypi.python.org/pypi?%3Aaction=list_classifiers -pyvips_classifiers = [ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - 'Topic :: Multimedia :: Graphics', - 'Topic :: Multimedia :: Graphics :: Graphics Conversion', - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', -] -setup_deps = [ - 'cffi>=1.0.0', -] - -install_deps = [ - 'cffi>=1.0.0', -] - -test_deps = [ - 'cffi>=1.0.0', - 'pytest', - 'pyperf', -] - -extras = { - 'test': test_deps, - 'doc': ['sphinx', 'sphinx_rtd_theme'], -} - -pyvips_packages = find_packages(exclude=['docs', 'tests', 'examples']) - -sys.path.append(path.join(here, 'pyvips')) - -def setup_API(): - setup( - name='pyvips', - version=info['__version__'], - description='binding for the libvips image processing library, API mode', - long_description=long_description, - url='https://github.com/libvips/pyvips', - author='John Cupitt', - author_email='jcupitt@gmail.com', - license='MIT', - classifiers=pyvips_classifiers, - keywords='image processing', - - packages=pyvips_packages, - setup_requires=setup_deps + ['pkgconfig'], - cffi_modules=['pyvips/pyvips_build.py:ffibuilder'], - install_requires=install_deps + ['pkgconfig'], - tests_require=test_deps, - extras_require=extras, - - # we will try to compile as part of install, so we can't run in a zip - zip_safe=False, - ) +from os import path +from setuptools import setup -def setup_ABI(): - setup( - name='pyvips', - version=info['__version__'], - description='binding for the libvips image processing library, ABI mode', - long_description=long_description, - url='https://github.com/libvips/pyvips', - author='John Cupitt', - author_email='jcupitt@gmail.com', - license='MIT', - classifiers=pyvips_classifiers, - keywords='image processing', +base_dir = path.dirname(__file__) +src_dir = path.join(base_dir, 'pyvips') - packages=pyvips_packages, - setup_requires=setup_deps, - install_requires=install_deps, - tests_require=test_deps, - extras_require=extras, - ) +# When executing the setup.py, we need to be able to import ourselves, this +# means that we need to add the pyvips/ directory to the sys.path. +sys.path.insert(0, src_dir) -# try to install in API mode first, then if that fails, fall back to ABI +# Try to install in API mode first, then if that fails, fall back to ABI # API mode requires a working C compiler plus all the libvips headers whereas # ABI only needs the libvips shared library to be on the system try: - setup_API() + setup(cffi_modules=['pyvips/pyvips_build.py:ffibuilder']) except Exception as e: - log.warn('Falling back to ABI mode. Details: {0}'.format(e)) - setup_ABI() + print('Falling back to ABI mode. Details: {0}'.format(e)) + setup() diff --git a/tests/helpers/helpers.py b/tests/helpers/helpers.py index 37af4bd..d7f14fd 100644 --- a/tests/helpers/helpers.py +++ b/tests/helpers/helpers.py @@ -1,7 +1,6 @@ # vim: set fileencoding=utf-8 : # test helpers import os -import sys import tempfile import pytest @@ -12,8 +11,6 @@ WEBP_FILE = os.path.join(IMAGES, "sample.webp") SVG_FILE = os.path.join(IMAGES, "logo.svg") -_is_PY3 = sys.version_info[0] == 3 - # an expanding zip ... if either of the args is a scalar or a one-element list, # duplicate it down the other side diff --git a/tests/test_progress.py b/tests/test_progress.py index d49261d..d931113 100644 --- a/tests/test_progress.py +++ b/tests/test_progress.py @@ -7,17 +7,21 @@ class TestProgress: def test_progress(self): - # py27 requires this pattern for non-local modification - notes = {} + seen_preeval = False + seen_eval = False + seen_posteval = False def preeval_cb(image, progress): - notes['seen_preeval'] = True + nonlocal seen_preeval + seen_preeval = True def eval_cb(image, progress): - notes['seen_eval'] = True + nonlocal seen_eval + seen_eval = True def posteval_cb(image, progress): - notes['seen_posteval'] = True + nonlocal seen_posteval + seen_posteval = True image = pyvips.Image.black(1, 100000) image.set_progress(True) @@ -26,9 +30,9 @@ def posteval_cb(image, progress): image.signal_connect('posteval', posteval_cb) image.avg() - assert notes['seen_preeval'] - assert notes['seen_eval'] - assert notes['seen_posteval'] + assert seen_preeval + assert seen_eval + assert seen_posteval def test_progress_fields(self): def preeval_cb(image, progress): diff --git a/tests/test_saveload.py b/tests/test_saveload.py index 248895a..d4c6c8a 100644 --- a/tests/test_saveload.py +++ b/tests/test_saveload.py @@ -3,9 +3,9 @@ import os import tempfile -import pytest import pyvips -from helpers import temp_filename, skip_if_no, _is_PY3, IMAGES, JPEG_FILE +from pathlib import Path +from helpers import temp_filename, skip_if_no, IMAGES, JPEG_FILE class TestSaveLoad: @@ -39,11 +39,6 @@ def test_load_file(self): @skip_if_no('jpegload') def test_save_file_pathlib(self): - if not _is_PY3: - pytest.skip('pathlib not in stdlib in Python 2') - - from pathlib import Path - filename = Path(temp_filename(self.tempdir, '.jpg')) im = pyvips.Image.black(10, 20) @@ -53,11 +48,6 @@ def test_save_file_pathlib(self): @skip_if_no('jpegload') def test_load_file_pathlib(self): - if not _is_PY3: - pytest.skip('pathlib not in stdlib in Python 2') - - from pathlib import Path - filename = Path(IMAGES) / 'sample.jpg' assert filename.exists()