Attrs: subclass with mandatory attribute cannot be created when the base class has a factory based one

Created on 11 Apr 2016  Â·  24Comments  Â·  Source: python-attrs/attrs

failing example

import attr

def test_example():

    @attr.s
    class Base(object):
        attr = attr.ib(default=False)

    @attr.s
    class Sub(Base):
        needed = attr.ib()
Feature

Most helpful comment

For anyone stumbling on this issue and still unclear on how to implement a keyword-only attribute, here's the syntax:

@attr.s
class MyClass:
    x = attr.ib(kw_only=True)

All 24 comments

This has been reported before and I’m not sure what to do about it. putting mandatory always before optionals? iow reordering?

they can always be given as keyword

the order should stay the same

it should be possible to give them as keyword instead of a positional argument

once inheritance is involved it seems sensible to go towards keywords in any case

basically all mandatory arguments after a optional one can be primary keyword based

btw, i do in fact use them as keywords, but putting in a default of None seems the wrong way

I kind of agree, but what’s better? having a sentinel and explode on it?

not quite sure what you mean by the sentinel

for the code in the example i posted i think that Sub(needed='foo') should work since all mandatory arguments where given

what you sketched it results in

def __init__(self, attr=False, needed):
    pass

which is a syntax error.

So we need some default value for needed here.

i see what you mean, in python3 its *, and valid, for python2 there is need for a workaround in form of a sentinel object and exploding then

I accidentally opened up a duplicate of this.

My personal opinion™ is that we should just allow users to mark attributes as init='kwonly'. That will allow users to fix the problem themselves on Python 3 and consider upgrading on Python 2. 😽

Full example:

@attr.s
class A:
    a = attr.ib()
    b = attr.ib(init='kwonly', default=1)

    # __init__ signature is:
    def __init__(self, a, *, b=1):
        pass

@attr.s
class B(A):
    c = attr.ib()

    # __init__ signature is:
    def __init__(self, a, c, *, b=1):
        pass

Also, in the case of the error we have now, a helpful error message can direct users to use kwonly.

I ran into this problem as well and I have a slightly different take on it that I wanted to submit. A more natural solution to me would be to write something like:

import attr

def test_example():

    @attr.s
    class Base(object):
        attr = attr.ib(default=False)

    @attr.s
    class Sub(Base):
        needed = attr.ib()
        attr = attr.ib(default=False)

At the cost of a very minor repetition of code, this lets the programmer explicitly control the order in which the attributes should appear for representation purposes or when converting to a tuple. However what I wrote obviously doesn't work in the current version of attr: because of the way inheritance is implemented, Sub receives two 'attr' attributes, which happens to break the __init__ method among other things. I do have a bit of beef with that and was both curious about why attributes don't get overridden in subclasses, and willing to do something about it -then I saw this issue and thought that would be a good starting point for my comment.

how about this:

  • add optional class-level var that defines the order of arguments (as list of strings?)
  • if some required argument encountered in the child, fail with error, requesting explicit order of attributes specified?
  • users can use required arguments in children but must provide order themselves explicitly

since this is starting to turn into a mirad of dozens and dozens of options i believe this should either not be supported for the sake of simplicity or be supported only by disabling positional constructor arguments in the complete inheritance tree

I tend to agree. I’m thinking about an option to @attr.s that changes the mode of __init__ from building a proper signature to def __init__(x=NOTHING, y=NOTHING, z=42, a=NOTHING) and so forth. Then it checks at the beginning if there’s a NOTHING in mandatory attributes. It’s slower but only for people who insist to subclass (I would add electro shocks if I could ;)).

Yea I run into this issue a lot when I have some common optional attributes that I want to share across many classes. e.g.

@attr.s()
class SmallOptions(object):
    verbose = attr.ib(default=None)
    other_option = attr.ib(default=None)

@attr.s()
class Main(SmallOptions):
     required = attr.ib()

