Skip to main content
added 12 characters in body
Source Link
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__}."
        )


@dataclass
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))
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))
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__}."
        )


@dataclass
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))
Source Link

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.