On 11Nov2022 15:29, Ian Pilcher <arequip...@gmail.com> wrote:
I am trying to figure out a way to gracefully deal with uncallable
classmethod objects.

I'm just going to trim your example below a bit for reference purposes:

class DUID(object):
   def __init__(self, d):
       for attr, factory in self._attrs.items():
           setattr(self, attr, factory(d[attr]))
   @classmethod
   def from_dict(cls, d):
       subcls = cls._subclasses[d['duid_type']]
       return subcls(d)

class DuidLL(DUID):
   @staticmethod
   def _parse_l2addr(addr):
       return bytes.fromhex(addr.replace(':', ''))
   _attrs = { 'layer2_addr': _parse_l2addr }

class DuidLLT(DuidLL):
   @classmethod
   def _parse_l2addr(cls, addr):
       return super()._parse_l2addr(addr)
   _attrs = {
           'layer2_addr': _parse_l2addr,
       }

So what you've got is that `for attr, factory in self._attrs.items():` loop, where the factory comes from the subclass `_attrs` mapping. For `DuidLL` you get the static method `_parse_l2addr` object and for `DuidLLT` you get the class method object.

[...]
This works with static methods (as well as normal functions and object
types that have an appropriate constructor): [...]
[...]

It doesn't work with a class method, such as DuidLLT._parse_l2addr():

duid_llt = DUID.from_dict({ 'duid_type': 'DUID-LLT', 'layer2_addr': 
'de:ad:be:ef:00:00', 'time': '2015-09-04T07:53:04-05:00' })
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
 File "/home/pilcher/subservient/wtf/wtf.py", line 19, in from_dict
   return subcls(d)
 File "/home/pilcher/subservient/wtf/wtf.py", line 14, in __init__
   setattr(self, attr, factory(d[attr]))
TypeError: 'classmethod' object is not callable

In searching, I've found a few articles that discuss the fact that
classmethod objects aren't callable, but the situation actually seems to
be more complicated.

type(DuidLLT._parse_l2addr)
<class 'method'>
callable(DuidLLT._parse_l2addr)
True

The method itself is callable, which makes sense.  The factory function
doesn't access it directly, however, it gets it out of the _attrs
dictionary.

type(DuidLLT._attrs['layer2_addr'])
<class 'classmethod'>
callable(DuidLLT._attrs['layer2_addr'])
False

I'm not 100% sure, but I believe that this is happening because the
class (DuidLLT) doesn't exist at the time that its _attrs dictionary is
defined.  Thus, there is no class to which the method can be bound at
that time and the dictionary ends up containing the "unbound version."

Yes. When you define the dictionary `_parse_l2addr` is an unbound class method object. That doesn't change.

Fortunately, I do know the class in the context from which I actually
need to call the method, so I am able to call it with its __func__
attribute.  A modified version of DUID.__init__() appears to work:

   def __init__(self, d):
       for attr, factory in self._attrs.items():
           if callable(factory):  # <============= ???!
               value = factory(d[attr])
           else:
               value = factory.__func__(type(self), d[attr])
           setattr(self, attr, value)

Neat!

A couple of questions (finally!):
* Is my analysis of why this is happening correct?

It seems so to me. Although I only learned some of these nuances recently.

* Can I improve the 'if callable(factory):' test above?  This treats
 all non-callable objects as classmethods, which is obviously not
 correct.  Ideally, I would check specifically for a classmethod, but
 there doesn't seem to be any literal against which I could check the
 factory's type.

Yeah, it does feel a bit touchy feely.

You could see if the `inspect` module tells you more precise things about the `factory`.

The other suggestion I have is to put the method name in `_attrs`; if that's a `str` you could special case it as a well known type for the factory and look it up with `getattr(cls,factory)`.

Cheers,
Cameron Simpson <c...@cskk.id.au>
--
https://mail.python.org/mailman/listinfo/python-list

Reply via email to