Search code examples
pythonpython-3.xclass-method

How to overcome "classmethod is not callable" in python


After reading Various errors in code that tries to call classmethods, Why are python static/class method not callable?, Are staticmethod and classmethod in python not callable? and others it is clear that Python class methods are not callable. The "why" does not seem to be answered that well and just saying that it is not needed is plainly not true as manifested by numerous questions on this topic.

Anyway, I have a XML with many different tags used in the same position each requiring fundamentally similar, but different nonetheless, handling. My idea was to have a base class and subclasses for the individual cases, each able to parse its own part (element) and operate with these. Like this (simplified):

import xml.etree.ElementTree as ET


class base():        
    parsers = dict()
    @classmethod
    def parseXml(cls, x:str):
        parser = base.parsers.get(x)
        parser(cls, x)  # different class, but same base (and all called cls elements are only in base class)
                # or parser(el)


class derivedOne(base):
    def __init__(self):
        self.one = None
    @classmethod
    def parseXml(cls, x:str):

        d1 = derivedOne()
        d1.one = 'one+'+x
    
    base.parsers['xmlTagOne']=parseXml 


class derivedTwo(base):
    def __init__(self):
        self.two = None
    @classmethod
    def parseXml(cls, x:str):
        d2 = derivedTwo()
        d2.two = 'two+'+x

    base.parsers['xmlTagTwo']=parseXml

    if __name__ == "__main__":
        base.parseXml('xmlTagOne')
        base.parseXml('xmlTagTwo')

The parser is found, but not callable.

Of course all the constructors could be called from one place with some if/else logic, but that is not very elegant and most importantly it does not allow (as I planned) to add additional handler/parser classes without changing the base class...

Is there any way to keep this "add parsing of new tag in single class"?


Solution

  • You can use __init_subclass__ to store a reference to a new subclass in base.parsers.

    class Base:        
        parsers = dict()
        @classmethod
        def parseXml(cls, x:str):
            parser = base.parsers.get(x)
            parser(cls, x)
    
        def __init_subclass__(cls, /, tag, **kwargs):
            super().__init_subclass__(**kwargs)  # removed ...(cls, ...
            Base.parsers[tag] = cls
    
    
    class DerivedOne(Base, tag="xmlTagOne"):
        def __init__(self):
            self.one = None
    
        @classmethod
        def parseXml(cls, x:str):
            ...
    
    
    class DerivedTwo(Base, tag="xmlTagTwo"):
        def __init__(self):
            self.two = None
        @classmethod
        def parseXml(cls, x:str):
            ...
    
    
    if __name__ == "__main__":
        base.parsers['xmlTagOne'].parseXml(some_string)
        base.parsers['xmlTagTwo'].parseXml(some_other_string)
    

    Base.__init_subclass__ is called after immediately after the subclass is created, with any keyword arguments in the base class list passed through to __init_subclass__. Each subclass is saved to Base.parsers with the specified tag.


    Note: I'm ignoring the issue of whether you should use classes and class methods at all here.