docs: documentation overhaul#142
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #142 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 31 31
Lines 1761 1761
=========================================
Hits 1761 1761
☔ View full report in Codecov by Sentry. |
There was a problem hiding this comment.
I started drafting notes for some docs for using app-model in napari and I swear some of it is word for word what you've written here.
I don't fully get contexts but app-model does not keep a registry of contexts? I guess this is why it only updates from an empty context?
app-model/src/app_model/backends/qt/_qaction.py
Lines 171 to 175 in 9463e67
Maybe a note about this and future plans?
The getting_started is really great, logical order and clear explanations (I may be biased because it's similar to how I would have done it 😅). I also found the motivations section useful. (Also like that you can easily get links to headers now)
|
Oh while you're here, I noticed that you can't build a see: https://github.com/napari/napari/pull/4865/files#r1288031823 Is this intended? |
providers and processors
Not yet. It's still up to the end-user to manage their contexts. That was always planned, but just something I wanted to consider further. I do think that a top level
I think that the vscode extensions docs still provide some of the best general motivation on this... (and of course https://napari.org/stable/guides/contexts_expressions.html) but the basic idea is that we need a declarative way to express conditions, so that we can set up various things to happen based on those conditions. In many ways, it overlaps with the concept of signals and slots... but without exposing as much of the underlying business logic to the plugin. For example, you could just literally pass your entire QMainWindow to a plugin and say "go nuts, connect whatever callbacks you want to any of the signals you can find in here"... and then you probably wouldn't need contexts. But, as you can imagine, that could very easily get unruly, and it also exposes a ton of stuff that you now can no longer easily change without breaking usage. Contexts let the application say: "here are the things you, the developer, can query about the state of the application". By declaring special names, like vscode does here (and like I would encourage napari to do as well, like I started here), you do this in a fully declarative and serializable way. So the developer can give you an expression that napari document above was my attempt to capture that in the context of napari, and I'll try to express it here as well but probably in a follow up PR. edit: it would be great to know more specifically what is still confusing about contexts. If, after reading that doc, any questions still come to mind, i'd love to hear them! I don't expect it to be immediately obvious, but i need a little guidance about which bits in general are confusing (whether it's the high level "why do we need this in the first place?" or a low level "how does this get implemented") |
exceptions are almost never intended 😂 please open issues when you find stuff like this! I'm not following napari issues so won't see stuff like that unless you ping me or open an issue.. but I'm more than happy to look into it when you encounter anything unexpected in app-model! |
|
going to merge this as an improvement over what we already have. Will follow up with @lucyleeow's improvement suggestions in new PRs |
Will do thanks. We didn't want to bother you with this one as it wasn't something we needed and we weren't sure of intention. I'll add details to the issue. |
|
I don't find it a bother :) I like knowing if there's something that the tests aren't catching here! |
Thank you for the background, I understood at a high level but the details are useful to know! I was more confused about the implementation. For some of the
class LayerListSelectionContextKeys(ContextNamespace['LayerSel']):
"""Available context keys relating to the selection in a LayerList.
Consists of a default value, a description, and a function to retrieve the
current value from `layers.selection`.
"""
num_selected_layers = ContextKey(
0,
trans._("Number of currently selected layers."),
_len,
)
# TODO: figure out how to move this context creation bit.
# Ideally, the app should be aware of the layerlist, but not vice versa.
# This could probably be done by having the layerlist emit events that
# the app connects to, then the `_ctx` object would live on the app,
# (not here)In what way does the app know about the |
|
@lucyleeow I have superficial answers to these questions but most likely @tlambert03 has more detail.
The
I think this is a class attribute, as I don't see any pydantic inheritance in the
Right now, I think the app does not know about the layerlist at all. It's napari's job (I think in the qtmainwindow?) to connect the right callbacks to update the context. I think that's the bit that is considered not great and that should be fixed by adding a context registry to app-model... |
I guess the question is what is it's purpose? Is it just for type hinting? The other question is a e.g., |
yep, it's entirely just for type hinting. whenever you see something like this, where you have a Generic type that is parameterized by some other type, you can think of it as the outer type "taking" the inner type. Lemme try to explain some of those terms with examples: from typing import Generic, TypeVar
T = TypeVar("T")
class MyContainer(Generic[T]):
...when you see something like this, it means:
Still weird? probably, but the next step is to "parametrize" the generic in a subclass, by replacing the type variable " class MyIntContainer(MyContainer[int]):
...The so, going back to
this tells us that class ContextNamespace(Generic[A]):
def __init__(self, context: MutableMapping) -> None:
...
self._getters: Dict[str, Callable[[A], Any]] = {} # value gettersok: all Why bother with all the type hinting? because when done properly it can make the developer experience really nice by alterting you to possible bugs and errors in your code before you even run anything. It can tell you that the function you're trying to use as to update one of the keys in your Context is not going to work, because it doesn't accept the right type(s). see this bit of the napari docs for a bit more on how the type hinting here gives you type feedback in your IDE is it more trouble than it's worth? perhaps now it is. I imagined that I would be working on this in napari for longer, and that doing this with careful type hinting would be able to provide a very nice developer experience. Now, however, I can see that it does add cognitive burden for those not already intimately familiar with Generic types and parameterization. You can feel free to either ignore it, or even remove it if it feels too cumbersome. |
|
btw, i think the mypy docs on generics and parameterization is better than the python docs: |
|
🤯 it makes so much more sense now, thanks! |
|
oh, and there's one more typing-related step: note how all of the functions def _n_unselected_links(s: LayerSel) -> int:
from napari.layers.utils._link_layers import get_linked_layers
return len(get_linked_layers(*s) - s)let's further look to see where the num_unselected_linked_layers = ContextKey(
0,
trans._("Number of unselected layers linked to selected layer(s)."),
_n_unselected_links,
)well, if you look at the class ContextKey(Name, Generic[A, T]):
def __init__(
self,
default_value: T | __missing = MISSING,
description: str | None = None,
getter: Callable[[A], T] | None = None,
*,
id: str = "", # optional because of __set_name__
) -> None:So now, we see how the return value of that function above might be used in type hinting. Specifically, it's used further down in the class ContextKey(Name, Generic[A, T]):
def __get__(self, obj: ContextNamespace[A], objtype: type) -> T:
# When we get from the object, we return the current value
...(note that So, if you put that all together: def _n_unselected_links(s: LayerSel) -> int:
...
class LayerListSelectionContextKeys(ContextNamespace['LayerSel']):
num_unselected_linked_layers = ContextKey(
0,
"Number of unselected layers linked to selected layer(s).",
_n_unselected_links,
)
And so if someone were to use at least that was the motivation behind the design 🫠 |
|
and (sorry) one more comment: I recognize that this all is rather complicated. I want to clarify that I never intended for anyone to have to understand all of that to just use the system. My primary target for napari developers was just this final usage here: class LayerListSelectionContextKeys(ContextNamespace):
num_unselected_linked_layers = ContextKey(
0,
"Number of unselected layers linked to selected layer(s).",
_n_unselected_links,
)I wanted napari developers to be able to use that relatively straightforward syntax to declare context key names, defaults, descriptions, and getter functions – and to be able to group related context keys in a |

general docs overhaul, with better full-api rendering
@lucyleeow, as one who probably looks/works with the docs here most, I'd be grateful for any feedback you have. I like the general layout better, and it's much more automated, (it does expose a few places where docstrings could be expanded, but happy to follow up on that in another PR)
you can preview the build here: https://app-model--142.org.readthedocs.build/