Python 3.5 type hinting dynamically generated inst

2019-06-16 10:32发布

问题:

I'd like to add Python 3.5 type hints for dynamically generated object attributes, so that IDEs correctly autocomplete them. Here by "dynamical" I mean that the attribute is not present during class creation or in __init__ or any other method.

E.g. is there a way to add these through comments or other tricks? If not I can fallback to add dummy class attributes.

Example::

 class Request:
      """Example HTTP request object.

      We have `get_user()`  but we do not declare it anyhere.
      """

 ...


 # Pyramid's way of plugging in methods and properties to request, enabled addon capabilities for the framework
 # adds Request.user - done in different part of application lifecycle, not during class creation
 config.add_request_method(auth.get_user, 'user', reify=True)

The goal is to make this work so that PyCharm and other IDEs would complete this attribute.

回答1:

In Python 3.6+ you can use the class-level type hints - these would not generate attributes in the class. I.e.

class Request(_Request):
    user: Optional[User]

This would not create an attribute in the class, only an annotation.

>>> Request.user
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'Request' has no attribute 'user'

>>> Request.__annotations__
{'user': typing.Union[foo.User, NoneType]}

In Python 3.5 it is possible to make a function that returns a non-data descriptor (i.e. a descriptor without __set__); this would be overridable by an instance attribute but it comes with some minimal runtime overhead - the descriptor will be fetched from __dict__ and checked if it defines the __set__ slot - even for all reads. It could then look something like

class Request(_Request):
    user = typed(User)

where the typed is defined as

def typed(type: Type[T]) -> T:
    ... return a dummy non-data-descriptor...

This should be enough for PyCharm to infer the types correctly.



回答2:

  • I subclassed the real class

  • I add faux __type_hinting__ method in my class

  • I use this class instead of the real one as the argument type hint

    class Request(_Request):
        """
        HTTP request class.
        This is a Pyramid Request object augmented with type hinting information for Websauna-specific functionality.
        To know more about request read also
        * py:class:`pyramid.request.Request` documentation
        * py:class:`webob.request.Request` documentation
    
        Counterintuitively, this request is also available in non-HTTP applications like command line applications and timed tasks. 
        These applications do not get request URL from a front end HTTP webserver, but a faux request is constructed pointing to the website URL taken from ``websauna.site_url`` setting. 
        This is to allow similar design patterns and methodology to be applied in HTTP and non-HTTP applications.
    
        By setting variables in ``__type_hinting__()`` based on arguments types allows IDEs to infer type information when you hint your views as::
    
             from websauna.system.http import Request
    
             def hello_world(request: Request):
                 request.  # <-- When typing, here autocompletion kicks in.
    
        """
    
        def __type_hinting__(self, user: Optional[User], dbsession: Session, session: ISession, admin: Admin, registry: Registry):
            """
            A dummy helper function to tell IDEs about reify'ed variables.
            :param user: The logged in user. None if the visitor is anonymous.
            :param dbsession: Current active SQLAlchemy session
            :param session: Session data for anonymous and logged in users.
            :param admin: The default admin interface of the site. Note that the site can have several admin interfaces for different purposes.
            :param registry: Pyramid registry's. E.g. 
                :py:attr:`pyramid.registry.Registry.settings` for reading settings and :py:meth:`pyramid.registry.Registry.notify` for sending events.
            """
            self.user = user
            self.dbsession = dbsession
            self.session = session
            self.admin = admin
            self.registry = registry