19

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.

Mikko Ohtamaa
  • 82,057
  • 50
  • 264
  • 435
  • I suspect it will require a lot of effort to make this work anywhere but in the library that adds the methods for you. If it adds methods with proper type annotations attached, it should just work. Perhaps you should file a feature-request bug on Pyramid? – Blckknght Dec 11 '15 at 19:02
  • Note that this question is very old. If anyone lands to this question from Google, it is no longer applicable for current Python versions. – Mikko Ohtamaa Feb 09 '23 at 13:16

2 Answers2

11

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.

3
  • 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 
    
phouse512
  • 650
  • 4
  • 15
  • 27
Mikko Ohtamaa
  • 82,057
  • 50
  • 264
  • 435