Add type hints support (#355)#564
Conversation
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.
|
This is great @JoshCLWren! I'm on holiday right now, but I'll read this carefully when I get home again. |
jcupitt
left a comment
There was a problem hiding this comment.
Other:
- we need a line in the changelog, and please credit yourself for doing this nice work
generate_type_stubs.pyneeds to be set executable- does the type stub generator need to be in
pyvips/? it's something for maintainers, so I'd be inclined to put it inexamples/. The doc generator is inpyvips/because it's used forhelp()output - perhaps we could run mypy on the demo scripts in
examples/as part of CI? but it'll need eg. a hint forimage * [1, 2, 3]first
pyvips/generate_type_stubs.py
Outdated
| stub += """ | ||
|
|
||
| # Operator overloads | ||
| def __add__(self, other: Union[Image, float, int]) -> Image: ... |
There was a problem hiding this comment.
You can have arrays of float and int too, eg. a + [1, 2, 3] adds 1 to band 0, 2 to band 1, 3 to band 2.
| def __call__(self, x: int, y: int) -> List[float]: ... | ||
| def __repr__(self) -> str: ... | ||
|
|
||
|
|
There was a problem hiding this comment.
There are a set of hand-written bindings too, perhaps some of them could have type hints?
Easy: image.floor() etc.
Medium: bandjoin, bandsplit, composite etc.
Horrible: ifthenelse.
…erator 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
pyvips/__init__.pyi
Outdated
| @staticmethod | ||
| def new_from_list(array: List[List[float]], scale: float =1.0, offset: float = 0.0) -> Image: ... | ||
| def new_from_list( | ||
| array: List[List[float]], scale: float = 1.0, offset: float = 0.0 |
There was a problem hiding this comment.
I think this can also be List[List[int]]
| enum_classes = "\n".join([f"class {name}: ..." for name in enum_names]) | ||
|
|
||
| stub = f'''"""Type stubs for pyvips. | ||
|
|
There was a problem hiding this comment.
Add:
# flake8: noqa: E501to stop flake8 moaning about long lines in the generated output.
| """ | ||
|
|
||
| import typing | ||
| from typing import Any, Optional, Union |
There was a problem hiding this comment.
You don't use any of these (I think).
| type_map, | ||
| type_from_name, | ||
| nickname_find, | ||
| at_least_libvips, |
examples/generate_type_stubs.py
Outdated
| nickname_find, | ||
| at_least_libvips, | ||
| ) | ||
| from pyvips import ffi, Error, _to_bytes, _to_string |
| import os | ||
|
|
||
| # Script is in examples/, need to go up one level to find pyvips/ | ||
| stub_file = os.path.join(os.path.dirname(__file__), "..", "pyvips", "__init__.pyi") |
There was a problem hiding this comment.
I'm not sure we need this -- just generate __init__.pyi in the current directory and let the user know. Or just print to stdout (the safest).
examples/generate_type_stubs.py
Outdated
| """Generate type signature for an operation method.""" | ||
| intro = Introspect.get(operation_name) | ||
|
|
||
| if (intro.flags & 4) != 0: # _OPERATION_DEPRECATED |
There was a problem hiding this comment.
Is this correct? VipsOperationFlags uses 8 for VipsOperationFlags. I think you're removed all uncacheable operations instead.
|
|
||
| # Return type | ||
| output_types = [ | ||
| gtype_to_python_type(intro.details[name]["type"]) |
|
|
||
| # Optional output dicts can contain any metadata value type | ||
| if len(intro.doc_optional_output) > 0: | ||
| dict_value_type = ( |
| return "\n".join([sig for _, sig in all_names]) | ||
|
|
||
|
|
||
| def get_all_enum_names() -> list[str]: |
There was a problem hiding this comment.
I suppose we should handle flags as well as enums, but perhaps that can wait for another PR.
There was a problem hiding this comment.
gen-enums has this code as well, of course, perhaps it should be moved into Introspect
|
Sorry, there are a lot of comments. I don't know how much time you have -- we could merge and share the work of fixups in later PRs, if you like. |
|
Oh, I just saw cdisplayagain, nice! You have:
Another possible solution is: $ pip install pyvips[binary]And it should download and use a libvips binary appropriate for your system. It might be a little easier for your users. |
No worries! Thanks for the feedback. I'll get on it this afternoon/evening. |
Good call, I think that'll help a lot. |
…precated 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.
…, add flake8 noqa
- 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.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
Match operator overload signatures which already support int and List[int] types. Ensures type consistency across the Image API.
- 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
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.
|
Sorry for the pause. Let's merge! Thank you again for doing this work, @JoshCLWren. |
Closes #355
This PR adds PEP 561 type stubs for pyvips, enabling IDE autocomplete and type checking with mypy.
Changes
pyvips/__init__.pyi- Type stubs for all Image operations (300+ methods)pyvips/generate_type_stubs.py- Script to regenerate stubs from libvips introspectiontests/test_type_hints.py- Test verifying type stubs work correctlyDesign Decisions
Type stubs are generated rather than handwritten because:
This follows the existing pattern used for:
examples/gen-enums.py)pyvips.Operation.generate_sphinx_all())Features
Any- specific Union types insteadin_forinparameter)Usage
Users can now type check pyvips code:
Regenerate stubs after libvips updates:
Testing
test_type_hints.pyadded