Skip to content

Factor out class generator mechanics - #12921

Closed
nstarman wants to merge 15 commits into
astropy:mainfrom
nstarman:subclass-factor-out
Closed

nstarman wants to merge 15 commits into
astropy:mainfrom
nstarman:subclass-factor-out

Conversation

@nstarman

@nstarman nstarman commented Mar 4, 2022 •

Copy link
Copy Markdown
Member

The class generator is factored out of Masked and Distribution as a meta class.

Fixes #12912

todo:

  • docstrings
  • documentation ?
  • tests? basically covered by Masked and Distribution
  • changelog entry

There's some residual weirdness with __init_subclass__ and a metaclasses that I haven't solved. Solutions / input appreciated.

Checklist for package maintainer(s)

This checklist is meant to remind the package maintainer(s) who will review this pull request of some common things to look for. This list is not exhaustive.

  • Do the proposed changes actually accomplish desired goals?
  • Do the proposed changes follow the Astropy coding guidelines?
  • Are tests added/updated as required? If so, do they follow the Astropy testing guidelines?
  • Are docs added/updated as required? If so, do they follow the Astropy documentation guidelines?
  • Is rebase and/or squash necessary? If so, please provide the author with appropriate instructions. Also see "When to rebase and squash commits".
  • Did the CI pass? If no, are the failures related? If you need to run daily and weekly cron jobs as part of the PR, please apply the Extra CI label.
  • Is a change log needed? If yes, did the change log check pass? If no, add the no-changelog-entry-needed label. If this is a manual backport, use the skip-changelog-checks label unless special changelog handling is necessary.
  • Is this a big PR that makes a "What's new?" entry worthwhile and if so, is (1) a "what's new" entry included in this PR and (2) the "whatsnew-needed" label applied?
  • Is a milestone set? Milestone must be set but astropy-bot check might be missing; do not let the green checkmark fool you.
  • At the time of adding the milestone, if the milestone set requires a backport to release branch(es), apply the appropriate backport-X.Y.x label(s) before merge.

@nstarman nstarman added this to the v5.1 milestone Mar 4, 2022
@nstarman
nstarman requested a review from mhvk March 4, 2022 20:47
@pep8speaks

pep8speaks commented Mar 4, 2022 •

Copy link
Copy Markdown

Hello @nstarman 👋! It looks like you've made some changes in your pull request, so I've checked the code again for style.

Line 80:68: E999 SyntaxError: invalid syntax

Line 25:67: E999 SyntaxError: invalid syntax

Line 50:67: E999 SyntaxError: invalid syntax

Line 207:50: E999 SyntaxError: invalid syntax

Line 20:49: E999 SyntaxError: invalid syntax

Comment last updated at 2022-06-25 20:55:56 UTC

@github-actions

github-actions Bot commented Mar 4, 2022

Copy link
Copy Markdown
Contributor

👋 Thank you for your draft pull request! Do you know that you can use [ci skip] or [skip ci] in your commit messages to skip running continuous integration tests until you are ready?

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really nice! I was a bit doubtful at first when I saw the approach with the metaclass, but it is clear it makes a lot of sense, with __init__ and __call__ both very logically used.

I have not had a chance yet to see what goes wrong...

Comment thread astropy/utils/masked/core.py Resolved
Comment thread astropy/utils/collections.py Resolved
Comment thread astropy/utils/masked/core.py Resolved
Comment thread docs/utils/masked/index.rst Resolved
Comment thread astropy/uncertainty/core.py Resolved
@nstarman
nstarman force-pushed the subclass-factor-out branch 2 times, most recently from 8a4a1b4 to 683fa13 Compare March 5, 2022 19:51
@nstarman

nstarman commented Mar 5, 2022 •

Copy link
Copy Markdown
Member Author

I'm getting a very weird error that I can reproduce on pytest / tex, but not in Jupyter, where the example correctly finds the MRO and makes a MaskedNDArray.

Solved!

@nstarman
nstarman force-pushed the subclass-factor-out branch from 683fa13 to 9a0b438 Compare March 9, 2022 16:23
@nstarman
nstarman marked this pull request as ready for review March 9, 2022 16:55
@nstarman
nstarman requested a review from mhvk March 9, 2022 16:56
@nstarman
nstarman requested a review from adrn March 23, 2022 16:42
@nstarman

Copy link
Copy Markdown
Member Author

@adrn, this might be used to solve JAX/Dask/etc + units.

Quantity becomes like Distribution or Masked and what people actually use is NumPyQuantity or DaskQuantity where the class is a multiple inheritance of Quantity and the array class.
One of the challenges will be re-implementing the __array_function__ overrides in a more array-class agnostic way.

