Enum Type Errors
Describe the bug
Currently, all wgpu-py enum member arguments are marked as type errors when given to functions that expect the overall enum type.
This applies for user-defined functions, but also library functions such as device.create_buffer, which require an enum argument.
To Reproduce
- Ensure
wgpu-pyis installed for the project - Install
pylancewith VSCode - Click the curly bracket pair at the bottom right of the status bar next to 'Python' and set the type checking mode to 'basic', 'standard', or 'strict'
- Create a python file with the following contents:
from wgpu import PowerPreference
def example_set_power_mode(preference: PowerPreference):
pass
example_set_power_mode(PowerPreference.high_performance)
Observe the following type error:
Argument of type "str" cannot be assigned to parameter "preference" of type "PowerPreference" in function "example_set_power_mode"
"str" is not assignable to "PowerPreference" (PylancereportArgumentType)
Expected behavior
I expect that I should be able to pass a PowerPreference.high_performance into a function which expects a PowerPreference.
Notes
It seems like all enums are being coerced into their underlying representative type by _coreutils/EnumType upon field access, which is what is causing this type error.
While there aren't any runtime problems with the current enum type, the linting errors can be a bit distracting. In a small ~4k LOC project, I have over 40 type errors just from the enum errors. Part of me likes that about Python, because I can have that many linting errors and still run my program with no problem, but today I woke up and chose violence against these squigglies.
Disabling argument typing errors altogether is also not an option for me because it would negate a lot of the static typing benefits.
I noticed (with some help from GPT) that using the Enum type defined by the python library, there are no such type errors that occur upon accessing a specific enum member, even when defining the underlying representation of the enum member.
from enum import Enum
class PowerPreference(Enum):
low_power = "low-power"
high_performance = "high-performance"
def example_set_power_mode(preference: PowerPreference):
pass
example_set_power_mode(PowerPreference.high_performance)
I'm thinking of a few ways this error can be mitigated:
- We could change all functions which take in an enum to take in the underlying type representation instead.
- The authors behind the webgpu type spec chose this route. Though, they alias the literal type (i.e. str or number) with a new type for more clarity. For example,
type GPUBufferUsageFlags = number;
- The authors behind the webgpu type spec chose this route. Though, they alias the literal type (i.e. str or number) with a new type for more clarity. For example,
- We could find out a way to access an enum member of
_coreutils/EnumTypewithout coercing the underlying type to a literal. - We could use the standard library
Enuminstead. Is there an advantage to our_coreutils/EnumTypeimplementation over the standard library enum that I'm not aware of?
Or maybe there's a way to silence these errors specifically without removing type-checking for all arguments that I'm not aware of?
Thanks for making wgpu-py! I've always wanted to use numpy with a more modern API than OpenGL and this project has made it possible to do so.
Your environment
██ system:
platform: macOS-15.5-arm64-arm-64bit-Mach-O
python_implementation: CPython
python: 3.13.3
██ versions:
wgpu: 0.22.2
I think the std enum types aren't available in 3.9 and 3.10 which we still support for a while, also a bunch of typing improvements ever since so it's a bit troublesome to have something work across the board. I tinkered with the codegen type hints quite a bit recently, so I will have a look what's possible.
I know that full typing coverage isn't an official goal, but I feel like it's really helpful for working on the lib itself and working with the lib for other projects. I once read that some large corporations are only allowed to use python dependencies that are fully typed with mypy so it's a good plus to have for adoption and potential contribution.
I agree that the typing needs some improvements.
I cannot reproduce the reported issue though 🤔 (I tried updating VSCode, and starting in a fresh default window).
@almarklein Do you have the "Type Checking" mode set to "basic" or higher? I forgot that it was disabled by default for the VSCode extension. My bad, I should update the repro steps!
Do you have the "Type Checking" mode set to "basic" or higher?
That does the trick, thanks!
I thought about this issue some more. I think we try to achieve three things:
- Generating clear API docs.
- Discoverability in an interactive session (Jupyter / terminal / Pyzo).
- Can just print
wgpu.PowerPreferenceand then see options. - Via autocompletion.
- Can just print
- Easy code writing and validation in VSCode et al.
- Autocompletion.
- 🚫 We currently don't have proper type checking.
For reference, this is the PR that initially introduced this enum implementation in Pygfx: https://github.com/pygfx/pygfx/pull/718, which was later also adopted in wgpu-py.
I explored some options, and I think this could work well:
PowerPreference: TypeAlias = Literal["high-performance", "low-power"] #:
(I'm not sure the TypeAlias makes a difference, but it seems appropriate.)
Docs work:
Interactively checking options works:
>>> wgpu.PowerPreference
typing.Literal['high-performance', 'low-power']
Type checking works too:
The only catches I see so far:
- No autocompletion, but maybe this is a good thing.
- Can not iterating over the enum to get the fields, but rather niche, and using
for option PowerPreference.__args__does work.
Flags need a slightly different approach. We can do similar to what WebGPU seems to be doing:
BufferUsageFlags: TypeAlias = int
class BufferUsage:
MAP_READ = 1
MAP_WRITE = 2
COPY_SRC = 4
COPY_DST = 8
INDEX = 16
VERTEX = 32
UNIFORM = 64
STORAGE = 128
INDIRECT = 256
QUERY_RESOLVE = 512
In the methods, use BufferUsageFlags. People can use wgpu.BufferUsage to discover the options. And its used to generate the docs.
Structs ... will get to that another time, but TupedDict could be really helpful.
Fo reference, Python's enums don't work too well for what we want:
class PowerPreference3(str, Enum):
high_performance = "high-performance"
low_power = "low-power"
def example_set_power_mode3(preference: PowerPreference3):
print(preference)
example_set_power_mode3(preference=PowerPreference3.high_performance) # Pylance happy ✅
example_set_power_mode3(preference="high-power") # Pylance sad 🚫
edit: also see https://github.com/python/typing/issues/781
I really like the verbosity, especially with flags for requesting multiple and setting defaults. The convenience of using a string literal is great and maybe what fits best for python. However explaining them all in a docstrings seems difficult for discoverability as the enums themselves don't have docstrings and I often look up details on the wgpu.rs site or webgpu spec. Also you wouldn't want the auto complete to show everything twice.
edit: In reading @Vipitis' comment I assumed it was referring to an explanation of the fields of the enum (e.g. what does high-performance mean).
Good point; we don't have per-field docstrings now, but that would be nice, we can actually obtain these from upstream (idl or webgpu.yml). We can do:
PowerPreference: TypeAlias = Literal["high-performance", "low-power"] #:
""" To help select the preferred GPU:
* "high-performance": use the biggest thing in your machine
* "low-power": safe your battery
"""
Though that only really helps our docs become more complete. I'm not sure if we can provide per-field documentation in a static way.
We could do:
PowerPreference: TypeAlias = Union[
Annotated[Literal["high-performance"], "use the biggest thing in your machine"],
Annotated[Literal["low-power"], "safe your battery"],
]
But VSCode does not seem to pick it up and show it (yet).
@Vipitis
However explaining them all in a docstrings seems difficult for discoverability as the enums themselves don't have docstrings and I often look up details on the wgpu.rs site or webgpu spec.
I'm not sure I understand. Do you mean that having to type out a literal in the argument to example_set_power_mode(preference="high-performance") makes it harder to discover the enumerations of the PowerPreference enum, compared to the current implementation, which requires set_power_mode(preference=wgpu.PowerPreference.high_performance)?
Assuming the user does not have autocomplete,
I think in @almarklein's implementation of the typing
PowerPreference: TypeAlias = Literal["high-performance", "low-power"] #: """ To help select the preferred GPU: * "high-performance": use the biggest thing in your machine * "low-power": safe your battery """ def set_power_mode(preference: PowerPreference): pass set_power_mode(preference="") #user tries to autocomplete here
The user would check the signature of set_power_mode, see PowerPreference, and then have to check the members of PowerPreference.
In the current implementation, the user still has to check the signature of set_power_mode, see wgpu.PowerPreference, and then have to check the members of wgpu.PowerPreference.
If the user has autocomplete, the options would stay the same for both scenarios.
Also you wouldn't want the auto complete to show everything twice.
Could you expand on this? @almarklein's implementation seems to autocomplete just once.