python-attrs / attrs

Python Classes Without Boilerplate
https://www.attrs.org/
MIT License
5.3k stars 372 forks source link

Support abstract functionality for attrs classes #976

Open zplizzi opened 2 years ago

zplizzi commented 2 years ago

I'd love to do something like:

@attr.s(auto_attribs=True, frozen=True)
class Parent:
    @abstract
    param: str

@attr.s(auto_attribs=True, frozen=True)
class Child(Parent):
    @override
    param = "test"

where you wouldn't be able to create an instance of a class with @abstract decorators, and you wouldn't be able to put the @override decorator on something that wasn't defined in the parent.

I commonly use attrs classes to hold configuration parameters, and allowing this functionality would make it much safer and more readable. Currently it's too easy to accidentally think you're overriding something in Child that's actually not defined in Parent (typo, got removed, etc) - and it's not clear when reading Child whether an attribute is new to the child or just overriding a default value from the parent. And leaving a value without a default in Parent is a little annoying because you have to put all the attrs without default first (which is not a big deal, but disrupts the logical grouping of members of the class).

I have no idea if this is remotely possible, so feel free to close if not!

hynek commented 2 years ago

What you want is impossible for the reason alone that you can't apply decorators to type annotations. Type annotations don't really exist in the classical sense (you can't use them for our decorator syntax of defaults and validators for that reason).

I'm also not 100% sure what you're trying to achieve. Do you want to crate an abstract class that defines fields and then fill them out in the body of the child class? Why not use an instance?

P.S. are you aware that you can just do @attr.frozen (or@attrs.frozen)?

hkclark commented 2 years ago

I don't know if this helps, but one thig I have done when I really want to have an abstract "attribute/field" (in quotes because I'm sort of faking it here) is:

from abc import ABC, abstractmethod
import attrs

@attrs.frozen(kw_only=True, slots=False)
class Parent(ABC):
    @abstractmethod
    def param(self):
        pass

@attrs.frozen(kw_only=True, slots=False)
class Child(Parent):
    param = attrs.field(default="test")

Yeah, the abstractmethod isn't attrs-specific (or even attribute/field specific), but so far it has worked for me and made I don't forget to define a given attribute/field in a subclass.

HTH, KC

zplizzi commented 2 years ago

Yeah, understood - I figured this wouldn't really be possible at least with the syntax I suggested. But @hkclark I appreciate your suggestion - definitely hacky but it gets at what I was looking for. Maybe there is some other syntax that could be added to get this functionality in a neater way?

Here's a little example of what I'm trying to do (sorry for the silly example, it's lunchtime!):

@attr.frozen
class FoodTruck:
    @abstract
    food_kind: str
    num_wheels: int = 4
    has_grill: bool = False
    serves_alcohol: bool = False

@attr.frozen
class HalalFoodTruck(FoodTruck):
    @override
    food_kind = "halal"
    @override
    has_grill = True
    rice_kind: str = "basmati"
    sauce_kind: List[str] = ["white"]

@attr.frozen
class DeluxeHalalFoodTruck(HalalFoodTruck):
    @override
    num_wheels = 8
    @override
    serves_alcohol = True
    @override
    sauce_kind = ["white", "spicy", "teriyaki"]

The reason for not using an instance is, for example, you can't further subclass an instance, but you can with a class. And with the subclass approach, you can both change default attributes / fill in abstract attributes, and also add new attributes, all in the same "container".

hynek commented 2 years ago

I'm just on my phone, but what happens if you declare the attributes as typing.ClassVar[str]? Attrs will leave them alone and that should be what you're asking for?

zplizzi commented 2 years ago

Hmm, sorta - but not quite. I lose the ability to do Child(param="stuff"), and it also doesn't solve the "avoiding typos" or "making clear which attrs in the child are overrides vs new". And a reader wouldn't know that typing.ClassVar[str] means "this parameter must be overridden".