@nstarman
nstarman force-pushed the subclass-factor-out branch 3 times, most recently from a04da84 to ebd08a5 Compare March 31, 2022 21:19
@nstarman

Copy link
Copy Markdown
Member Author

The RTD error is real, but all I've done is import abc.ABCMeta and use it as a base class. Hm....

@nstarman

Copy link
Copy Markdown
Member Author

@pllim I don't think putting the wrapper meta class in utils/collections.py is the best place, but there isn't obviously a better one. Should we make a new file utils/metaclasses.py or something?

@pllim

pllim commented Mar 31, 2022

Copy link
Copy Markdown
Member

utils/metaclasses.py

I guess it doesn't hurt...

@mhvk

mhvk commented Apr 1, 2022

Copy link
Copy Markdown
Contributor

What about astropy/utils/codegen.py? That seems fairly appropriate since we are creating new classes!

@pllim

pllim commented Apr 1, 2022

Copy link
Copy Markdown
Member

I think codegen is too cryptic... We don't want people to think it has AI code or something... 😸

@nstarman

nstarman commented Apr 1, 2022 •

Copy link
Copy Markdown
Member Author

Oops. I can move it from codegen.

I called the new file abcs.py. I didn't want to call it abc.py, like the builtin, so the plural seemed fine.

@nstarman
nstarman force-pushed the subclass-factor-out branch from 4f8e40b to 63a28de Compare April 1, 2022 16:17
@nstarman

nstarman commented Apr 1, 2022 •

Copy link
Copy Markdown
Member Author

@mhvk @adrn @pllim. What do you think the next steps are? Do we need a set of tests for this beyond the tests in Masked and Distribution? More documentation? I don't think there should be anything yet in the narrative docs. Perhaps when this is used for Quantity then we'll write something up.

Comment thread astropy/utils/abcs.py Resolved
nstarman added 8 commits June 25, 2022 16:54
Signed-off-by: Nathaniel Starkman (@nstarman) <[email protected]>
Signed-off-by: Nathaniel Starkman (@nstarman) <[email protected]>
Signed-off-by: Nathaniel Starkman (@nstarman) <[email protected]>
Signed-off-by: Nathaniel Starkman (@nstarman) <[email protected]>
Signed-off-by: Nathaniel Starkman (@nstarman) <[email protected]>
Signed-off-by: Nathaniel Starkman (@nstarman) <[email protected]>
Signed-off-by: Nathaniel Starkman (@nstarman) <[email protected]>
Signed-off-by: Nathaniel Starkman (@nstarman) <[email protected]>
@nstarman
nstarman force-pushed the subclass-factor-out branch from 1f8c895 to be3f9e6 Compare June 25, 2022 20:55

@mhvk mhvk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking very nice! It seems clear to me that having subclasses of FactoryMeta is the right solution.

With that, one now can get to bikeshedding about names... My general sense would be that the metaclass names do not have to have the _inmix prefix, and in principle could even be public.

One a bit larger thing is that I worry at times a little too much moved to the metaclasses. I also have a sense some things could be simpler (the weak dicts being a particular example). But I really need a bit more time to look through this properly! As I'm off tomorrow, I'm not quite sure when I'll have time, but I'll try! So, I'll send the comments I have so far (somewhat random, I fear).


def __init_subclass__(cls, base_cls=None, data_cls=None, **kwargs):
"""Register a Masked subclass.
def from_unmasked(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel this should remain as a class method on Masked proper - after all, it was a public method, and currently it can be used from a masked instance.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's reasonable to not want to change the API. I'll move it back.

mask = getattr(self.mask, method)(*args, **kwargs)

result = self.from_unmasked(data, mask, copy=False)
result = type(self).from_unmasked(data, mask, copy=False)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping from_unmasked as a class method, this change would not be necessary.

class FactoryMeta(ABCMeta):
"""Meta-class for generating classes of a data class.

Classes derived from this metaclass.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete sentence

name: str,
bases: tuple[type, ...],
namespace: dict[str, Any],
base_cls: type | None=None,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add a * so base_cls and data_cls are keyword only (works by default, of course, but maybe clearer?)


# inject data_cls into namespace before `__new__` so
# `__init_subclass__` can use it.
namespace["__intomixclass__"] = data_cls

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't __init_subclass__ get whatever is passed in to the class anyway?

"""Astropy's ABCs."""

