Closed Andrej730 closed 2 months ago
Found a bit more issues with mesh.materials:
mesh.materials.values
is missing possible None
values. Note that keys
and items
are also missing possible None
values but for keys
and items
it is correct. 🙃mesh.materials.find
allows None
value though in Blender it's not actually allowedmesh.materials.get
allows None
value though in Blender it's not actually allowedmesh.materials.pop
allows None
value though in Blender it's not actually allowedmesh.materials.pop
allows supplying index a positional argument but in Blender this argument is keyword only and Blender will raise an Exception if it's supplied positionally.import bpy
obj = bpy.data.objects["Cube"]
mesh = obj.data
assert isinstance(mesh, bpy.types.Mesh)
mesh.materials.append(None)
print(mesh.materials.values()) # [bpy.data.materials['Material'], None]
values = mesh.materials.values()
# Type of "values" is "list[Material]"
# Should have been: list[Material | None]
# reveal_type(values)
# though this has a correct typing - those methods do skip None
print(mesh.materials.keys()) # ['Material']
print(mesh.materials.items()) # [('Material', bpy.data.materials['Material'])]
# no type errors for two statements below
# in Blender actually will fail with the same error:
# Error: Python: TypeError: bad argument type for built-in operation
# The above exception was the direct cause of the following exception:
# Traceback (most recent call last):
# File "\Text", line 6, in <module>
# SystemError: <built-in method find of bpy_prop_collection object at 0x0000023092A7A3D0> returned a result with an exception set
print(mesh.materials.find(None))
print(mesh.materials.get(None))
Another example:
>>> C.object.data.materials.pop(index=None)
Traceback (most recent call last):
File "<blender_console>", line 1, in <module>
TypeError: IDMaterials.pop(): error with keyword argument "index" - Function.index expected an int type, not NoneType
>>> C.object.data.materials.pop(5)
Traceback (most recent call last):
File "<blender_console>", line 1, in <module>
TypeError: IDMaterials.pop(): required parameter "index" to be a keyword argument!
Mesh.materials
is documented as an IDMaterials
bpy_prop_collection
of Material
—no mention of None
. bpy_prop_collection.values()
is then working as intended for a bpy_prop_collection
that does not contain None
.
Issues 2 to 5 are issues with bpy_prop_collection
(and bpy_struct
in the case of pop()
)—I would suggest opening a separate issue for them.
I'll look into drafting a Blender docs PR to add None
to places where material slots can be empty.
Found 1 more issue - a bit confusing one. When we use mesh.materials.__contains__
it doesn't allow any types besides the strings, which is correct. But if we do material in mesh.materials
it does allow a Material type though in Blender it will still result in error as it's still the same __contains__
method and only strings are allowed.
import bpy
mesh = bpy.data.meshes["Cube"]
material = bpy.data.materials["Material"]
print("Material" in mesh.materials)
# Shows a type error which is okay.
mesh.materials.__contains__(material)
# Traceback (most recent call last):
# File "<blender_console>", line 1, in <module>
# TypeError: bpy_prop_collection.__contains__: expected a string or a tuple of strings
# Doesn't show a type error though it won't work in Blender.
print(material in mesh.materials)
@Road-hog123 @Andrej730 @JonathanPlasse
This discussion may relate to #243 .
Mesh.materials is documented as an IDMaterials bpy_prop_collection of Material—no mention of None.
I think this can not be handled from the documentation because never None
and other options are generated by the internal flag.
Before fixing this issue, we should consider the strategy to find which arguments/return/attributes are optional (accept None) or not.
Current strategy is here.
never None
or readonly
, data type will be non-optional.active
, data type will be optional. or None
, data type will be optional.Optional
, data type will be optional.The code can be found at https://github.com/nutti/fake-bpy-module/blob/991583418cf2026dc1603a247b0d84f98983e17e/src/fake_bpy_module/transformer/data_type_refiner.py#L490-L552 https://github.com/nutti/fake-bpy-module/blob/991583418cf2026dc1603a247b0d84f98983e17e/src/fake_bpy_module/transformer/data_type_refiner.py#L571-L580
Could you give me the advice to improve the strategy to fix this issue? Optional data type is annoying point due to the inconsistent information on documentation.
Optional data type is annoying point due to the inconsistent information on documentation.
Yeah, I suspect the only way to properly fix this is to make the documentation consistent—I hope to find some time and energy to really dive into issues like this soon.
I'll look into drafting a Blender docs PR to add
None
to places where material slots can be empty.
As I feared, this is non-trivial, so I can't do it right now. 😞
With regards to:
5.
mesh.materials.pop
allows supplying index a positional argument but in Blender this argument is keyword only and Blender will raise an Exception if it's supplied positionally.
@nutti how best to declare arguments as keyword-only?
how best to declare arguments as keyword-only?
perhaps this issue could help - https://github.com/nutti/fake-bpy-module/issues/226
Update. Found one more problem - it's regarding mesh.materials.__setitem__
. Updated the tests in the first post.
# Argument of type "None" cannot be assigned to parameter "value" of type "Material" in function "__setitem__"
"None" is incompatible with "Material"
# fails but shouldn't
mesh.materials[0] = None
# type check fails and should keep failing as it will raise an error in Blender
# TypeError: bpy_prop_collection[key]: invalid key, must be a string or an int, not str
mesh.materials["Material"] = None
It's a bit tricky and requires an overload to resolve...
@overload
def __setitem__(self, key: int, value: GenericType1 | None): ...
@overload
def __setitem__(self, key: str, value: GenericType1): ...
def __setitem__(self, key: int | str, value: GenericType1 | None): ...
Maybe #226 is a bit complicated because this uses the transformers.
Does below syntax work to specify the keyword-only argument in mod file?
.. function:: some_func(arg_1, arg_2, *, kwonly_arg_1, kwonly_arg_2)
mesh.materials['Material'] = None
fails because assignment to string keys is not supported by bpy_prop_collection
—even `mesh.materials['Material'] = mesh.materials['Material'] will fail with the same error. I have opened #264 to remove that support from this module, and Blender PR #123577 to fix the unhelpful error message.
The problem with mesh.materials[0] = None
is not that __setitem__
does not accept None
in addition to GenericType
, but that GenericType
is not Material | None
for IDMaterials
—all of the inherited methods that accept or return GenericType
will be wrong when the value passed as GenericType
is wrong.
While investigating in the code, I did find a function pyrna_prop_collection_type_check
which includes this snippet:
if (value == Py_None) {
if (RNA_property_flag(self->prop) & PROP_NEVER_NULL) {
PyErr_Format(PyExc_TypeError,
"bpy_prop_collection[key] = value: invalid, "
"this collection doesn't support None assignment");
return -1;
}
return 0; /* None is OK. */
}
I don't know if the generation code has access to the flags, but it would seem that for bpy_prop_collection
there exists a flag that declares whether GenericType
should be a union with None
.
Does below syntax work to specify the keyword-only argument in mod file?
.. class:: IDMaterials
.. method:: pop(self, *, index)
This had no effect on the generated modules, but it seems like a reasonable proposal.
Other related issue.
From #279.
import bpy
from typing import assert_type
class ADDON_preferences(bpy.types.AddonPreferences):
def draw(self, context):
# "assert_type" mismatch: expected "UILayout" but received "UILayout | None"
assert_type(self.layout, bpy.types.UILayout)
layout = self.layout
# "box" is not a known attribute of "None"
box = layout.box()
# "row" is not a known attribute of "None"
row = layout.row()
Related issue:
def run_on_local_datablock(datablock: bpy.types.ID):
if datablock.library is not None:
# linked datablock
return
# reports unreachable code, because datablock.library is supposedly never None
...
From #292.
I've noticed in a few places that typing is suggesting hat it's possible that provided
context
will beNone
:class Menu: def poll(cls, context: Context | None) -> bool: ... def draw(self, context: Context | None): ... class AssetShelf: def poll(cls, context: Context | None) -> bool: ...
leading to issues like this:
class VIEW3D_MT_PIE_bim_class(bpy.types.Menu): bl_label = "Class" @classmethod def poll(cls, context): if not context.active_object and not context.selected_objects: cls.poll_message_set("No object selected.") return False return True
And it's not just
Menu
andAssetShelf
, there are other places (e.g.UIList.draw_item
,UIList.draw_filter
) - you can find them by searchingContext | None
in thebpy\types\__init__.pyi
, so it could to be a part of some more general issue.
@Andrej730
Is it possible to report this kind of issue here to understand the pattern whether the argument is optional or not? If you are difficult to distinguish whether an issue is relates to this issue, you can discuss at Discord channel.
Updated the strategy to find "never none" and "accept none". This strategy does not work correctly in some case. I will close this issue for now because this should be solved by the official document.
fake-bpy-module-latest==20240618
We can add None to the
mesh.materials
but if we try to iter over it, it's missing possible None type.Test snippet that should pass:
Related: