Package: purepy
purepyPure virtual class functionality in Python.Averysmall metaclass to do some of the testing for us.BranchStatusCoveragemasterdevThe WhatIn C++ and other strong typed, OOP, languages, we use virtual classes and pure virtual classes to help
handle some incredibly cool paradigms when it comes to plugin design, object organization and more.Python thinks ofeverythingas a virtual class. Which is great because polymorphism doesn't
require us to explicitly set which functions are virtual or overloaded but instead just works!This is awesome until it's not.The WhySo, with this knowledge, you ask, "Why bother with pure virtual classes? There are plenty of reasons
not to use this in Python." You would be right! There's plenty of reasonsnotto use/need this tool.But, when the need arises, you may just find this quite helpful. For us, we found it most useful when
we were integrating an API into multiple third party applications and wanted to assure ourselves we had
the right functionality and signatures without needing to write additional test code or wait for the
interpreter to make an instance of an ABCMeta object for it to fail.The AdvantageWe first took a stab with theabc.ABCMetaobject from Pythons default libs but ran into the issue ofI can do whatever I want anduntilthe object is made, it will be wrong!Which is good sometimes, because it allows for crazy stuff likesetattr()and dynamic class buildingbut, when it comes to integration of an app, there's usually less desire for out-there solutions like__setitem__orsetattr().We want the interpreter, as soon as it loads our class into memory, to alert us if it's not "up to
code" and tell us what we need to fix about it. This is very "preprocessor" like and it has some major
advantages with a few caveats.Basic ExampleGiven the following:frompurepyimportPureVirtualMeta,pure_virtualclassInterface(metaclass=PureVirtualMeta):@pure_virtualdefsave(self,filepath=None):raiseNotImplementedError()@pure_virtualdefload(self,filepath=None):raiseNotImplementedError()classOverload(Interface):defsave(self,filepath=None):print("Saving")If we put this into the interpreter, without even creating an instance of the Overload class, we
would get:# ...# PureVirtualError: Virtual Class Declaration:# - 'Overload': The following pure virtual functions must be overloaded from#               base: 'Interface' before class can be used:#     - def load(self, filepath=None)We got that error without having to execute any manual code or writing a test. This may not be the way
you want to work, at which point you don't need this utility!Additional FeaturesTo act like a proper pure virtual class,PureVirtualMetaand the defaultpure_virtualutilities areextremelystrict when it comes to working with the classes. There are a wide variety of ways to augment
this however as described below.Signature VerificationBy defaultpurepywill assert that the signatures of thepure_virtualfunction match the
overloaded.classInterface(metaclass=PureVirtualMeta):@pure_virtualdefsave(self,filepath=None):raiseNotImplementedError()classOverload(Interface):defsave(self):print("Saving")# Result:# ...# PureVirtualError: Virtual Class Declaration:# - 'Overload': The following overload functions have the#               wrong signature from base: 'Interface'#     - def save(self): -> def save(self, filepath=None):This can be disabled by setting the class variablepv_explicit_args = FalseclassInterface(metaclass=PureVirtualMeta):pv_explicit_args=False# ...Base InstancesBy defaultpurepywill mimic theabc.abstractmethodand raise and error when we try to
instantiate a pure virtual class.classInterface(metaclass=PureVirtualMeta):@pure_virtualdefsave(self,filepath=None):raiseNotImplementedError()>>>Interface()# ...# PureVirtualError: Cannot instantiate pure virtual class# 'Interface' with pure virtual functions: (save)This can be disabled with the class variablepv_allow_base_instance = TrueclassInterface(metaclass=PureVirtualMeta):pv_allow_base_instance=True@pure_virtualdefsave(self,filepath=None):raiseNotImplementedError()>>>print(Interface())# <__main__.Interface object at ...>Forced NotImplementedErrorBy default, thepure_virtualdecorator will force all it's functions to raise aNotImplementedErroreven
when there is information defined and the class can be instantiated.classInterface(metaclass=PureVirtualMeta):pv_allow_base_instance=True@pure_virtualdefsave(self,filepath=None):print("Saving ",str(filepath))>>>inst=Interface()>>>inst.save("foo")# ...# NotImplementedError: Illegal call to pure virtual function saveThis can be disabled with a custom decorator by settingforce_not_implemented = False.my_pure_virtual=PureVirtualMeta.new(force_not_implemented=False)classInterface(metaclass=PureVirtualMeta):pv_allow_base_instance=True@my_pure_virtualdefsave(self,filepath=None):print("Saving ",filepath)>>>inst=Interface()>>>inst.save("foo")# Saving fooCustomized DecoratorBy default, thepure_virtualdecorator provided is quite strict. In some cases you may want to
augment the properties to make it more forgiving. This can be done with thePureVirtualMeta.new()andPureVirtualMeta.new_class()functions. Both functions take additional**kwargsthat augment the
decorator and subsequent validation.my_pure_virtual=PureVirtualMeta.new(strict_types=False)classInterface(metaclass=PureVirtualMeta):@my_pure_virtualdeffoo(self,filepath:str):raiseNotImplementedError()classOverload(Interface):# This is NOT okay by default, but okay with our custom decoratordeffoo(self,filepath):passRegistryThere are two ways to control/retrieve the pure virtual functions available in the api.From IdEachpure_virtualdecorator gets a unique identifier and all functions it its registry are handled
underneath that.classInterface(metaclass=PureVirtualMeta):@pure_virtualdeffoo(self,filepath):raiseNotImplementedError()print(PureVirtualMeta.virtual_functions_from_id(pure_virtual.id()))# [<function Interface.save at ...>]From ClassEach class registers the pure virtual functions and can be polled by both the class and an instance of
said class.classInterface(metaclass=PureVirtualMeta):pv_allow_base_instance=True@pure_virtualdeffoo(self,filepath):raiseNotImplementedError()print(PureVirtualMeta.pure_virtual_functions(Interface))# [<function Interface.save at ...>]print(PureVirtualMeta.pure_virtual_functions(Interface()))# [<function Interface.save at ...>]print(PureVirtualMeta.is_pure_virtual_class(Interface))# TrueOverride DecoratorFor clarity, we may want to decorate the overloaded functions. In C++ we use something like:voidmyFunction(intvariable)override;purepyprovides theoverridedecorator this this purpose.frompurepyimportoverrideclassOverload(Interface):@override()deffoo(self,filepath):print("This is overloaded")Note: Youmustcall the override decorator, even with no arguments, to setup the proper function
binding.In the future, this may to be used to further augment the functionality of overloaded functions.