Skip to content

Simplify OrderedDescriptor[Container] #11094

Closed

Description

@mhvk

The work to remove metaclasses in favour of __init_subclass__ at #11090 reminded me that @embray had mentioned that this would become possible, and that similar changes would also allow one to simplify OrderedDecriptor:

Once Python 3.6 is out I would encourage looking for bits in Astropy
that can be simplified by these PEPs. The most obvious that comes to
mind is the OrderedDescriptor class, and its associated
OrderedDescriptorContainer class (that a class must subclass in order
for OrderedDescriptors to work on it). PEP 520 makes
OrderedDescriptor trivial, and obviates the need for it.

This is a reminder for us to look at this.

Details

The combination of OrderedDescriptor and OrderedDescriptorContainer provided two functionalities:

It allowed setting a "name" attribute on each OrderedDescriptor when the class is defined. This can be replaced with the __set_name__ magic method introduced in Python 3.6 by PEP 487. For example, given:

class ExampleDecorator(OrderedDescriptor):
    _name_attribute_ = 'name'

this can now be written:

class ExampleDescriptor:
    def __set_name__(self, owner, name):
        self.name = name

when defining a class using this descriptor, e.g. like

class MyClass:
    descr1 = ExampleDescriptor()
    descr2 = ExampleDescriptor()

at class definition time __set_name__ will be called on each ExampleDescriptor with the name it was assigned to in the class body, resulting in:

>>> MyClass.descr1.name
'descr1'
>>> MyClass.descr2.name
'descr2'

The other functionality of OrderedDescriptor + OrderedDescriptorContainer is that it would set an attribute on the class containing an OrderedDict of all instances of some OrderedDescriptor subclass. For example, given

class ExampleDescriptor(OrderedDescriptor):
    _class_attribute_ = '_examples_'

this can now be implemented in the class which contains the ExampleDescriptors (without using OrderedDescriptor or OrderedDescriptorContainer at all) by implementing __init_subclass__ on a base class:

class MyBaseClass:
    _examples_ = {}

    def __init_subclass__(cls):
        cls._examples_ = {k, v for k, v in cls.__dict__.items()
                                      if isinstance(v, ExampleDescriptor)}

Now use this base class and put some ExampleDescriptors in the class body:

class MyClass(MyBaseClass):
    descr1 = ExampleDescriptor()
    descr2 = ExampleDescriptor()

Resulting in:

>>> MyClass._examples_
{'descr1': <__main__.ExampleDescriptor at 0x7f4e2bfb44e0>,
 'descr2': <__main__.ExampleDescriptor at 0x7f4e2bfb4ef0>}

Finally, a more advanced feature of OrderedDescriptorContainer was if you set _inherit_descriptors_ = True in classes which used it. This would cause the descriptors in its _class_attribute_ to be inherited from base classes.

If this functionality is needed it can also be achieved with a slightly more advanced __init_subclass__:

class MyBaseClass:
    _examples_ = {}

    def __init_subclass__(cls):
        for basecls in cls.__mro__:
            if hasattr(basecls, '_examples_') and isinstance(basecls._examples_, dict):
                cls._examples_ = dict(basecls._examples_)
                break
        else:
            cls._examples_ = {}

        cls._examples_.update(
            {k, v for k, v in cls.__dict__.items()
             if isinstance(v, ExampleDescriptor)})

Then given

class MyClass(MyBaseClass):
    descr1 = ExampleDescriptor()
    descr2 = ExampleDescriptor()

and

class MySubClass(MyClass):
    descr3 = ExampleDescriptor()

The result is:

>>> MySubClass._examples_
{'descr1': <__main__.ExampleDescriptor at 0x7f4e2bfb44e0>,
 'descr2': <__main__.ExampleDescriptor at 0x7f4e2bfb4ef0>,
 'descr3': <__main__.ExampleDescriptor at 0x7f4e2bf9b550>}

Activity

  1. embray commented on Nov 27, 2020

    @embray
    Member

    Cool, thanks for the reminder. Indeed, I believe this is still the case. In fact, class __dict__s now preserve the order that values are added to them, which means that when class bodies are executed the class __dict__ entries will be in the same order as the class body.

  2. embray commented on Nov 27, 2020

    @embray
    Member

    Combined also with the __set_name__ hook from PEP 487 I think OrderedDescriptor could even be deprecated/removed entirely, though I would need to do a quick review of how they're actually being used to be sure.

  3. added a commit that references this issue on Nov 27, 2020
  4. embray commented on Nov 27, 2020

    @embray
    Member

    I'm updating the description of this issue with details on how to replace OrderedDescriptor in a class, so I can reference this issue in the deprecation message.

  5. added 8 commits that reference this issue on Nov 27, 2020
  6. 42 remaining items

  7. added 7 commits that reference this issue on Feb 28, 2021
  8. added 8 commits that reference this issue on Mar 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions