Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/xrudelis/pytrait

Traits for Python3
https://github.com/xrudelis/pytrait

Last synced: 7 days ago
JSON representation

Traits for Python3

Awesome Lists containing this project

README

        

Find the package here: https://pypi.org/project/pytrait/

PyTrait
=======

Do you like Python, but think that multiple inheritance is a bit too flexible? Are you
looking for a more constrained way to define interfaces and re-use code?

Try using PyTraits!

We provide three metaclasses that aid writing code for shared behavior separately from
concrete types. For the most part, `Trait`s define interfaces, `Struct`s define state,
and `Impl`s define implementation. `Trait`s must be defined before any `Impl`s which
implement them, and `Impl`s must be defined before the `Struct`s that use them.

See examples under `examples/`.

Traits
------

Traits are abstract base classes (ABCs). There's really not much else to say, except
that these ABCs are always implemented in `Impl` classes, which themselves have no
abstract methods, but are not concrete classes; instead an `Impl` is associated with
another type that it bestows implementation upon. This would be either a concrete class
(always a `Struct`) or all such concrete classes implementing a given `Trait`.

```python
from pytrait import Trait, abstractmethod

class MyTrait(metaclass=Trait):
@abstractmethod
def my_method(self) -> str:
pass
```

Structs
-------

Python has _dataclasses_, and they're great. We're using them internally for our
Structs, so whenever you see `metaclass=Struct`, the class is also a dataclass.
Don't get confused with the existing Python module `struct` -- that one is lower-case.

```python
from pytrait import Struct

class MyStruct(metaclass=Struct):
my_message: str = "this is a dataclass"

def __post_init__(self):
assert my_message == "this is a dataclass"
```

Impls
-----

`Impl`s bring together `Trait`s and `Struct`s. They represent the implementation details
that satisfy one particular interface.

Why isn't the implementation just all together under the `Struct`? Organization,
mostly. Also, "blanket" `Impl`s can provide implementation for any `Struct` implementing
a given `Trait`, so `Impl`s allow for greater code re-use.

`Impl`s have to indicate which `Struct`s they bestow implementation upon. You can
follow a strict naming convention, like `ImplMyTraitForMyStruct`. This is sufficient.
Or, you can use any name you want so long as you also provide a keyword argument
`target="StructName"` alongside the `metaclass` argument.

```python
from pytrait import Impl

class MyImpl(MyTrait, metaclass=Impl, target="MyStruct"):
...
```

or equivalently,

```python
from pytrait import Impl

class ImplMyTraitForMyStruct(MyTrait, metaclass=Impl):
...
```

This is used to automate the list of implementations for `MyStruct`; you don't need to
explicitly list any superclasses of `MyStruct`, just based on the `Impl` name it will
inherit from all relevant `Impl`s.

FAQ
===

This reminds me of another programming language
-----------------------------------------------

That is not a question, but you have indeed figured me out. This way of organizing
Python code was heavily inspired by the Rust programming language. But beyond being an
imitation, it's a testament to how powerful Python is. My philosophy is that if
you're not using the flexibility of Python to limit yourself, you're not making use of
the full flexibility of Python.

What doesn't work?
------------------

A Struct can't have traits with overlapping method names. Rust can solve this
with its "fully qualified syntax", or by type constraints, but Python will
by default only resolve to the method from the first listed superclass (see
Python's "Method Resolution Order").

I don't think there's any easy way around this, because in Python there's no clear way
to choose which implementation to use based on type annotation. If you _really_ want to
let a `Struct` implement two traits that have the same method name, you can always wrap
your class definition in a try block and catch the `MultipleImplementationError`. Maybe
you can find a way to make it work.