I'm kinda inspired by go's embedded struct types:
https://www.goinggo.net/2014/05/methods-interfaces-and-embedded-types.html
http://www.hydrogen18.com/blog/golang-embedding.html
which I'd like to replicate that same sort of composability here. Instead of inheriting from SmallOptions, I'd want class Main to 'embed' SmallOptions instance. Maybe it would look something like so.

@attr.s()
class Main(object):
     required = attr.ib()
     __attrs_embedded__ = {'options': SmallOptions}

with some __getattr__ generated functionality. There would be a SmallOptions instance embedded within Main instance that would be proxied, and can be accessed directly by Main().options as well.

or like this

@attr.s()
class Main(object):
     required = attr.ib()
     options = attr.embed(SmallOptions)

This is a closed issue, and I'm definitely out of my depth as I wade into this discussion, but I'm working (with @asford, fwiw) on a project using attrs, and am getting the "No mandatory attributes after..." error message. I'm at the moment stumped as to what the order of attributes is for my class and which ones have and which lack defaults/factory methods. It would be super useful if the exception printed more about the offending attributes. In my case, I want to know:

what is the first attribute that has a default that appears ahead of an attribute which lacks one

right now, the error message only tells me what attribute lacks a default.

Could you open a feature request please? Comments on closed bugs get inevitably lost.

For anyone stumbling on this issue and still unclear on how to implement a keyword-only attribute, here's the syntax:

@attr.s
class MyClass:
    x = attr.ib(kw_only=True)

it is still unclear what the solution is in python 2, which is not yet deprecated

@hynek can you clarify why this issue is closed even though #411 only tackles python 3?

Because we didn't find a good way to implement it in Python 2 without introducing a forbidding amount of complexity.

While we're committed to maintain Python 2-compatibility as long as it's feasible, we reserve to not implement new features for it if they are either impossible or too painful to implement/maintain.

@hynek while I completely agree, I also think it would be a good idea to state exactly that in the documentation, and/or the error

it is very misleading as the ticket is closed, everything is "fixed", py2 is alive... it makes people think they can get away with a workaround in py2, yet this is not true

Can anyone tell me what is the recommended way now?

Either make the subclass kw_only=True or overwrite the attribute with the default in the subclass:

import attr

def test_example():

    @attr.s
    class Base(object):
        a = attr.ib(default=False)

    @attr.s
    class Sub(Base):
        a = attr.ib()
        needed = attr.ib()

I think this should be working code:

@attr.s
class Base():
    optional = attr.ib( default = False, kw_only = True )
    _internal = attr.ib( default = None, init = False )
@attr.s
class Subclass( Base ):
    required = attr.ib()

It isn't though, currently, because:

  • _internal (which is kw_only = False) can not follow optional (which is kw_only = True), even though _internal is init = False and wouldn't appear in an __init__() arg list anyway. (Is this a separate bug?)

  • required (which is kw_only = False) can not follow optional (which is kw_only = True). While I understand the design here, I think it is an imposition on developers. At the very least it would be nice to be able to opt-in to being able to this with something like:

@attr.s( group_kw_only_last_in_init = True )
class Subclass:
    required = attr.ib()

(Obviously with a better option name than that. :wink:)

My Python-2-Compatible solution is just

def validate_not_none(obj, attr_obj, val):
    assert val is not None, "Attribute '{}' needs to be defined (not None)".format(attr_obj.name)

@attr.s
class Base():
    optional = attr.ib(default=False, kw_only=True)
    _internal = attr.ib(default=None, init=False)

@attr.s
class Subclass(Base):
    required = attr.ib(default=None, validator=validate_not_none)

s = Subclass()  # <- Will fail validator
Was this page helpful?
0 / 5 - 0 ratings

Related issues

2mol picture 2mol  Â·  5Comments

Nicholas-Mitchell picture Nicholas-Mitchell  Â·  15Comments

altendky picture altendky  Â·  12Comments

hynek picture hynek  Â·  12Comments

smarie picture smarie  Â·  14Comments