-
-
Notifications
You must be signed in to change notification settings - Fork 42
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
bug: Wrong alias target path when inspecting class, causing stubs-merging to fail silently #296
Comments
Hi @tlambert03, thanks for the report, and sorry for the troubles 🙂 My first intuition is that Griffe doesn't understand how the stubs are expected to be merged. I'll investigate with the repo you linked 🙂 |
Oh, by the way, the |
Thanks, yeah I had a feeling that was the case :) included it "just in case" :) |
OK so basically Griffe has trouble understanding your module/object layout. TL;DR at the end 😄
>>> from pymmcore import CMMCore
>>> CMMCore.__module__
'pymmcore.pymmcore_swig' It actually comes from But >>> from pymmcore import CMMCore
>>> CMMCore.__init__.__module__
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'wrapper_descriptor' object has no attribute '__module__'. Did you mean: '__reduce__'? Note that methods usually provide a >>> import griffe
>>> griffe.GriffeLoader.__init__.__module__
'_griffe.loader' So, without the >>> import inspect
>>> from pymmcore import CMMCore
>>> inspect.getmodule(CMMCore.__init__) ...which returns None, so it falls back again by obtaining the module from the runtime object tree it built while loading the package. Basically, it gets Then it compares the two, and sees that Finally, Griffe wrongly computes the following target path for the alias, Phew. So, what do we learn from this? First, inspection of runtime objects is not trivial 😂 But this I already knew. Second, and more importantly, I see two things than can be improved in Griffe, while avoiding special cases:
To explain the second point, Griffe actually tries to trust objects when they tell it about their parent module. Since the case where an object from a private module says its parent is the equivalent public module is "common" (declared in I mentioned special cases above because Griffe actually has one for pymmcore (from previous tickets): _cyclic_relationships = {
("os", "nt"),
("os", "posix"),
("numpy.core._multiarray_umath", "numpy.core.multiarray"),
("pymmcore._pymmcore_swig", "pymmcore.pymmcore_swig"),
} # Special cases: break cycles.
if (parent_module_path, child_module_path) in _cyclic_relationships:
return None ...but here it's in reverse, parent module path is Finally, it would also have helped if TL;DR - Griffe thinks that the methods of Footnotes
|
thank you so much! this is all suuuper helpful, and I really appreciate your time. also, no need to qualify the word lie 😄 indeed, it's a big fat lie.
fwiw, I did also play with moving the stubs a
fascinating... must be a result of how swig wraps methods
mind pointing me to the code in griffe that checks this and falls back to inspect? might want to poke around there a bit.
in terms of "repo control", I can certainly contribute improvements there. I suspect that the core problem here though probably relates to swig itself. Fwiw, my extremely naive expectation here (or one that I imagined was achievable via some configuration) was that once griffe encountered a stub object for the I'm going to take some time to dig more carefully through your response here, and will reply with some more opinions later as to the "best" longer term fix. |
also, would you prefer to move this topic to griffe? |
Yes, that makes sense: Griffe's logic for finding and merging stubs works fine, but the logic for inspecting runtime objects does not (in this case at least). It causes the stubs-merging code to fail and silently skip the CMMCore class (because aliases can't be resolved when merging method overloads). If we fix the inspection logic, your stubs will happily get merged 🙂
Of course. The crux of it is here: griffe/src/griffe/agents/nodes/_runtime.py Line 236 in ce186a4
This function is used to decide whether to alias an object, or inspect it in place and set it as a member of the current object in the tree. It is used by the inspector, here: griffe/src/griffe/agents/inspector.py Line 233 in ce186a4
That's a completely valid point! And we have an option for that:
Nah that's fine 👍 |
Also, based on my notes, I initially decided that supporting |
Actually yeah I'll move the issue to Griffe 👍 |
I took a couple slow days, so I guess I was energized enough to actually write down my whole investigation 😂 No problem at all, I like hunting bugs 🙂 If |
minor note about swig while looking into this. I was at least able to determine what is "special" about the few SWIGINTERN PyMethodDef SwigPyBuiltin__CMMCore_methods[] = {
{ "noop", (PyCFunction)(void(*)(void))_wrap_CMMCore_noop, METH_STATIC|METH_NOARGS, "noop()" },
{ "enableFeature", (PyCFunction)(void(*)(void))_wrap_CMMCore_enableFeature, METH_STATIC|METH_VARARGS, "\n"
"enableFeature(char const * name, bool enable)\n"
"\n"
"Parameters\n"
"----------\n"
"name: char const *\n"
"enable: bool\n"
"\n"
"" },
{ "isFeatureEnabled", (PyCFunction)(void(*)(void))_wrap_CMMCore_isFeatureEnabled, METH_STATIC|METH_O, "\n"
"isFeatureEnabled(char const * name) -> bool\n"
"\n"
"Parameters\n"
"----------\n"
"name: char const *\n"
"\n"
"" },
{ "loadDevice", _wrap_CMMCore_loadDevice, METH_VARARGS, "\n"
"loadDevice(CMMCore self, char const * label, char const * moduleName, char const * deviceName)\n"
"\n"
"Parameters\n"
"----------\n"
"label: char const *\n"
"moduleName: char const *\n"
"deviceName: char const *\n"
"\n"
"" },
all the other are simply defined as SWIGINTERN PyObject *_wrap_CMMCore_loadDevice(PyObject *self, PyObject *args) { ... so, it would appear that swig isn't generating a proper method definition 🤷 |
Ah, super interesting, thanks! So, supposedly, if it generated all the methods the same way, casting them with |
yep, I'm out of my depth as well... however, it does seem based on that C-Python docs for In [8]: from types import BuiltinFunctionType
In [9]: isinstance(pymmcore.CMMCore.enableFeature, BuiltinFunctionType)
Out[9]: True
In [10]: isinstance(pymmcore.CMMCore.__init__, BuiltinFunctionType)
Out[10]: False
In [11]: type(pymmcore.CMMCore.enableFeature)
Out[11]: <class 'builtin_function_or_method'>
In [12]: type(pymmcore.CMMCore.__init__)
Out[12]: <class 'wrapper_descriptor'> relevant sections of the swig source code:
I suspect that from SWIG's perspective, it's different because they don't need to wrap the first argument, unlike for a classmethod or regular method |
…der module paths to be equivalent even with arbitrary private components When deciding whether an object should be aliased during dynamic analysis, we previously said "no" only if the parent module path and the child module path were equal after removing any leading underscores. In short, `_a` is equal to `a`, and `a.b` is equal to `_a.b`. But in some cases (see mentioned issue), path components other than the first have leading underscores or not. For example: `a.b` and `a._b`. These cases where not supported, and would result in objects being aliased instead of inspected in-place, later causing alias resolution issues (cyclic aliases, pointing at themselves). Now we decide that paths are equivalent if all their components stripped from leading underscores are equal. It means that cases like `a._b.c` vs. `a.b._c` are supported, and an object analyzed in one of them but declared in the other will be inspected in-place and not aliased. Even though this specific case is weird (like many other possible cases), we suppose that users know what they are doing with their module layout / public-private API. Issue-296: #296
I've pushed two bug fixes 😌 They will make it to the next 0.x version. I believe we can close this now 🙂 |
thanks as always! Will try it out later today. |
wow, great work! definitely fixes this issue 👍 |
You confirm this works well even with |
Perfect, thanks for confirming! |
Description of the bug
Hey @pawamoy. I'm back working on updating docs for one of my packages to work with the latest griffe. (I've been using a griffe fork for this specific package back since #115 and I'm trying to get off of it. This isn't exactly the same issue, but basically i'm still struggling to get griffe/mkdocstrings to find and inject the stubs provided in a pyi file. I just spent an hour or two trying to debug it, and i'm stumped. as best as I can tell, griffe does find all the members/methods, but mkdocs strings only shows 3 of them.
To Reproduce
screenshot
This class has a ton of methods, all of which are documented in the stub file, but only a handful are found.
For what it's worth, the stubs i want it to find are at the top level
__init__.pyi
(however, I have also tried moving them to other places including
_pymmcore_swig.pyi
, etc...If you want to quickly see the stubs, they are here
Expected behavior
find all the methods
Environment information
The text was updated successfully, but these errors were encountered: