-
-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Simplify Model __new__ and metaclass #7473
Simplify Model __new__ and metaclass #7473
Conversation
The mypy failure is because my code change allows mypy to infer the type of All test failures except one of the Ubuntu ones can be attributed to this recent change in PyTensor. I am not sure what caused the remaining one, but I'm pretty confident it is unrelated. Edit: Rebased on main branch and added one more set of type hints. |
d203e0f
to
5e08d1e
Compare
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #7473 +/- ##
==========================================
+ Coverage 92.69% 92.74% +0.04%
==========================================
Files 104 104
Lines 17409 17376 -33
==========================================
- Hits 16138 16115 -23
+ Misses 1271 1261 -10
|
5e08d1e
to
383d08e
Compare
Thanks @thomasaarholt, there are still mypy failures, do you have a handle on them or would you like some help? |
@twiecki Thanks! The remaining one is one that I'm not sure on how best to handle:
This stems from the Help much appreciated! |
7e1243e
to
74665c8
Compare
There! @twiecki wanna take a look? I went through the logic related to the above failing mypy error and think I found a good resolution. See commit description of the last two commits. The failing test is unrelated. |
Any idea about that failing test?
|
I assume that if we reran it (maybe you can trigger that?) it would pass. Seeing as it fails only on windows and not Linux/mac, I assumed it was a result of variance due to random sampling. |
I restarted it |
Ran passing everything. Ready for review! |
@thomasaarholt the PR looks good, but since it touches on such as fundamental functionality I've asked for further reviews. You have some conflicts that need to be resolved as well. Thanks for the work so far! |
b6c74de
to
95bcf19
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work @thomasaarholt. The original code was very hard to read, and yours is much better now. I think that your refactor could be extended to a few other places. I left comments in the relevant parts.
@lucianopaz you tagged the wrong person ;) Please see my reply about the |
c367caa
to
95bcf19
Compare
Oops, how embarrassing! Sorry about that 😬
Everything looks fine now. Thanks for the great work! |
@ricardoV94, I remember seeing the same test error on one of your earlier PRs. Was it fixed now? @thomasaarholt, maybe if you rebase on top of the latest main, the test will pass. |
get_context returns an instance of a Model, not a ContextMeta object We don't need the typevar, since we don't use it for anything special
All of these are supported on python>=3.9.
We create a global instance of it within this module, which is similar to how it worked before, where a `context_class` attribute was attached to the Model class. We inherit from threading.local to ensure thread safety when working with models on multiple threads. See pymc-devs#1552 for the reasoning. This is already tested in `test_thread_safety`.
UNSET is the instance of the _UnsetType type. We should be typing the latter here.
We use the new ModelManager.parent_context property to reliably set any parent context, or else set it to None.
We set this directly on the class as a classmethod, which is clearer than going via the metaclass.
The original function does not behave as I expected. In the following example I expected that it would return only the final model, not root. This method is not used anywhere in the pymc codebase, so I have dropped it from the codebase. I originally included the following code to replace it, but since it is not used anyway, it is better to remove it. ```python` @classmethod def get_contexts(cls) -> list[Model]: """Return a list of the currently active model contexts.""" return MODEL_MANAGER.active_contexts ``` Example for testing behaviour in current main branch: ```python import pymc as pm with pm.Model(name="root") as root: print([c.name for c in pm.Model.get_contexts()]) with pm.Model(name="first") as first: print([c.name for c in pm.Model.get_contexts()]) with pm.Model(name="m_with_model_None", model=None) as m_with_model_None: # This one doesn't make much sense: print([c.name for c in pm.Model.get_contexts()]) ```
We only keep the __call__ method, which is necessary to keep the model context itself active during that model's __init__.
In pymc/distributions/distribution.py, this change allows the type checker to infer that `rv_out` can only be a TensorVariable. Thanks to @ricardoV94 for type hint on rv_var.
I originally tried numpy's ArrayLike, replacing Sequence entirely, but then I realized that ArrayLike also allows non-sequences like integers and floats. I am not certain if `values="a string"` should be legal. With the type hint sequence, it is. Might be more accurate, but verbose to use `list | tuple | set | np.ndarray | None`.
…unction We don't want to allow the user to pass a `dims=[None, None]` to our function, but current behaviour set `dims=[None] * N` at the end of `determine_coords`. To handle this, I created a `new_dims` with a larger type scope which matches the return type of `dims` in `determine_coords`. Then I did the same within def Data to support this new type hint.
The only case where dims=[None, ...] is when the user has passed dims=None. Since the user passed dims=None, they shouldn't be expecting any coords to match that dimension. Thus we don't need to try to add any more coords to the model.
95bcf19
to
dbbb9a2
Compare
* Type get_context correctly get_context returns an instance of a Model, not a ContextMeta object We don't need the typevar, since we don't use it for anything special * Import from future to use delayed evaluation of annotations All of these are supported on python>=3.9. * New ModelManager class for managing model contexts We create a global instance of it within this module, which is similar to how it worked before, where a `context_class` attribute was attached to the Model class. We inherit from threading.local to ensure thread safety when working with models on multiple threads. See pymc-devs#1552 for the reasoning. This is already tested in `test_thread_safety`. * Model class is now the context manager directly * Fix type of UNSET in type definition UNSET is the instance of the _UnsetType type. We should be typing the latter here. * Set model parent in init rather than in __new__ We use the new ModelManager.parent_context property to reliably set any parent context, or else set it to None. * Replace get_context in metaclass with classmethod We set this directly on the class as a classmethod, which is clearer than going via the metaclass. * Remove get_contexts from metaclass The original function does not behave as I expected. In the following example I expected that it would return only the final model, not root. This method is not used anywhere in the pymc codebase, so I have dropped it from the codebase. I originally included the following code to replace it, but since it is not used anyway, it is better to remove it. ```python` @classmethod def get_contexts(cls) -> list[Model]: """Return a list of the currently active model contexts.""" return MODEL_MANAGER.active_contexts ``` Example for testing behaviour in current main branch: ```python import pymc as pm with pm.Model(name="root") as root: print([c.name for c in pm.Model.get_contexts()]) with pm.Model(name="first") as first: print([c.name for c in pm.Model.get_contexts()]) with pm.Model(name="m_with_model_None", model=None) as m_with_model_None: # This one doesn't make much sense: print([c.name for c in pm.Model.get_contexts()]) ``` * Simplify ContextMeta We only keep the __call__ method, which is necessary to keep the model context itself active during that model's __init__. * Type Model.register_rv for for downstream typing In pymc/distributions/distribution.py, this change allows the type checker to infer that `rv_out` can only be a TensorVariable. Thanks to @ricardoV94 for type hint on rv_var. * Include np.ndarray as possible type for coord values I originally tried numpy's ArrayLike, replacing Sequence entirely, but then I realized that ArrayLike also allows non-sequences like integers and floats. I am not certain if `values="a string"` should be legal. With the type hint sequence, it is. Might be more accurate, but verbose to use `list | tuple | set | np.ndarray | None`. * Use function-scoped new_dims to handle type hint varying throughout function We don't want to allow the user to pass a `dims=[None, None]` to our function, but current behaviour set `dims=[None] * N` at the end of `determine_coords`. To handle this, I created a `new_dims` with a larger type scope which matches the return type of `dims` in `determine_coords`. Then I did the same within def Data to support this new type hint. * Fix case of dims = [None, None, ...] The only case where dims=[None, ...] is when the user has passed dims=None. Since the user passed dims=None, they shouldn't be expecting any coords to match that dimension. Thus we don't need to try to add any more coords to the model. * Remove unused hack
Description
This PR is a refactor that improves the code that creates models and keeps track of the model contexts that are active at a given moment.
I have previously done some work (#6809) on type hinting the Model class. It was a bit difficult to understand what was going on, and the code was lacking documentation that would help further development. In particular, the code creating the Model class and instance, and keeping track of active contexts was rather hacky.
I am on parental leave, which (maybe ironically) gives me the time to take a look behind the scenes and hopefully make some improvements that give back to the community. I do recognize that this PR comes a bit "unannounced", and since it doesn't fix any major issues or introduce hot features, I don't have any expectations for it to be reviewed soon (or even at all).
Each commit is a logical change with a reasonably descriptive text.
This passes all tests in
tests/model
.Checklist
Type of change
📚 Documentation preview 📚: https://pymc--7473.org.readthedocs.build/en/7473/