Initial impressions
- Re-using the ABC registration system is a nice idea. Note that this will not raise any errors at class definition time, only at instantiation time.
- I personally think is a mis-feature of ABCs and not really your fault, but maybe you are happy with that behavior.
- Another option is to entirely skip runtime type-checking of subclasses of interfaces, leaving all of this work to the type checker.
- Write docstrings! Some of this code is what I would call "non-obvious". You should document what each of these functions does.
- At the very least, you should put in a comment on each of the functions that are meant to be attached to
Interface classes, because it took a couple re-reads to realize what you were doing.
- Some names are a bit "opaque".
_registration_hook could be _register_interface_subclass
comparable could be is_valid_interface_subclass
- In
comparable(), returning a string-ified "error message" is an awkward signaling mechanism. You should structure this data somehow, and only string-ify it for presentation to the user. Otherwise it will makes this library difficult to work with. See below.
- In
subclasshook(), you probably don't want "".join. Perhaps you want "\n".join or similar?
- You can (and should) type-hint
InterfaceMeta and its associated functions.
- Use
warnings.warn() instead of print().
- I'm not sure I understand how
for_id() is supposed to be used, nor do I understand the get_classification() function in the demo. Is it meant to be some kind of dynamic implementation lookup?
- What is a "classification" supposed to be, anyway? This doesn't look like it's related to the design of your interface system, but it's part of your demo and I don't understand it.
Don't use strings to convey structured information
The stringly-typed output from comparable() is not great. You might want to build a more-structured hierarchy of interface-subclassing failures, which is only string-ified "on-demand" by the user.
For example:
from __future__ import annotations
import inspect
from typing import TypeVar, Sequence
_Interface = TypeVar('_Interface', bound='Interface')
@dataclass
class ImplementationSubclassingError(TypeError):
"""Base class for errors in an implementation of an Interface."""
parent: _Interface
subclass: Type[_Interface]
@dataclass
class AttributeMissing(ImplementationSubclassingError):
attribute_name: str
def __str__(self) -> str:
return (
f"Attribute {self.attribute_name} of {self.parent.__name__} is "
f"not implemented in {self.subclass.__name__}."
)
class SignatureMismatch(ImplementationSubclassingError):
method_name: str
parent_argspec: inspect.Argspec
subclass_argspec: inspect.Argspec
def __str__(self) -> str:
return (
f"Signature of {self.subclass.__name__}.{self.method_name} "
f"does not match "
f"signature of {self.parent.__name__}.{self.method_name}."
)
class ImplementationInvalid(TypeError):
# Re-use the "args" attribute from BaseException
errors: Sequence[ImplementationSubclassingError, ...]
def __init__(*errors: ImplementationSubclassingError):
self.errors = errors
super().__init__(*errors)
def __str__(self) -> str:
return "\n".join(map(str, self.errors))
This kind of setup gives you a programmable and introspectable system, which also results in nice error messages for the user.