Skip to content

Calling help executes @classmethod @property decorated methods #89519

@randolf-scholz

Description

@randolf-scholz
BPO 45356
Nosy @rhettinger, @terryjreedy, @ambv, @berkerpeksag, @serhiy-storchaka, @wimglenn, @corona10, @pablogsal, @akulakov, @sirosen, @randolf-scholz, @AlexWaygood
PRs
  • bpo-45356: fix incorrect access of class property in pydoc and inspect #29239
  • Files
  • classmethod_property.py
  • classmethod_property.py
  • ClassPropertyIdea.py
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = 'https://github.com/rhettinger'
    closed_at = None
    created_at = <Date 2021-10-03.19:45:55.963>
    labels = ['type-bug', '3.11']
    title = 'Calling `help` executes @classmethod @property decorated methods'
    updated_at = <Date 2022-02-17.20:03:40.349>
    user = 'https://github.com/randolf-scholz'

    bugs.python.org fields:

    activity = <Date 2022-02-17.20:03:40.349>
    actor = 'rhettinger'
    assignee = 'rhettinger'
    closed = False
    closed_date = None
    closer = None
    components = []
    creation = <Date 2021-10-03.19:45:55.963>
    creator = 'randolf.scholz'
    dependencies = []
    files = ['50325', '50326', '50344']
    hgrepos = []
    issue_num = 45356
    keywords = ['patch']
    message_count = 23.0
    messages = ['403109', '403110', '403111', '403504', '403505', '403578', '403582', '403611', '403613', '403653', '403699', '403713', '405100', '405115', '405121', '405142', '405143', '406600', '406605', '406606', '407493', '407499', '413451']
    nosy_count = 13.0
    nosy_names = ['rhettinger', 'terry.reedy', 'grahamd', 'lukasz.langa', 'berker.peksag', 'serhiy.storchaka', 'wim.glenn', 'corona10', 'pablogsal', 'andrei.avk', 'sirosen0', 'randolf.scholz', 'AlexWaygood']
    pr_nums = ['29239']
    priority = 'normal'
    resolution = None
    stage = 'patch review'
    status = 'open'
    superseder = None
    type = 'behavior'
    url = 'https://bugs.python.org/issue45356'
    versions = ['Python 3.11']

    Linked PRs

    Activity

    randolf-scholz

    randolf-scholz commented on Oct 3, 2021

    @randolf-scholz
    MannequinAuthor

    I noticed some strange behaviour when calling help on a class inheriting from a class or having itself @classmethod @Property decorated methods.

    from time import sleep
    from abc import ABC, ABCMeta, abstractmethod
    
    class MyMetaClass(ABCMeta):
        @classmethod
        @property
        def expensive_metaclass_property(cls):
            """This may take a while to compute!"""
            print("computing metaclass property"); sleep(3)
            return "Phew, that was a lot of work!"
    
        
    class MyBaseClass(ABC, metaclass=MyMetaClass):
        @classmethod
        @property
        def expensive_class_property(cls):
            """This may take a while to compute!"""
            print("computing class property .."); sleep(3)
            return "Phew, that was a lot of work!"
        
        @property
        def expensive_instance_property(self):
            """This may take a while to compute!"""
            print("computing instance property ..."); sleep(3)
            return "Phew, that was a lot of work!"
    
    class MyClass(MyBaseClass):
        """Some subclass of MyBaseClass"""
        
    help(MyClass)

    Calling help(MyClass) will cause expensive_class_property to be executed 4 times (!)

    The other two properties, expensive_instance_property and expensive_metaclass_property are not executed.

    Secondly, only expensive_instance_property is listed as a read-only property; expensive_class_property is listed as a classmethod and expensive_metaclass_property is unlisted.

    The problem is also present in '3.10.0rc2 (default, Sep 28 2021, 17:57:14) [GCC 10.2.1 20210110]'

    Stack Overflow thread: https://stackoverflow.com/questions/69426309

    added
    3.9only security fixes
    3.10only security fixes
    type-bugAn unexpected behavior, bug, or error
    on Oct 3, 2021
    randolf-scholz

    randolf-scholz commented on Oct 3, 2021

    @randolf-scholz
    MannequinAuthor

    I updated the script with dome more info. The class-property gets actually executed 5 times when calling help(MyClass)

    Computing class property of <class '__main__.MyClass'> ...DONE!
    Computing class property of <class '__main__.MyClass'> ...DONE!
    Computing class property of <class '__main__.MyClass'> ...DONE!
    Computing class property of <class '__main__.MyBaseClass'> ...DONE!
    Computing class property of <class '__main__.MyClass'> ...DONE!
    
    AlexWaygood

    AlexWaygood commented on Oct 3, 2021

    @AlexWaygood
    Member
    terryjreedy

    terryjreedy commented on Oct 8, 2021

    @terryjreedy
    Member

    Randolf, what specific behaviors do you consider to be bugs that should be fixed. What would a test of the the changed behavior look like?

    This should perhaps be closed as a duplicate of bpo-44904. Randolf, please check and say what you thing.

    terryjreedy

    terryjreedy commented on Oct 8, 2021

    @terryjreedy
    Member

    On current 3.9, 3.10, 3.11, on Windows running in IDLE, I see
    computing class property ..
    computing class property ..
    computing class property ..
    computing class property ..
    computing class property ..
    Help ...

    added
    3.11only security fixes
    and removed
    3.9only security fixes
    3.10only security fixes
    on Oct 8, 2021
    randolf-scholz

    randolf-scholz commented on Oct 10, 2021

    @randolf-scholz
    MannequinAuthor

    @terry I think the problem boils down to the fact that @classmethod @property decorated methods end up not being real properties.

    Calling MyBaseClass.__dict__ reveals:

    mappingproxy({'__module__': '__main__',
                  'expensive_class_property': <classmethod at 0x7f893e95dd60>,
                  'expensive_instance_property': <property at 0x7f893e8a5860>,
                  '__dict__': <attribute '__dict__' of 'MyBaseClass' objects>,
                  '__weakref__': <attribute '__weakref__' of 'MyBaseClass' objects>,
                  '__doc__': None,
                  '__abstractmethods__': frozenset(),
                  '_abc_impl': <_abc._abc_data at 0x7f893fb98740>})

    Two main issues:

    1. Any analytics or documentation tool that has special treatment for properties may not identify 'expensive_class_property' as a property if they simply check via isinstance(func, property). Of course, this could be fixed by the tool-makers by doing a conditional check:
    isinstance(func, property) or (`isinstance(func, classmethod)` and `isinstance(func.__func__, property)`
    1. expensive_class_property does not implement getter, setter, deleter. This seems to be the real dealbreaker, for example, if we do
    MyBaseClass.expensive_class_property = 2
    MyBaseClass().expensive_instance_property = 2

    Then the first line erroneously executes, such that MyBaseClass.dict['expensive_class_property'] is now int instead of classmethod, while the second line correctly raises AttributeError: can't set attribute since the setter method is not implemented.

    randolf-scholz

    randolf-scholz commented on Oct 10, 2021

    @randolf-scholz
    MannequinAuthor

    If fact, in the current state it seem that it is impossible to implement real class-properties, for a simple reason:

    descriptor.__set__ is only called when setting the attribute of an instance, but not of a class!!

    import math
    
    class TrigConst: 
        const = math.pi
        def __get__(self, obj, objtype=None):
            print("__get__ called")
            return self.const
        
        def __set__(self, obj, value):
            print("__set__ called")
            self.const = value
            
    
    class Trig:
        const = TrigConst()              # Descriptor instance
    Trig().const             # calls TrigConst.__get__
    Trig().const = math.tau  # calls TrigConst.__set__
    Trig.const               # calls TrigConst.__get__
    Trig.const = math.pi     # overwrites TrigConst attribute with float.
    rhettinger

    rhettinger commented on Oct 10, 2021

    @rhettinger
    Contributor

    I'm merging bpo-44904 into this one because they are related.

    87 remaining items

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

    Metadata

    Metadata

    Labels

    3.11only security fixestype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      Calling `help` executes @classmethod @property decorated methods · Issue #89519 · python/cpython