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

__name__ attribute in typing module #88690

Closed
farcat mannequin opened this issue Jun 28, 2021 · 43 comments
Closed

__name__ attribute in typing module #88690

farcat mannequin opened this issue Jun 28, 2021 · 43 comments
Assignees
Labels
3.10 only security fixes 3.11 only security fixes topic-typing type-bug An unexpected behavior, bug, or error

Comments

@farcat
Copy link
Mannequin

farcat mannequin commented Jun 28, 2021

BPO 44524
Nosy @gvanrossum, @ambv, @serhiy-storchaka, @JelleZijlstra, @pablogsal, @miss-islington, @uriyyo, @BvB93, @Fidget-Spinner
PRs
  • bpo-44524: Add missed __name__ and __qualname__ to typing module objects #27237
  • [3.10] bpo-44524: Add missed __name__ and __qualname__ to typing module objects (GH-27237) #27246
  • bpo-44524: Fix an issue wherein _GenericAlias._name was not properly set for specialforms #27614
  • [3.10] bpo-44524: Fix an issue wherein _GenericAlias._name was not properly set for specialforms (GH-27614) #27632
  • bpo-44524: Make exc msg more useful when subclass from special form #27710
  • [3.10] bpo-44524: Fix cryptic TypeError message when trying to subclass special forms in typing (GH-27710) #27815
  • bpo-44524: Don't modify MRO when inheriting from typing.Annotated #27841
  • bpo-44524: Do not set _name of _SpecialForm without need #27861
  • [3.10] bpo-44524: Do not set _name of _SpecialForm without need (GH-27861) #27871
  • [3.10] bpo-44524: Don't modify MRO when inheriting from typing.Annotated (GH-27841) #27950
  • Files
  • typing_attributes.py: tools to see what items in a module miss given attributes; applied to typing
  • 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/ambv'
    closed_at = None
    created_at = <Date 2021-06-28.14:15:33.897>
    labels = ['type-bug', '3.10', '3.11']
    title = '__name__ attribute in typing module'
    updated_at = <Date 2021-08-28.18:09:48.928>
    user = 'https://bugs.python.org/farcat'

    bugs.python.org fields:

    activity = <Date 2021-08-28.18:09:48.928>
    actor = 'miss-islington'
    assignee = 'lukasz.langa'
    closed = False
    closed_date = None
    closer = None
    components = []
    creation = <Date 2021-06-28.14:15:33.897>
    creator = 'farcat'
    dependencies = []
    files = ['50135']
    hgrepos = []
    issue_num = 44524
    keywords = ['patch']
    message_count = 41.0
    messages = ['396638', '396715', '396725', '396729', '396739', '396778', '397684', '397790', '397795', '397804', '397807', '397818', '397819', '397824', '398945', '398969', '398983', '398986', '398991', '399011', '399015', '399019', '399029', '399081', '399087', '399105', '399107', '399157', '399160', '399164', '399176', '399865', '399918', '399924', '399928', '399929', '400020', '400024', '400283', '400286', '400490']
    nosy_count = 11.0
    nosy_names = ['gvanrossum', 'farcat', 'lukasz.langa', 'serhiy.storchaka', 'JelleZijlstra', 'pablogsal', 'miss-islington', 'uriyyo', 'BvB93', 'kj', 'lars2']
    pr_nums = ['27237', '27246', '27614', '27632', '27710', '27815', '27841', '27861', '27871', '27950']
    priority = 'normal'
    resolution = None
    stage = 'patch review'
    status = 'open'
    superseder = None
    type = 'behavior'
    url = 'https://bugs.python.org/issue44524'
    versions = ['Python 3.10', 'Python 3.11']

    @farcat
    Copy link
    Mannequin Author

    farcat mannequin commented Jun 28, 2021

    I noticed some (perhaps intentional) oddities with the __name__ attribute:

    • typing classes like Any (subclass of _SpecialForm) do not have a __name__ attribute,
    • abstract base classes in typing, like MutableSet do not have a __name__ attribute,
    • 'ChainMap', 'Counter', 'OrderedDict' do not have a __name__ attribute when imported from typing, but do when imported from collections.

    I have written a function to show presence/absence if the name __name__ attribute:

    def split_module_names(module):
        unnamed, named = set(), set()
        for name in dir(module):
            if not name.startswith('_'):
                attr = getattr(module, name)
                try:
                    if hasattr(attr, '__name__'):
                        named.add(name)
                    else:
                        unnamed.add(name)
                except TypeError:
                    pass
        return named, unnamed
    
    import typing
    import collections

    typing_named, typing_unnamed = split_module_names(typing)
    collec_named, collec_unnamed = split_module_names(collections)

    print("typing_unnamed:", typing_unnamed)
    print("collec_named & typing_unnamed:", collec_named & typing_unnamed)

    Is this intentional? It seems a little inconsistent.

    I also found something that sometimes the __name__ attribute does resolve:

    class S(typing.Sized):
            def __len__(self):
                return 0
    
    print("'Sized' in typing_unnamed:", 'Sized' in typing_unnamed)
    print("[t.__name__ for t in S.__mro__]:", [t.__name__ for t in S.__mro__])  # here __name__ is resolved!
    print("getattr(typing.Sized, '__name__', None):", getattr(typing.Sized, '__name__', None))

    printing:

    'Sized' in typing_unnamed: True
    [t.__name__ for t in S.__mro__]: ['S', 'Sized', 'Generic', 'object']
    getattr(typing.Sized, '__name__', None): None

    @farcat farcat mannequin added 3.9 only security fixes type-bug An unexpected behavior, bug, or error labels Jun 28, 2021
    @Fidget-Spinner
    Copy link
    Member

    Is this intentional? It seems a little inconsistent.

    The __name__ attribute is for internal use only. It's subject to change every release along with other implementation details.

    Sorry, I don't really understand what this issue is requesting. Do you want to add the split_module_names function or standardize __name__ or something else? Depending on what you're suggesting the follow up would be different.

    @lars2
    Copy link
    Mannequin

    lars2 mannequin commented Jun 29, 2021

    I was not aware the __name__ attribute is an implementation detail. It is described in the docs: https://docs.python.org/3/reference/datamodel.html.

    I have been using it since python 2.7, for example for logging.

    The function “split_module_names” is just a function to see what items in a module have and do not have a __name__ attribute; thought it might help proceedings.

    If I were to suggest an improvement, it would be that all classes and types (or minimally the abc’s) would have a __name__ attribute, being the name under which it can be imported.
    Also that the abc’s in typing and collections are as similar as possible.

    @Fidget-Spinner
    Copy link
    Member

    Lars, yes you're right that __name__ is documented in datamodel, sorry I wasn't clear in my original message. What I meant was that specifically for the typing module, it's not exposed anywhere in its docs https://docs.python.org/3/library/typing.html.

    If I were to suggest an improvement, it would be that all classes and types (or minimally the abc’s) would have a __name__ attribute, being the name under which it can be imported.

    I think this makes sense. It should be as simple as adding self.__name__ = name or some variant. Note that some types hack their names, such as TypeVar or ParamSpec. So it's not always that __name__ ​== type/class name.

    Also that the abc’s in typing and collections are as similar as possible.
    We strive towards this but it's difficult to get it 100%. The abcs in typing are implemented in pure Python and alias the ones in collections, while the ones in collections are sometimes tied to C. AFAIK, most types in typing only do what the PEPs promise. I hope you understand.

    @gvanrossum
    Copy link
    Member

    It sounds reasonable to add the __name__ attribute. Since these objects
    aren't really types, the default mechanism for constructing a type doesn't
    give them this. Are there other attributes that are missing? We should
    probably add those too.

    @farcat
    Copy link
    Mannequin Author

    farcat mannequin commented Jun 30, 2021

    I have been doing some research, but note that I don't have much experience with the typing module. That said, there seem to be 2 main cases:

    • '_SpecialForm': with instances Any, Union, etc.
    • '_BaseGenericAlias'/'_SpecialGenericAlias': base classes collections classes.

    I think '_SpecialForm' can be enhanced to have '__name__' by replacing the '_name' attribute with '__name__'. Maybe add '__qualname__' as well. I cannot say whether there are many more attributes that could be implemented to have the same meaning as in 'type'. The meaning of attributes like '__mro__' seem difficult to define.
    Alternatively '__getattr__' could be added (but that might be too much):

    def __getattr__(self, attr):
        return getattr(self._getitem, attr)

    '_BaseGenericAlias''_SpecialGenericAlias' the '__getattr__' method could perhaps be adapted (or overridden in '_SpecialGenericAlias') as follows, from:

    def __getattr__(self, attr):
        # We are careful for copy and pickle.
        # Also for simplicity we just don't relay all dunder names
        if '__origin__' in self.__dict__ and not _is_dunder(attr):
            return getattr(self.__origin__, attr)
        raise AttributeError(attr)

    to:

    def __getattr__(self, attr):
        if '__origin__' in self.__dict__:
            return getattr(self.__origin__, attr)
        raise AttributeError(attr)

    or perhaps:

    def __getattr__(self, attr):
        if '__origin__' in self.__dict__ and hasattr(type, attr):
            return getattr(self.__origin__, attr)
        raise AttributeError(attr)

    to forward unresolved attribute names to the original class.

    I have written some tools and tested some with the above solutions and this seems to solve the missing '__name__' issue and make the typing abc's much more in line with the collection abc's. However I did not do any unit/regression testing (pull the repo, etc.)

    tools are attached.

    @gvanrossum
    Copy link
    Member

    Sorry for the slow progress. I don’t think it is important for Any orUnion to have these attributes, but the ones that match ABCs or concrete classes (e.g. MutableSet, Counter) should probably have __name__, __qualname__, and __module__, since the originals have those. I think __module__ should be set to ‘typing’, and __qualname__ to ‘typing.WhatEver’.

    @farcat
    Copy link
    Mannequin Author

    farcat mannequin commented Jul 19, 2021

    Happy to see progress on this issue and I can see that adding these attributes to the ABC's in typing makes the most sense. However for my direct use-case (simplified: using Any in a type checking descriptor) it would be really practical to have the __name__ (and perhaps __qualname__ and __module__) attributes in the Any type. This is mainly for consistent logging/printing purposes.

    Since Any already has a _name attribute, changing this to __name__ might achieve this.

    @ambv
    Copy link
    Contributor

    ambv commented Jul 19, 2021

    I think __module__ should be set to ‘typing’, and __qualname__ to ‘typing.WhatEver’.

    PEP-3155 specifies that __qualname__ does not include the module name:
    https://www.python.org/dev/peps/pep-3155/#excluding-the-module-name

    Rather, it's for nested classes and classes created in local scopes.

    @Fidget-Spinner
    Copy link
    Member

    Yurii has a working PR for __name__ in _BaseGenericAlias, but not for _SpecialForm yet.

    Guido and/or Lukasz, do y'all think we should support __name__ and __qualname__ for special forms too? Personally I don't see how it'd hurt and I'm +1 for this.

    @gvanrossum
    Copy link
    Member

    I see this as part of a trend to improve runtime introspection of complex
    type expressions. That seems to be going ahead regardless of whether we
    like it or not, so let's do this.

    @ambv
    Copy link
    Contributor

    ambv commented Jul 19, 2021

    New changeset bce1418 by Yurii Karabas in branch 'main':
    bpo-44524: Add missed __name__ and __qualname__ to typing module objects (bpo-27237)
    bce1418

    @ambv
    Copy link
    Contributor

    ambv commented Jul 19, 2021

    Thanks! ✨ 🍰 ✨

    @ambv ambv closed this as completed Jul 19, 2021
    @ambv ambv closed this as completed Jul 19, 2021
    @ambv ambv added 3.10 only security fixes 3.11 only security fixes and removed 3.9 only security fixes labels Jul 19, 2021
    @ambv
    Copy link
    Contributor

    ambv commented Jul 19, 2021

    New changeset c895f2b by Miss Islington (bot) in branch '3.10':
    bpo-44524: Add missed __name__ and __qualname__ to typing module objects (GH-27237) (bpo-27246)
    c895f2b

    @BvB93
    Copy link
    Mannequin

    BvB93 mannequin commented Aug 4, 2021

    This PRs herein have created a situation wherein the __name__/__qualname__ attributes of certain typing objects can be None.
    Is this behavior intentional?

    >>> from typing import Union
    
    >>> print(Union[int, float].__name__)
    None
    

    @gvanrossum
    Copy link
    Member

    Serhiy or Ken-Jin?

    @Fidget-Spinner
    Copy link
    Member

    This PRs herein have created a situation wherein the __name__/__qualname__ attributes of certain typing objects can be None. Is this behavior intentional?

    The affected objects are special forms which can hold types, so Union[], TypeGuard[], and Concatenate[].

    Personally, I don't really understand the meaning of __name__ for special forms. From the docs https://docs.python.org/3/library/stdtypes.html#definition.\_\_name__, __name__ refers to "The name of the class, function, method, descriptor, or generator instance.". __name__ make sense for GenericAlias because it's supposed to be an almost transparent proxy to the original class (eg. typing.Callable -> collections.abc.Callable). A special form is not really any one of those things listed in the docs (though I'm aware it's implemented using GenericAlias internally).

    OTOH, Python's definition of a type and typing's are wildly different. Union[X, Y] is called a "Union type" (despite being an object), and all types ought to have __name__ ;).

    @Lars, would it help your use case for Union[X, Y] and friends to have __name__ too? Note that this would mean the builtin union object (int | str) will need to support __name__ too. It looks a bit strange to me, but if it's useful I'm a +0.5 on this.

    CC-ed Serhiy for his opinion too.

    @BvB93
    Copy link
    Mannequin

    BvB93 mannequin commented Aug 5, 2021

    I do agree that it's nice to have a __name__ for special forms, as they do very much behave like types even though they're strictly speaking not distinct classes.

    Whether we should have this feature is a distinct "problem" from its __name__ being None (as can happen in its the current implementation),
    the latter of which is actively breaking tests over in numpy/numpy#19612.
    I don't recall ever seeing a non-string name before, so I'd argue that this is a bug.

    It seems to be easy to fix though (see below for a Union example), so if there are objections I'd like to submit a PR.

    -    return _UnionGenericAlias(self, parameters)
    +    return _UnionGenericAlias(self, parameters, name="Union")
    

    @ambv
    Copy link
    Contributor

    ambv commented Aug 6, 2021

    Looks like we can re-close this again. Thanks for your quick reaction, Bas! ✨ 🍰 ✨

    @ambv ambv closed this as completed Aug 6, 2021
    @ambv ambv closed this as completed Aug 6, 2021
    @pablogsal
    Copy link
    Member

    Unfortunately PR27614 and its backport has introduced reference leaks:

    ❯ ./python -m test test_typing -R :
    0:00:00 load avg: 1.12 Run tests sequentially
    0:00:00 load avg: 1.12 [1/1] test_typing
    beginning 9 repetitions
    123456789
    .........
    test_typing leaked [29, 29, 29, 29] references, sum=116
    test_typing leaked [10, 10, 10, 10] memory blocks, sum=40
    test_typing failed (reference leak)

    == Tests result: FAILURE ==

    1 test failed:
    test_typing

    1 re-run test:
    test_typing

    Total duration: 1.2 sec

    @pablogsal
    Copy link
    Member

    Unfortunately given that the all refleak buildbots will start to fail and the fact that this got into the release candidate, per our buildbot policy (https://discuss.python.org/t/policy-to-revert-commits-on-buildbot-failure/404) we will be forced to revert 8bdf12e and its backport to avoid masking other issues if this is not fixed in 24 hours.

    @pablogsal
    Copy link
    Member

    Wow, turns out the reference leak has been here since forever! I opened https://bugs.python.org/issue44856? to tackle it

    @ambv
    Copy link
    Contributor

    ambv commented Aug 7, 2021

    Curiously, while the root cause for the refleaks is in BPO-44856, while hunting down how test_typing.py triggered them, I found that for a while now this exception has been kind of broken:

    >>> class C(Union[int, str]): ...
    ...
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: __init__() takes 2 positional arguments but 4 were given
    >>>

    It's still a TypeError but the message is cryptic. This regressed in Python 3.9. In Python 3.8 and before, this used to be more descriptive:

    >>> class C(Union[int, str]): ...
    ...
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/Users/ambv/.pyenv/versions/3.8.9/lib/python3.8/typing.py", line 317, in __new__
        raise TypeError(f"Cannot subclass {cls!r}")
    TypeError: Cannot subclass <class 'typing._SpecialForm'>

    Interestingly, after the Bas' last change, the exception is now yet different:

    >>> class C(Union[int, str]): ...
    ...
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

    This makes sense, the conflict is due to bases being (typing.Union, <class 'typing.Generic'>) where "typing.Union" is really a _UnionGenericAlias since this is a subscripted Union (unlike bare "typing.Union" which is an instance of _SpecialForm). And in _GenericAlias' __mro_entries__ we're finding:

    cpython/Lib/typing.py

    Lines 1089 to 1090 in a40675c

    if self._name: # generic version of an ABC or built-in class
    return super().__mro_entries__(bases)

    Clearly Ivan only intended _name to be used for shadowing builtins and ABCs.

    BTW, the "__init__() takes 2 positional arguments but 4 were given" is about _SpecialForm's __init__. It's called with 4 arguments through here in builtin___build_class__:

    PyObject *margs[3] = {name, bases, ns};
    cls = PyObject_VectorcallDict(meta, margs, 3, mkw);

    This isn't high priority since the end result is a TypeError anyway, but it's something I will be investigating to make the error message sensible again.

    @ambv
    Copy link
    Contributor

    ambv commented Aug 18, 2021

    New changeset a3a4d20 by Yurii Karabas in branch 'main':
    bpo-44524: Fix cryptic TypeError message when trying to subclass special forms in typing (GH-27710)
    a3a4d20

    @serhiy-storchaka
    Copy link
    Member

    There are still cryptic TypeError messages for Annotated:

    >>> class X(Annotated[int | float, "const"]): pass
    ... 
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

    @serhiy-storchaka
    Copy link
    Member

    There are some side effects of setting _name. In 3.9:

    >>> class X(Annotated[int, (1, 10)]): pass
    ... 
    >>> X.__mro__
    (<class '__main__.X'>, <class 'int'>, <class 'object'>)

    In 3.10:

    >>> class X(Annotated[int, (1, 10)]): pass
    ... 
    >>> X.__mro__
    (<class '__main__.X'>, <class 'int'>, <class 'typing.Generic'>, <class 'object'>)

    Now a subclass of an Annotated alias is a generic type. Should it be?

    @Fidget-Spinner
    Copy link
    Member

    Now a subclass of an Annotated alias is a generic type. Should it be?

    I'm unsure if Annotated should be subclassable in the first place, but if I understand PEP-593 correctly,
    class X(Annotated[int, (1, 10)]), should be equivalent to class X(int) right? If that's the case, it's subclassable and Generic shouldn't be in the MRO.

    FWIW, the other special forms don't allow subclassing, so we don't need to think about this problem for them. Annotated is a special cookie.

    I propose we just drop the _name hack temporarily in Annotated. A real fix requires fixing up __mro_entries__, but I am uncomfortable with us backporting to 3.10 anything that touches __mro_entries__ due to the numerous edge cases it has and how close we are to 3.10 final.

    @JelleZijlstra
    Copy link
    Member

    I don't think we need to support Annotated as a base class. PEP-593 is titled "Flexible function and variable annotations", and base classes are neither of those things. None of the examples in the PEP or the implementation use Annotated as a base class either.

    On the other hand, subclassing Annotated[T, ...] does work at runtime in 3.9, so maybe we're bound by backward compatibility now.

    @serhiy-storchaka
    Copy link
    Member

    New changeset 4ceec49 by Serhiy Storchaka in branch 'main':
    bpo-44524: Do not set _name of _SpecialForm without need (GH-27861)
    4ceec49

    @serhiy-storchaka
    Copy link
    Member

    New changeset 5bd27c3 by Miss Islington (bot) in branch '3.10':
    bpo-44524: Do not set _name of _SpecialForm without need (GH-27861) (GH-27871)
    5bd27c3

    @serhiy-storchaka
    Copy link
    Member

    New changeset 23384a1 by Ken Jin in branch 'main':
    bpo-44524: Don't modify MRO when inheriting from typing.Annotated (GH-27841)
    23384a1

    @miss-islington
    Copy link
    Contributor

    New changeset 06e9a35 by Miss Islington (bot) in branch '3.10':
    bpo-44524: Don't modify MRO when inheriting from typing.Annotated (GH-27841)
    06e9a35

    @miss-islington
    Copy link
    Contributor

    New changeset 81fa08c by Miss Islington (bot) in branch '3.10':
    bpo-44524: Fix cryptic TypeError message when trying to subclass special forms in typing (GH-27710)
    81fa08c

    @ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
    @AlexWaygood
    Copy link
    Member

    Is there anything left to do here, or can this now be closed?

    @JelleZijlstra
    Copy link
    Member

    This issue has gone through a bit of a journey, but the original complaint was that __name__ was missing on typing objects. I checked @farcat's script on current main and the only objects in typing without a __name__ are {'EXCLUDED_ATTRIBUTES', 'TYPE_CHECKING'}, which seems reasonable enough.

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    3.10 only security fixes 3.11 only security fixes topic-typing type-bug An unexpected behavior, bug, or error
    Projects
    None yet
    Development

    No branches or pull requests

    8 participants