Search code examples
python-3.xpycharmgetattrslots

Avoid Pycharm __dict__ lookup when using __getattr__ and __slots__ for composition


Say I have a class:

class Example:
    __slots__ = ("_attrs", "other_value")
    
    def __init__(self):
       self._attrs = OrderedDict()
       self.other_value = 1
       self.attribute = 0
    
    def __setattr__(self, key, value):
       if key in self.__slots__:
            return super().__setattr__(key, value)
       else:
           self._attrs[key] = value
     
    def __getattr__(self, key):
         return self._attrs[key]

The goal is to have Example have two slots:

  • if those are set, then set them as usual. (works)
  • If additional attributes are set, assign them in _attrs. (works)

For getting attributes, the code should:

  • Act as usual if anything from slots is requested (works)
  • get the value from _attrs if it exists in _attrs.keys() (works)
  • error in any other case as usual (issue).

For the issue, I'd like the error to mimic what would normally happen if an attribute was not present for an object. Currently when running code I get a key error on self._attrs. Although this is fine, it would be nice for it to hide this nuance away. More annoyingly, if I debug in Pycharm, the autocomplete will chuck out a large error trying to look at dict before I've even hit enter:

Example().abc # hit tab in pycharm

# returns the error:
Traceback (most recent call last):
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/_pydevd_bundle/pydevd_comm.py", line 1464, in do_it
    def do_it(self, dbg):
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/_pydev_bundle/_pydev_completer.py", line 159, in generate_completions_as_xml
    def generate_completions_as_xml(frame, act_tok):
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/_pydev_bundle/_pydev_completer.py", line 77, in complete
    def complete(self, text):
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/_pydev_bundle/_pydev_completer.py", line 119, in attr_matches
    def attr_matches(self, text):
  File "/Applications/PyCharm.app/Contents/plugins/python/helpers/pydev/_pydev_bundle/_pydev_imports_tipper.py", line 165, in generate_imports_tip_for_module
    def generate_imports_tip_for_module(obj_to_complete, dir_comps=None, getattr=getattr, filter=lambda name:True):
  File "/Users/xxxxxxxxx/", line 46, in __getattr__
    def __getattr__(self, key: str) -> None:
KeyError: '__dict__'

Is there a way to suppress this by writing the code differently?


Solution

  • You might be able to make it work by implementing __dir__ on the class, so it has a canonical source of names that can be completed:

    def __dir__(self):
        return 'other_value', *self._attrs.keys()
    

    I can't swear to how PyCharm implements their tab-completion, so there's no guarantee it works, but this is the way to define the set of enumerable attributes for a type, and hopefully PyCharm will use it when available, rather than going for __dict__.

    The other approach (and this is probably a good idea regardless) it to make sure you raise the right error when __getattr__ fails so PyCharm knows the problem is a missing attribute, not some unrelated issue with a dict:

    def __getattr__(self, key):
        try:
            return self._attrs[key]
        except KeyError:
            raise AttributeError(key)