Skip to content

Commit e855f6c

Browse files
authored
Add type hints support (#355) (#564)
* Add type hints support (#355) Adds PEP 561 type stubs for pyvips, enabling IDE autocomplete and type checking with mypy. - Add pyvips/__init__.pyi with type stubs for 300+ Image operations - Add generate_type_stubs.py script using introspection (follows existing enum/doc pattern) - Add test_type_hints.py with mypy validation - Update README with type checking documentation - Add mypy check to CI workflow Type stubs are generated to handle libvips's 300+ operations and frequent updates. Zero runtime overhead, full type coverage. * Add comprehensive type hints for hand-written bindings and enhance operator overloads - Add type hints for ifthenelse, composite, floor, ceil, rint, bandsplit, bandjoin, bandrank, hasalpha, get_page_height - Enhance operator overloads to support List[float] and List[int] operands - Move generate_type_stubs.py from pyvips/ to examples/ for better maintainability - Add CI mypy checks for examples/affine.py and examples/convolve.py - Update documentation references for type stub generation path * Update CHANGELOG credit to JoshCLWren * Add type hints for composite and ifthenelse hand-written bindings * Expand mypy type checking to more example scripts * Fix operation filtering in type stub generator to correctly filter deprecated operations Changed hardcoded value 4 (NOCACHE) to use _OPERATION_DEPRECATED constant (8). This fixes incorrect filtering that was excluding uncacheable operations instead of deprecated ones. * Clean up type stub generator: remove unused imports, extra blank line, add flake8 noqa * Complete comprehensive type hints for pyvips - Add SourceCustom class with on_read() and on_seek() methods - Add Source.new_from_descriptor() and Target.new_to_descriptor() static methods - Add pyvips.call() function for calling libvips operations - Add erode() and dilate() methods to Image class stub - Add explicit enum attributes for Direction and Align (HORIZONTAL, VERTICAL, LOW, CENTRE, HIGH) - Fix try7.py to use .format instead of non-existent .bandfmt attribute - Add type: ignore comments for Union type narrowing issues in 7 example files - All 35 example files now pass mypy type checking - Type stub file passes mypy with 0 errors * Update version to 3.2.0 and update CI - Update version.py to 3.2.0 - Update CHANGELOG.rst with version 3.2.0 entry - Update doc/conf.py to version 3.2.0 - Expand CI mypy checks to include more example scripts * Fix new_from_image type annotation to include int and List[int] support Match operator overload signatures which already support int and List[int] types. Ensures type consistency across the Image API. * Revert linting changes in examples and doc/conf.py - Revert examples/*.py to origin/master (remove type: ignore comments that caused flake8 E501 errors) - Revert doc/conf.py quote-style changes back to single quotes - Keep only version bump (3.1 -> 3.2, 3.1.1 -> 3.2.0) - Fix remaining line-length issues in doc/conf.py * Fix doc/conf.py syntax and line-length errors * pin mypy to <1.19 for PyPy 3.9 compatibility mypy 1.19+ introduced a dependency on the librt module which is not available on PyPy 3.9, causing ModuleNotFoundError during type checking. This pins mypy to version 1.18.x which works across all Python versions in the CI matrix.
1 parent ae98cd3 commit e855f6c

File tree

14 files changed

+1358
-58
lines changed

14 files changed

+1358
-58
lines changed

.github/workflows/ci.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ jobs:
3232
pip install flake8
3333
flake8 .
3434
35+
- name: Type check with mypy
36+
run: |
37+
# Pin mypy to <1.19 due to librt dependency in 1.19+
38+
# which is not available on PyPy 3.9
39+
pip install "mypy<1.19"
40+
mypy pyvips/__init__.pyi --no-error-summary
41+
mypy examples/affine.py examples/convolve.py examples/try5.py examples/watermark.py \
42+
examples/annotate-animation.py examples/join-animation.py examples/progress.py \
43+
--no-error-summary
44+
3545
- name: Install tox and any other packages
3646
run: pip install tox
3747

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## Version 3.2.0 (released TBA)
2+
3+
- add comprehensive type hints for all pyvips operations via generated stubs [JoshCLWren]
4+
- add operator overload type hints with array support (int, float, list[int], list[float]) [JoshCLWren]
5+
- add hand-written binding type hints for common methods [JoshCLWren]
6+
- add test coverage for type stubs [JoshCLWren]
7+
18
## Version 3.1.1 (released 9 December 2025)
29

310
- fix get_gainmap arguments [jcupitt]

README.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,30 @@ Stylecheck:
228228
229229
$ flake8
230230
231+
Stylecheck:
232+
233+
.. code-block:: shell
234+
235+
$ flake8
236+
237+
Type checking:
238+
239+
pyvips includes type hints via PEP 561 type stub files (``pyvips/__init__.pyi``).
240+
To enable type checking in your project, install a type checker like mypy:
241+
242+
.. code-block:: shell
243+
244+
$ pip install mypy pyvips
245+
246+
Then run mypy on your code:
247+
248+
.. code-block:: shell
249+
250+
$ mypy your_script.py
251+
252+
Note: ``pyvips`` methods accept arbitrary keyword arguments for libvips options,
253+
which may not be fully covered by type hints.
254+
231255
Generate HTML docs in ``doc/build/html``:
232256

233257
.. code-block:: shell
@@ -246,6 +270,16 @@ then
246270
247271
Then check and move `enums.py` into `pyvips/`.
248272

273+
Regenerate type stubs:
274+
275+
After adding new libvips operations or updating libvips itself, regenerate type stubs:
276+
277+
.. code-block:: shell
278+
279+
$ python examples/generate_type_stubs.py
280+
281+
This updates ``pyvips/__init__.pyi`` with the latest operations.
282+
249283
Regenerate autodocs:
250284

251285
Make sure you have installed a libvips with all optional packages enabled,

doc/conf.py

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import os
2020
import sys
2121
import sphinx_rtd_theme
22+
2223
sys.path.insert(0, os.path.abspath('..'))
2324

2425

@@ -55,24 +56,24 @@
5556
master_doc = 'index'
5657

5758
# General information about the project.
58-
project = u'pyvips'
59-
copyright = u'2019, John Cupitt'
60-
author = u'John Cupitt'
59+
project = 'pyvips'
60+
copyright = '2019, John Cupitt'
61+
author = 'John Cupitt'
6162

6263
# The version info for the project you're documenting, acts as replacement for
6364
# |version| and |release|, also used in various other places throughout the
6465
# built documents.
6566
#
6667
# The short X.Y version.
67-
version = u'3.1'
68+
version = u'3.2'
6869
# The full version, including alpha/beta/rc tags.
69-
release = u'3.1.1'
70+
release = u'3.2.0'
7071

7172
# The language for content autogenerated by Sphinx. Refer to documentation
7273
# for a list of supported languages.
7374
#
7475
# This is also used if you do content translation via gettext catalogs.
75-
# Usually you set "language" from the command line for these cases.
76+
# Usually you set 'language' from the command line for these cases.
7677
language = None
7778

7879
# List of patterns, relative to source directory, that match files and
@@ -103,7 +104,7 @@
103104

104105
# Add any paths that contain custom static files (such as style sheets) here,
105106
# relative to this directory. They are copied after the builtin static files,
106-
# so a file named "default.css" will overwrite the builtin "default.css".
107+
# so a file named 'default.css' will overwrite the builtin 'default.css'.
107108
html_static_path = ['_static']
108109

109110
# Custom sidebar templates, must be a dictionary that maps document names
@@ -126,7 +127,7 @@
126127
# each page.
127128
'github_user': 'libvips',
128129
'github_repo': 'pyvips',
129-
'github_version': 'master/doc/'
130+
'github_version': 'master/doc/',
130131
}
131132

132133
# -- Options for HTMLHelp output ------------------------------------------
@@ -141,15 +142,12 @@
141142
# The paper size ('letterpaper' or 'a4paper').
142143
#
143144
# 'papersize': 'letterpaper',
144-
145145
# The font size ('10pt', '11pt' or '12pt').
146146
#
147147
# 'pointsize': '10pt',
148-
149148
# Additional stuff for the LaTeX preamble.
150149
#
151150
# 'preamble': '',
152-
153151
# Latex figure (float) alignment
154152
#
155153
# 'figure_align': 'htbp',
@@ -159,19 +157,15 @@
159157
# (source start file, target name, title,
160158
# author, documentclass [howto, manual, or own class]).
161159
latex_documents = [
162-
(master_doc, 'pyvips.tex', u'pyvips Documentation',
163-
u'john', 'manual'),
160+
(master_doc, 'pyvips.tex', 'pyvips Documentation', 'john', 'manual'),
164161
]
165162

166163

167164
# -- Options for manual page output ---------------------------------------
168165

169166
# One entry per manual page. List of tuples
170167
# (source start file, name, description, authors, manual section).
171-
man_pages = [
172-
(master_doc, 'pyvips', u'pyvips Documentation',
173-
[author], 1)
174-
]
168+
man_pages = [(master_doc, 'pyvips', 'pyvips Documentation', [author], 1)]
175169

176170

177171
# -- Options for Texinfo output -------------------------------------------
@@ -180,19 +174,26 @@
180174
# (source start file, target name, title, author,
181175
# dir menu entry, description, category)
182176
texinfo_documents = [
183-
(master_doc, 'pyvips', u'pyvips Documentation',
184-
author, 'pyvips', 'One line description of project.',
185-
'Miscellaneous'),
177+
(
178+
master_doc,
179+
'pyvips',
180+
'pyvips Documentation',
181+
author,
182+
'pyvips',
183+
'One line description of project.',
184+
'Miscellaneous',
185+
),
186186
]
187187

188188

189189
# see https://stackoverflow.com/questions/20569011
190190
# adds autoautosummary directive, see vimage.rst
191191

192+
192193
# try to exclude deprecated
193194
def skip_deprecated(app, what, name, obj, skip, options):
194195
if hasattr(obj, "func_dict") and "__deprecated__" in obj.func_dict:
195-
print("skipping " + name)
196+
print('skipping ' + name)
196197
return True
197198
return skip or False
198199

@@ -206,10 +207,9 @@ def setup(app):
206207
from sphinx.util.inspect import safe_getattr
207208

208209
class AutoAutoSummary(Autosummary):
209-
210210
option_spec = {
211211
'methods': directives.unchanged,
212-
'attributes': directives.unchanged
212+
'attributes': directives.unchanged,
213213
}
214214

215215
required_arguments = 1
@@ -227,8 +227,10 @@ def get_members(obj, typ, include_public=None):
227227
continue
228228
if documenter.objtype == typ:
229229
items.append(name)
230-
public = [x for x in items
231-
if x in include_public or not x.startswith('_')]
230+
public = [
231+
x for x in items
232+
if x in include_public or not x.startswith('_')
233+
]
232234
return public, items
233235

234236
def run(self):
@@ -242,17 +244,21 @@ def run(self):
242244
_, methods = self.get_members(c,
243245
'method', ['__init__'])
244246

245-
self.content = ["~%s.%s" % (clazz, method)
246-
for method in methods
247-
if not method.startswith('_')]
247+
self.content = [
248+
'~%s.%s' % (clazz, method)
249+
for method in methods
250+
if not method.startswith('_')
251+
]
248252
if 'attributes' in self.options:
249253
_, attribs = self.get_members(c, 'attribute')
250-
self.content = ["~%s.%s" % (clazz, attrib)
251-
for attrib in attribs
252-
if not attrib.startswith('_')]
254+
self.content = [
255+
'~%s.%s' % (clazz, attrib)
256+
for attrib in attribs
257+
if not attrib.startswith('_')
258+
]
253259
finally:
254260
return super(AutoAutoSummary, self).run()
255261

256-
app.add_directive('autoautosummary', AutoAutoSummary)
262+
app.add_directive("autoautosummary", AutoAutoSummary)
257263
except BaseException as e:
258264
raise e

0 commit comments

Comments
 (0)