-
-
Notifications
You must be signed in to change notification settings - Fork 101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generate type stubs at the same time as generating modules #327
Comments
Memo for type hint symbolsQuestion: Does it require backward compatibility with generic-alias for builtin classes, related to PEP585?Answer: No, "There are currently no plans to remove the aliases from
|
Memo for type variable
|
Memo for
|
Currently, |
I thought what to do about the problem of I posted an issue to I got agreement to add |
The I will submit a PR to change the comtypes/comtypes/automation.py Line 556 in cc9a013
|
PEP585 will be updated. see python/peps#2778 |
Type hinting for statically defined module, like
|
I would like to use a similar process to generate runtime code and type stub code. I will create a base class that abstracts the However, defining these abstract and concrete classes in the same tools.codegenerator will result in complicated class names and bloated code in a single module. Therefore, |
The
This is a very nice suggestion.
I'm pretty sure you mean #263. My concern is that if all of these are included at once in one PR, it may be difficult to review. Please share in this issue about the overall changes. Thank you for taking the time to read this long article. |
No I don't mean #263. I did a redo of the code and made it easy to access the data. I did want to point out then when doing either inline type hinting or the commented way this is how you should go about doing it. This is from comtypes.typeinfo class ITypeLib(IUnknown):
_iid_: GUID = GUID("{00020402-0000-0000-C000-000000000046}")
# type-checking only methods use the default implementation that comtypes
# automatically creates for COM methods.
def GetTypeInfoCount(self) -> int:
"""Return the number of type informations"""
return self._GetTypeInfoCount()
def GetTypeInfo(self, index: int) -> "ITypeInfo":
"""Load type info by index"""
return self._GetTypeInfo(index)
def GetTypeInfoType(self, index: int) -> TYPEKIND:
"""Return the TYPEKIND of type information"""
return self._GetTypeInfoType(index)
def GetTypeInfoOfGuid(self, guid: GUID) -> "ITypeInfo":
"""Return type information for a guid"""
return self._GetTypeInfoOfGuid(guid)
def GetTypeComp(self) -> "ITypeComp":
"""Return an ITypeComp pointer."""
return self._GetTypeComp()
def GetDocumentation(
self,
index: int
) -> Tuple[str, str, int, Optional[str]]:
"""Return documentation for a type description."""
return self._GetDocumentation(index)
def ReleaseTLibAttr(self, ptla: "TLIBATTR") -> None:
"""Release TLIBATTR"""
self._ReleaseTLibAttr(byref(ptla))
def GetLibAttr(self) -> "TLIBATTR":
"""Return type library attributes"""
return _deref_with_release(self._GetLibAttr(), self.ReleaseTLibAttr)
def IsName(self, name: str, lHashVal: Optional[int] = 0) -> Optional[str]:
"""Check if there is type information for this name.
Returns the name with capitalization found in the type
library, or None.
"""
from ctypes import create_unicode_buffer
namebuf = create_unicode_buffer(name)
found = BOOL()
self.__com_IsName(namebuf, lHashVal, byref(found))
if found.value:
return namebuf[:].split("\0", 1)[0]
return None
def FindName(
self,
name: str,
lHashVal: Optional[int] = 0
) -> Optional[Tuple[int, "ITypeInfo"]]:
# Hm...
# Could search for more than one name - should we support this?
found = c_ushort(1)
tinfo = POINTER(ITypeInfo)()
memid = MEMBERID()
self.__com_FindName(
name,
lHashVal,
byref(tinfo),
byref(memid),
byref(found)
)
if found.value:
return memid.value, tinfo # type: ignore
return None If memory serves I believe this is how the COM interfaces are set up. you have the ability to set methods using the same name as the underlying COM method. You can access the method so that the "in" and "out" flags work properly by calling the method with a preceding underscore. you can also access the COM method by preceding the method name with "com", when going this route the "in" and "out" flags are ignored and you have to pass the data containers as is needed. Structures and Unions from ctypes are set up a bit differently class N11tagTYPEDESC5DOLLAR_203E(Union):
# C:/Programme/gccxml/bin/Vc71/PlatformSDK/oaidl.h 584
@property
def lptdesc(self) -> TYPEDESC:
return TYPEDESC()
@lptdesc.setter
def lptdesc(self, value: TYPEDESC):
pass
@property
def lpadesc(self) -> tagARRAYDESC:
return tagARRAYDESC()
@lpadesc.setter
def lpadesc(self, value: tagARRAYDESC):
pass
@property
def hreftype(self) -> int:
return int()
@hreftype.setter
def hreftype(self, value: int):
pass
_fields_ = [
# C:/Programme/gccxml/bin/Vc71/PlatformSDK/oaidl.h 584
('lptdesc', POINTER(tagTYPEDESC)),
('lpadesc', POINTER(tagARRAYDESC)),
('hreftype', HREFTYPE),
] I know that looks a little goofy but what happens on the back end is when the class gets built the properties get overwritten by what is in fields An IDE does not see the c code that works that magic so it is a seamless transition. Using the mechanism you are using makes the code more difficult to read. A comment can be added to the methods that get overridden saying that they get overridden by what is in fields I would really consider using the data_types module I have written. It will make the generated typelibs match what is actually in the typelib. I know that C code really has no difference between things like a LONG and an INT or a HANDLE and a HWND, it would still be nice to have the proper data type being seen in the generated code. I also want to change up the data_types file so that instead of a variable being created for a specific data type a subclass of the data type is used instead. I have done this for enumerations and I have cleverly come up with a way to wrap an enumeration item in a manner that allows it to be identified by its name or by its value. |
You also have some odd type hinting.
This would be easier from ctypes import POINTER
from typing import TypeVar, Type
_TV_POINTER = TypeVar("_TV_POINTER", bound=POINTER)
_T_POINTER = Type[_TV_POINTER] and that allows your type hint to be The use of from typing import TYPE_CHECKING
if TYPE_CHECKING:
from some_module import SomeClass
def do() -> "SomeClass":
... |
Those suggestions make the code a lot easier to read. They are only suggestions and you can do whatever it is that you like. You should have the maintainer of comtypes create a new branch and label that branch so it aligns with a milestone/project that has been made for dropping support for Python 3. This allows modifications to be made now without effecting the master branch and it also allows the code to get used and tested. |
If so, please PR that revised version.
This is certainly a good way to go. The method defined by the metaclass is annotated with
I understood this behavior when I refactored the metaclass that generates methods(#367, #368 and #373).
I have some opinions on this. class N11tagTYPEDESC5DOLLAR_203E(Union):
lptdesc: TYPEDESC
...
_fields_ = [
('lptdesc', POINTER(tagTYPEDESC)),
...
] I don't want to have something recognized as a
This should be really great. I would like you to implement this feature in a different issue scope than this, regardless of type hinting. |
This should be a useful generics. Thanks for the advice. |
Previously, inadvertently adding/existing symbols in a module would cause a bug when a COM object with the same name existed on the type library(This caused #330). Currently, I have set So when we drop the Python 2 system and no longer need the workaround, we should use the way you suggested. |
I feel that making a branch to start working on dropping python2 is the way to go. There is really no sense in doing all the type hints for python 2 and 3 just to have to change them all in a year from now. |
I agree. So I will create a new branch once I get agreement from the other maintainers. If we can do that, then let's proceed with the type hinting in there. |
@vasily-v-ryabov @kdschlosser knows how to do better with inline annotations for this type hinting enhancement without generating pyi files. This is a feature that is not available with the current Python2 support. So I would like to create a separate If there are no objections in the next week, or if you agree, we will proceed in that direction. Please consider this. |
I sent mention to the other maintainers. While waiting for replies, please PR any features you would like to merge into the current I will review them. |
I created #392 to discuss regarding Python2 drops. |
And my rebuttal to this is 2 fold. First is it is a property that gets created (see below) and second is docstrings!! While I know that docstrings can be added to attributes they are only for sphinx and not a built in python feature. They might show up in an IDE if the IDE supports sphinx style attribute docstrings. Doing it the way I have suggested is 100% working with all IDEs that support pyhton because properties are a built in feature of python and not sphinx. https://github.com/python/cpython/blob/main/Modules/_ctypes/stgdict.c line 585. if (isStruct) {
prop = PyCField_FromDesc(desc, i,
&field_size, bitsize, &bitofs,
&size, &offset, &align,
pack, big_endian); and then in https://github.com/python/cpython/blob/main/Modules/_ctypes/cfield.c line 208 static int
PyCField_set(CFieldObject *self, PyObject *inst, PyObject *value)
{
CDataObject *dst;
char *ptr;
if (!CDataObject_Check(inst)) {
PyErr_SetString(PyExc_TypeError,
"not a ctype instance");
return -1;
}
dst = (CDataObject *)inst;
ptr = dst->b_ptr + self->offset;
if (value == NULL) {
PyErr_SetString(PyExc_TypeError,
"can't delete attribute");
return -1;
}
return PyCData_set(inst, self->proto, self->setfunc, value,
self->index, self->size, ptr);
}
static PyObject *
PyCField_get(CFieldObject *self, PyObject *inst, PyTypeObject *type)
{
CDataObject *src;
if (inst == NULL) {
return Py_NewRef(self);
}
if (!CDataObject_Check(inst)) {
PyErr_SetString(PyExc_TypeError,
"not a ctype instance");
return NULL;
}
src = (CDataObject *)inst;
return PyCData_get(self->proto, self->getfunc, inst,
self->index, self->size, src->b_ptr + self->offset);
} Look at the function names It is a property that is created. Now unfortunately there is no mechanics in place for docstrings and the properties set in place that get overridden by the fields happens when the class is built so for purposes of sphinx the docstrings would be useless and at runtime |
gotta go to the backend mechanics to see what is actually happening. Personally I like how comtypes works and how a method doesn't get overridden by what is in _methods_ and instead an alternative attribute gets created. This allows for special handling of that method to be performed. With ctypes Unions and Structures this is not the case and to be honest it frankly sucks because whatever data gets passed to a specific field you might want to alter/change. An example of this would be VARIANTBOOL which uses |
Thank you for the great commentary! >>> import ctypes
>>> class Foo(ctypes.Structure):
... pass
...
>>> Foo._fields_ = [("ham", ctypes.c_int)]
>>> Foo.ham
<Field type=c_long, ofs=0, size=4>
>>> isinstance(Foo.ham, property)
False From the above, I was concerned that the >>> Foo.ham.__get__
<method-wrapper '__get__' of _ctypes.CField object at 0x0000017E53D8D3C0>
>>> Foo.ham.__set__
<method-wrapper '__set__' of _ctypes.CField object at 0x0000017E53D8D3C0> Since both As for docstring, I think it is inevitable that it will be lost in runtime. But
is helpful for developers. |
yessir. That is why these kinds of brain storming sessions are needed. we want to make sure the code is going to be right and that it is going to work properly. The back end c code for ctypes is hard to follow. It takes a while to track down what exactly is going on. and if you call type on the class method that is set for the field the type of CField gets returned. There technically speaking is a difference between a property and a get/set descriptor tho they do function almost identical. A property is just a convenience class for a get/set descriptor and it implements mechanisms to be used as decorators. It also provides some additional functions instead of calling __get__ and __set__ and __delete__. Those methods are fget, fset and fdel. I am willing to bet if you did |
As I had almost written type hints for these, but deliberately avoided starting on them because they would be too complicated. I may be wrong in some areas, but this is what I imagined. from typing import (
Any, Callable, Generic, NoReturn, Optional, overload, SupportsIndex,
TypeVar
)
_R_Fget = TypeVar("_R_Fget")
_T_Instance = TypeVar("_T_Instance")
class bound_named_property(Generic[_R_Fget, _T_Instance]):
def __init__(
self,
name: str,
fget: Optional[Callable[..., _R_Fget]],
fset: Optional[Callable[..., Any]],
instance: _T_Instance
) -> None: ...
def __getitem__(self, index: SupportsIndex) -> _R_Fget: ...
def __call__(self, *args: Any) -> _R_Fget: ...
def __setitem__(self, index: SupportsIndex, value: Any) -> None: ...
def __iter__(self) -> NoReturn: ...
class named_property(Generic[_R_Fget, _T_Instance]):
def __init__(
self,
name: str
fget: Optional[Callable[..., _R_Fget]] = ...,
fset: Optional[Callable[..., Any]] = ...,
doc: Optional[text_type] = ...
) -> None: ...
@overload
def __get__(self, instance: _T_Instance, owner: Optional[_T_Instance] = ...) -> bound_named_property[_R_Fget, _T_Instance]: ...
@overload
def __get__(self, instance: None, owner: Optional[_T_Instance] = ...) -> named_property[_R_Fget, None]: ... However, this would lose the type information of the arguments, so I considered a pattern using # It's PEP585 and PEP604 style. Please think them will be changed to `typing` symbols.
_T = TypeVar("_T")
_R = TypeVar("_R")
_T_Inst = TypeVar("_T_Inst")
_GetP = ParamSpec("_GetP")
_SetP = ParamSpec("_SetP")
class bound_named_property(Generic[_T_Inst, _GetP, _R, _SetP]):
name: str
instance: _T_Inst
fget: Callable[Concatenate[_T_Inst, _GetP], _R]
fset: Callable[Concatenate[_T_Inst, _SetP], None]
def __init__(self, name: str, fget: Callable[Concatenate[_T_Inst, _GetP], _R], fset: Callable[Concatenate[_T_Inst, _SetP], None], instance: _T_Inst) -> None: ...
def __getitem__(self, index: Any) -> _R: ...
def __call__(self, *args: _GetP.args, **kwargs: _GetP.kwargs) -> _R: ...
def __setitem__(self, index: Any, value: Any) -> None: ...
def __iter__(self) -> NoReturn: ...
class named_property(Generic[_T_Inst, _GetP, _R, _SetP]):
name: str
fget: None | Callable[Concatenate[_T_Inst, _GetP], _R]
fset: None | Callable[Concatenate[_T_Inst, _SetP], None]
__doc__: None | str
def __init__(self, name: str, fget: Callable[Concatenate[_T_Inst, _GetP], _R] | None = ..., fset: Callable[Concatenate[_T_Inst, _SetP], None] | None = ..., doc: str | None = ...) -> None: ...
@overload
def __get__(self, instance: None, owner: type[_T_Inst]) -> named_property[None, _GetP, _R, _SetP]: ...
@overload
def __get__(self, instance: _T_Inst, owner: type[_T_Inst] | None) -> bound_named_property[_T_Inst, _GetP, _R, _SetP]: ...
def __set__(self, instance: _T_Inst, value: Any) -> NoReturn: ... Please let me know what you think about |
type hinting those classes are pointless. actually type hinting most of what is in that file is pointless. This is because it is never really going to end up getting used at all. All of the stuff in the _memberspec file is for the dynamic creation of the methods and properties that are used when a class is dynamically built. Those type hints will never be realized when the user creates a COM interface. There is nothing that can be done for the users code and they would be responsible for the type hinting of their code. It's because of the dynamic nature of how it works. Technically speaking those classes should not be public classes either and they should be prefixed with an "_".. |
I see, so there is a better way to express the runtime behavior of And I also think |
The back end mechanics of comtypes was designed for early Python 2. A lot has changed since then and I am sure that there is a lot of refactoring of the code that can be done. I don't fully grasp why there is like a line of classes used to set a simple property... But I am sure it probably had something to do with getting comtypes to work with early versions of Python 2. |
At some point I will dive into it in depth and see what is actually happening with the back end. |
I am curious about the methods of each class patched by Lines 407 to 503 in 7e90e54
These correspond to one class per attribute name that is In any case, this implementation is a bad fit with static type analysis. I'm sure you have some great ideas for this too. |
I will close this issue since it has been shifted to #400 and #401, related to inline type annotations. The scope of this issue resulted in only adding PEP484-compliant type annotations for statically defined modules and refactoring of the module and code generation process. @kdschlosser |
Abstract of proposal
I wish
client.GetModule
generatesFriendly.pyi
type stub at same time as generating_xxxxxxxxxxxx_xxxxxxxx_xxxxxxxx_xxxxxxxxxxxx_x_x_x.py
andFriendly.py
runtime modules.Rationale
comtypes
dynamically creates type-defined modules. This is a feature that other COM manipulation libraries don't have.However, the type definitions are imported from the wrapper module
_xxxxxxxxxxxx_xxxxxxxx_xxxxxxxx_xxxxxxxxxxxx_x_x_x.py
to the user-friendly moduleFriendly.py
as shown below, so all type information is hidden from the type checker.In terms of coding usability, it is no different than dynamically calling methods and attributes from a
Dispatch
object, and it is difficult to figure out what API the module has.Also, the methods and properties of the derived classes of
IUnknown
andCoClass
in the generated module are defined by the metaclass, so the type checker cannot obtain information on arguments and return values.I would like to add the feature to generate type stubs in a format according to PEP561 at the same time as the runtime module to
client.GetModule
to figure out easily what API the module has.When
and
and
static
modules below.comtypes/__init__.py
comtypes/automation.py
comtypes/client/__init__.py
comtypes/client/_genetate.py
The text was updated successfully, but these errors were encountered: