Skip to content
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

DEPR: deprecate direct mutations of TableElement.fields by making it an owned list #16316

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

neutrinoceros
Copy link
Contributor

@neutrinoceros neutrinoceros commented Apr 20, 2024

Description

Fixes #16314

The deprecation process is quite involved since we need to raise a warning for external usage of, say, table.fields.append, but still allow this operation to be performed silently internally.
My solution is to make TableElement.fields a "owned list" (my wording), that is, a list that has an owner object (namely the TableElement instance it belongs to), which is the only one allowed to directly mutate it (using a private keyword argument _owned=True). Mutations from other actors are still allowed as a temporary measure, but now raise a DeprecationWarning.

The commit history is meant to allow reviewing my process step by step. I'll also add some comments to the diff if I can help anticipate some questions from reviewers.

  • By checking this box, the PR author has requested that maintainers do NOT use the "Squash and Merge" button. Maintainers should respect this when possible; however, the final decision is at the discretion of the maintainer that merges the PR.

TODO

  • comment diff
  • handle magic methods too (__iadd__, __imul__, __setitem__ and __delitem__)
  • Add docstrings to _OwnedList and _OwnedHomegeneousList (inheritance order matters !)
  • open follow up issue to keep track of what to do when the deprecation ends (or we could just keep API: Make fields property immutable #16314 opened while the deprecation is on-going ?)

Copy link

Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.

  • 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 instructions for rebase and squash.
  • 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. Codestyle issues can be fixed by the bot.
  • 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?
  • 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.

@github-actions github-actions bot added the utils label Apr 20, 2024
Copy link

👋 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?

@neutrinoceros neutrinoceros force-pushed the io/votable/depr/direct_mutations_TableElement_data branch from 1421f5f to fef1d87 Compare April 20, 2024 09:01
@neutrinoceros neutrinoceros changed the title DEPR: depracte direct mutations of TableElement.fields by making it an owned list DEPR: deprecate direct mutations of TableElement.fields by making it an owned list Apr 20, 2024
@neutrinoceros neutrinoceros force-pushed the io/votable/depr/direct_mutations_TableElement_data branch from fef1d87 to 27d3bca Compare April 20, 2024 09:02
@neutrinoceros neutrinoceros changed the title DEPR: deprecate direct mutations of TableElement.fields by making it an owned list DEPR: deprecate direct mutations of TableElement.fields by making it an owned list Apr 20, 2024
@neutrinoceros neutrinoceros force-pushed the io/votable/depr/direct_mutations_TableElement_data branch 2 times, most recently from 464abe7 to 171dcc5 Compare April 20, 2024 09:43
return super().__setitem__(key, value)

@_locked_mutator
def __delitem__(self, key: SupportsIndex | slice) -> None:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A note on type annotations: I didn't intend to add them, but got them for free from my editor's supercharged auto complete.

return inner


class _OwnedList(list):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's not intended as a new feature, which is why I'm making it private by default, but I could just as easily make this class public, or move it to a different module, closer to where it's used.

@@ -819,40 +819,41 @@ def test_select_columns_binary(format_):
assert table.colnames == ["string_test", "string_test_2", "unicode_test"]


def table_from_scratch():
def test_direct_fields_mutation():
Copy link
Contributor Author

Choose a reason for hiding this comment

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

here the diff looks fishy because it's collapsing 2 independent changes

  • I added a new test whose preamble is based on table_from_scratch
  • I removed table_from_scratch when I found that it wasn't used anywhere

I could split this cleanup into its own PR if desired.

@@ -2658,13 +2674,12 @@ def _resize_strategy(self, size):
return int(np.ceil(size * RESIZE_AMOUNT))

def add_field(self, field: Field) -> None:
self.fields.append(field)
self.all_fields.append(field)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I found that this line was not needed and in fact caused some bugs that are revealed by existing tests once we start using add_field instead of direct mutations. This is a regression from #15959, which can easily be fixed independently in a -1/+0 separate PR if desired.

@neutrinoceros neutrinoceros force-pushed the io/votable/depr/direct_mutations_TableElement_data branch 3 times, most recently from d4716b1 to 990c546 Compare April 20, 2024 13:58
@neutrinoceros neutrinoceros marked this pull request as ready for review April 20, 2024 14:28
@neutrinoceros neutrinoceros force-pushed the io/votable/depr/direct_mutations_TableElement_data branch from 990c546 to d672492 Compare April 21, 2024 06:59
@neutrinoceros neutrinoceros force-pushed the io/votable/depr/direct_mutations_TableElement_data branch from d672492 to dae25d7 Compare April 21, 2024 07:25
@pllim pllim added this to the v7.0.0 milestone Apr 22, 2024
@eerovaher
Copy link
Member

Having a subclass that must disable a large number of its inherited methods doesn't look right. Have you considered implementing HomogeneousTuple instead?

@neutrinoceros
Copy link
Contributor Author

Unfortunately mutability is still needed internally, so I don't think a tuple would suitably replace the existing data structure.

@pllim pllim added the API change PRs and issues that change an existing API, possibly requiring a deprecation period label Apr 22, 2024
@eerovaher
Copy link
Member

I'm not sure what the best solution here would be, but disabling this many methods makes it apparent that you don't want this to be a list, so why would you inherit from it?

@neutrinoceros
Copy link
Contributor Author

I still want it to be a list, I just don't want it mutated by anyone who's not the container object.

@eerovaher
Copy link
Member

I still want it to be a list, I just don't want it mutated...

So you don't want it to be a list.

@neutrinoceros
Copy link
Contributor Author

Well to take a different angle, we're talking about an attribute that's both a list (mutable) and a property (read-only), which one could argue is already a contradiction, but that ship sailed long ago. I don't think however that it's fundamentally contradictory, the language just doesn't offer a simple way to build such properties.

@eerovaher
Copy link
Member

Couldn't you have a private list and have the users access its contents through a public property that converts the list to a tuple? If the list is private then there is no need to deprecate any of its methods.

@neutrinoceros
Copy link
Contributor Author

neutrinoceros commented Apr 22, 2024

Whatever we do needs to go through a gradual deprecation process. We can't make public API private or broken in one go.

@eerovaher
Copy link
Member

A vanilla tuple wouldn't be appropriate immediately, it would be necessary to implement some weird subclass of tuple that would be aware of the private list it is representing and it should also have list methods that would emit deprecation warnings. That does not sound more appealing than the weird list subclass you are creating here. The difference is that such a tuple subclass could be removed when the deprecation period is over, whereas your _OwnedList is something we would be stuck with.

@neutrinoceros
Copy link
Contributor Author

Couldn't you have a private list and have the users access its contents through a public property that converts the list to a tuple?

The difference is that such a tuple subclass could be removed when the deprecation period is over

I think I see your point. However it doesn't feel right to me that the proposed intermediate step involves a "mutable tuple" of sorts. I also think such a class would end up looking a lot like _OwnedList. Actually, now that I think about it, wouldn't it make sense to keep _OwnedList as the intermediate state and then implement your proposed final state when we complete the deprecation cycle ? The one downside I see with this approach is that the public attribute would move from being a list to a tuple over a single commit, but that's also true with your proposal taken as a whole, and the alternative (having an intermediate state where it's both) seems worse (?).

@eerovaher
Copy link
Member

The important thing for me is that we don't end up with a permanent immutable list subclass. Implementing something strange for the transition period can be acceptable as long as it is temporary and I don't really care about details beyond that.

@neutrinoceros
Copy link
Contributor Author

ok then I'll add a note to #16314 and rework the warning message here so that it avoids making promises about which kind of error will replace it in the future.

@neutrinoceros
Copy link
Contributor Author

@tomdonaldson I think you're in a great position to review this, but no rush, I'm just letting you know :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
API change PRs and issues that change an existing API, possibly requiring a deprecation period io.votable utils
Projects
None yet
Development

Successfully merging this pull request may close these issues.

API: Make fields property immutable
3 participants