Skip to main content

Create abstract class variables

Project description

AbstractCP -- Abstract Class Property

Tox tests

This package allows one to create classes with abstract class properties. The initial code was inspired by this question (and accepted answer) -- in addition to me struggling many time with the same issue in the past. I first wanted to just post this as separate answer, however since it includes quite some python magic, and I would like to include quite some tests (and possibly update the code in the future), I made it into a package (even though I'm not a big fan of small packages that hardly do anything).

The package is python3.8 and higher (version 0.9.9 runs on 3.6-3.11).

TL;DR Examples

Note: the examples use PEP-526 type hints; this is obviously optional.

All examples assume the following imports:

import tying as t
import abstractcp as acp

Note that all typing (including the import typing as t is optional. In addition, for python < 3.8, the Literal type hint can be found in typing_extensions.

class Parser(acp.Abstract):
    PATTERN: str = acp.abstract_class_property(str)

    @classmethod
    def parse(cls, s):
        m = re.fullmatch(cls.PATTERN, s)
        if not m:
            raise ValueError(s)
        return cls(**m.groupdict())

class FooBarParser(Parser):
    PATTERN = r"foo\s+bar"

    def __init__(...): ...

class SpamParser(Parser):
    PATTERN = r"(spam)+eggs"

    def __init__(...): ...

Example with (more) type hints:

class Array(acp.Abstract):
    payload: np.ndarray
    DIMENSIONS: int = acp.abstract_class_property(int)

    def __init__(self, payload):
        assert len(payload) == type(self).DIMENSIONS

class Vector(Array):
    DIMENSIONS: t.Literal[1] = 1

class Matrix(Array):
    DIMENSIONS: t.Literal[2] = 2

Note that in the previous example, we actually fix the value for DIMENSIONS using t.Literal. This is allowed in mypy (however it may actually be a bug that it's allowed). It would possibly feel more natural to use a t.Final here, however mypy doesn't allow this.

Note that if we forget to assign a value for DIMENSIONS, an error will occur:

class OtherArray(Array):
    pass

> TypeError: Class OtherArray must define abstract class property DIMENSIONS, or have Abstract as direct parent

In some cases, however, we might indeed intend for the OtherArray class to be abstract as well (because we will subclass this later). If so, make OtherArray inherit from Abstract directly to fix this:

class OtherArray(Array, acp.Abstract):
   ...

class OtherVector(OtherArray):
    DIMENSIONS = 1

Introduction

I quite often find myself in a situation where I want to store some configuration in a class-variable, so that I can get different behaviour in different subclasses. Quite often this starts with a top-level base class that has the methods, but without a reasonable value to use in the configuration. In addition, I want to make sure that I don't accidentally forget to set this configuration for some child class -- exactly the behaviour that one would expect from abstract classes. However Python doesn't have a standard way to define abstract class variables (or class constants). The search for a solution initially led me to this question -- the accepted answer works well, as long as you accept that each subclass of the parent must be non-abstract. In addition, it would not play nice at all with type-hinting and tools like mypy.

So I decided to write something myself -- it started as a small StackOverflow answer, however since I felt lots of tests and docs would be required, better make it a proper module.

Design Considerations

I had some clear requirements in mind when writing this package:

  • Pythonic syntax
  • Works well with PEP-526 style type hints and static type checkers (if possible without any # type: ignore in either this code, and the code using this module).
  • No runtime slowdowns (i.e.: all the work gets done at setup-time)
  • Useful error messages -- stuff needs to be explicit, no silent failures.
  • No need to define all abstract class properties directly in the first child -- so an abstract class can have abstract children.

Installation

The package is a 100% python package. Installation is as simple as

pip install abstractcp

Use

The system consists of 2 elements: The Abstract base class. Each class that is abstract (i.e. that has abstract class properties -- this is completely independent of the ways to make a class abstract in abc) must inherit directly from Abstract, meaning that Abstract should be a direct parent. This is done so that it's explicit which classes are abstract (and hence, we can throw an error if a class is abstract and does not inherit directly from Abstract).

The second part of the system is the _AbstractClassProperty class. Every abstract class property gets assigned an _AbstractClassProperty() instance, through the acp.abstract_class_property(...) method. Note that this method has typehints to return the exact class that you provide, so from a type checker point of view, acp.abstract_class_property(int) is identical to 3 (or 4, or any other int instance). This means that we can be more flexible here, for instance doing acp.abstract_class_property(t.Dict[str, int]), however note that acp.abstract_class_property(t.Mapping[str, int]) does not work, since mypy wants a concrete type there.

Note that abstract_class_property() can only be assigned in classes that have Abstract as direct parent.

See the Examples section above for exact use.

Update from 0.9.1

Note that since 0.9.1 the syntax has changed a bit. Rather than writing:

class A(acp.Abstract):
   i = acp.AbstractInt()

you now use

class A(acp.Abstract):
   i = acp.abstract_class_property(int)

It results in cleaner code, and also means that we don't have to make our own classes for new types.

FAQ

I'm getting Argument 1 to "abstract_class_property" has incompatible type "object"; expected "Type[<nothing>]" errors

This happens when you try to feed something that is not actually a type to abstract_class_property, for instance x = acp.abstract_class_property(t.Union[str, int]) (or even, more correctly, t.Type[t.Union[str, int]] or t.Union[t.Type[str], t.Type[int]]. Also x = acp.abstract_class_property(t.Type[Employee]) will not work (since t.Type does not actually make something a type; in this case use type(Employee) instead (which would give you an abstract property that could receive some subclass of Employee).

Note that the argument to abstract_class_property is only for readability and used in the __repr__ of the _AbstractClassProperty class -- and for static typing. So as long as you satisfy static typing, all will be fine:

T = t.TypeVar("T", int, str)

class A(t.Generic[T], acp.Abstract):
    VALUE_TYPE: t.Type[T] = acp.abstract_class_property(t.cast(t.Type[t.Type[T]], "union of int and str"))
    def to_value(self) -> T:
        ...

Note the double t.Type, since acp.abstract_class_property will remove 1 t.Type.

Why am I getting warnings when I inherit a class from acp.Asbtract but don't define any abstract fields

You will get a Python warning if you run the following code:

class A(acp.Abstract):
   i = 3

You are defining class A to be abstract, however it has no fields with abstract_class_property. In almost all cases this means that either you should add an abstract class property, or remove the acp.Abstract inherritance.

Defining a class like this used to result in a TypeError in versions <= 0.9.8, but is a warning from version 0.9.9 forward.

You can safely ignore the warning (if you understand what you're doing; for instance if you just commented out the abstract class property during development for a moment), or if you really want to silence the warning forever in production code, add the following code to your program:

import warnings
warnings.filterwarnings("ignore", category=acp.AbstractClassWithoutAbstractPropertiesWarning)

If you do this, I would appreciate if you drop me a line, since it probably means you've found a novel use for the package that I'd be happy to learn about (and possibly document).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

abstractcp-1.0.0.tar.gz (8.2 kB view hashes)

Uploaded Source

Built Distribution

abstractcp-1.0.0-py3-none-any.whl (7.3 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page