from __future__ import annotations
from ast import Raise

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opening this file in emacs showed wiggly red lines: Raise is not used.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. yes. One of the less desirable side effects of VSCode auto-imports. I must have typed a Raise with capital and it tried to help. I try to pair VSCode with flake's detect-unused-imports in pre-commit to catch these. But we don't have that enabled... 🤔

import sys
import weakref
from abc import ABCMeta
from typing import Sequence, Dict, Any, Tuple, TypeVar, Union

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Union is not used.

namespace: dict[str, Any],
base_cls: type | None=None,
data_cls: type | None=None,
**_: Any

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not **kwargs and actually pass them on? In case someone inherits and puts something in between.

self.__inmixbase__ = self
"""Base class for generating new intermixed classes."""

self.__inmixed_base_classes = weakref.WeakKeyDictionary()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using WeakKeyDictionary here seems to add a lot of complication for what seems very little gain: generally, if, say, a Masked subclass is created, it will happen again soon enough. Similarly for SkyOffsetFrame and Distribution. Plus, these dictionaries cannot get very big. I'd suggest to just use dict


For subclasses of these unmasked classes, masked counterparts can be generated.
"""
masked_cls = self._inmix_get_subclass(data.__class__)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just

return super()._inmix_make_instance(data, mask, copy=copy)

Alternatively, keep it as a class method _get_masked_instance, which can call this directly. Or, perhaps even better, in that case just call cls(data, mask, copy=copy), which I think will do the right thing.

@github-actions github-actions Bot added the Close? Tell stale bot that this issue/PR is stale label Nov 23, 2022
@github-actions

Copy link
Copy Markdown
Contributor

Hi humans 👋 - this pull request hasn't had any new commits for approximately 4 months. I plan to close this in 30 days if the pull request doesn't have any new commits by then.

In lieu of a stalled pull request, please consider closing this and open an issue instead if a reminder is needed to revisit in the future. Maintainers may also choose to add keep-open label to keep this PR open but it is discouraged unless absolutely necessary.

If this PR still needs to be reviewed, as an author, you can rebase it to reset the clock.

If you believe I commented on this pull request incorrectly, please report this here.

@astrofrog astrofrog modified the milestones: v5.2, v5.3 Dec 8, 2022
@github-actions github-actions Bot added the closed-by-bot Closed by stale bot label Dec 24, 2022
@github-actions

Copy link
Copy Markdown
Contributor

I'm going to close this pull request as per my previous message. If you think what is being added/fixed here is still important, please remember to open an issue to keep track of it. Thanks!

If this is the first time I am commenting on this issue, or if you believe I closed this issue incorrectly, please report this here.

@github-actions github-actions Bot closed this Dec 24, 2022
@nstarman nstarman added keep-open and removed Close? Tell stale bot that this issue/PR is stale labels Dec 31, 2022
@nstarman nstarman reopened this Dec 31, 2022
@nstarman nstarman removed the closed-by-bot Closed by stale bot label Jan 14, 2023
@saimn saimn modified the milestones: v5.3, v6.0 Apr 21, 2023
@mhvk

mhvk commented Apr 26, 2023

Copy link
Copy Markdown
Contributor

@nstarman - working on Distribution a bit, I'm realizing that it could use some of the things Masked had, like new class generation with Distribution(class) - easiest would be through this PR. Do you want to revive it?

@nstarman

nstarman commented May 1, 2023 •

Copy link
Copy Markdown
Member Author

I am interested in doing that. One major drawback I found is that we are missing an underlying piece of the mechanics: a resolution order of the class generation.

Let's say we make class InterpolatedCoordinate that adds an affine parameter to a Coordinate so that it describes a path. Then InterpolatedCoordinate(SkyOffsetFrame(...)) needs to understand how to reconstruct the dynamic class from the original type. I think it's the same as the MRO. Unifying the class generation framework is definitely better than the alternative, but it has some tricky subtleties.

@mhvk

mhvk commented May 7, 2023

Copy link
Copy Markdown
Contributor

Yes, indeed, it is all a bit tricky. For Distribution, I have been finding that it is almost impossible to do it with just a single class - it seems one needs something on top (mostly for __getitem__ and __setitem__, perhaps also __new__) and something just above ndarray). So, maybe it is best to keep the default implementation really simple and just allow subclass factories to override.

p.s. It may also be that SkyOffsetFrame just doesn't quite fit; need to look at the code again!

@nstarman nstarman closed this May 17, 2023
@nstarman

Copy link
Copy Markdown
Member Author

Not that this isn't useful, I just think a more studied approach is needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Future: factor out new class generation from Distribution and Masked

7 participants