attrs icon indicating copy to clipboard operation
attrs copied to clipboard

attr.Factory provided with the attribute beeing initialized

Open flocreate opened this issue 5 years ago • 6 comments

Hi there,

I'm looking for a way to read default value for an attribute from os.environ

The natural way i see for this is to use a attr.Factory in combination with attr.converters.default_if_none. This factory would call os.environ with some env-key built with attribute's name or metadata.

But i dont see how this factory could be provided with the attribute's name or meta-data.

The other way i tried to was to build a custom decorator

def env_binding(prefix):
    def modify_ib_default(attribute):
        env_key = (prefix + '_' + attribute.name).upper()
        attribute.default = os.environ.get(env_key, attribute.default)
        return attribute

    def wrap(cls):
        new_cls = attr.s(cls)
        new_cls.__attrs_attrs__ = tuple(map(modify_ib_default, new_cls.__attrs_attrs__))
        return new_cls
    return wrap

but since Attribute instances are read-only (setattr --> raise FrozenInstanceError), this method is not valid...

Note: I know the nice python attr-based module config-environ but I need a different behavior.

flocreate avatar Sep 23 '20 13:09 flocreate

I’m not 100% sure I understand your use-case, but do you know about the takes_self option of Factory?

hynek avatar Sep 24 '20 09:09 hynek

Hi, if i'm correct, the takes_self provies de instance being built, not the attribute beeing managed.

flocreate avatar Sep 24 '20 13:09 flocreate

Yes, but you can introspect self.__class__ using attr.fields() and attr.fields_dict()?

hynek avatar Sep 24 '20 13:09 hynek

To be more specific, i would like in a Factory to know what attribute is beeing managed (in any converter, validator).

I see two usages for this: The first one, specific, beeing able to load env variable using the attribute's name. The second one, mandatory in my optinion, would be to have exceptions that tell you what attribute is invalid.

About introspection, i dont realy understand what you have in mind. Here is some code maybee i dont see something obvious

import attr

def my_factory(): 
    def _inner(self): 
        print("self:", self) 
        print("fields:", attr.fields(type(self))) 
        print("fdict:", attr.fields_dict(type(self))) 
    return attr.Factory(_inner, True) 

@attr.s 
class A: 
    a = attr.ib(my_factory()) 
    b = attr.ib(my_factory()) 

A()

So, when the factory is called for the attribute a:

self: A(a=NOTHING, b=NOTHING)
fields: (
    Attribute(name='a', default=Factory(factory=<function my_factory.<locals>._inner at 0x7f8535e1c510>, takes_self=True), validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), 
    Attribute(name='b', default=Factory(factory=<function my_factory.<locals>._inner at 0x7f8535e1c378>, takes_self=True), validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False)
)
fdict: {
    'a': Attribute(name='a', default=Factory(factory=<function my_factory.<locals>._inner at 0x7f8535e1c510>, takes_self=True), validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), 
    'b': Attribute(name='b', default=Factory(factory=<function my_factory.<locals>._inner at 0x7f8535e1c378>, takes_self=True), validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False)
}

And when factory is called for attribute b:

self: A(a=None, b=NOTHING)

fields: (
    Attribute(name='a', default=Factory(factory=<function my_factory.<locals>._inner at 0x7f8535e1c510>, takes_self=True), validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False),     
    Attribute(name='b', default=Factory(factory=<function my_factory.<locals>._inner at 0x7f8535e1c378>, takes_self=True), validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False)
)
fdict: {
    'a': Attribute(name='a', default=Factory(factory=<function my_factory.<locals>._inner at 0x7f8535e1c510>, takes_self=True), validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False), 
    'b': Attribute(name='b', default=Factory(factory=<function my_factory.<locals>._inner at 0x7f8535e1c378>, takes_self=True), validator=None, repr=True, cmp=True, hash=None, init=True, metadata=mappingproxy({}), type=None, converter=None, kw_only=False)
}
A(a=None, b=None)

I see no way in the factory to infer that it is applied for a or for b.

flocreate avatar Sep 24 '20 14:09 flocreate

What i mean by having better factory, validator, converter messages is:

import attr

@attr.s
class A:
    a = attr.ib(converter=int)
    b = attr.ib(converter=int)
  1. A() -> TypeError: __init__() missing 2 required positional arguments: 'a' and 'b'
  2. A(b=1) -> TypeError: __init__() missing 1 required positional argument: 'a'
  3. A('str', 1) -> ValueError: invalid literal for int() with base 10: 'str'

Cases 1 and 2 are ok, but in case 3 how the user (dev) will know that the error applies to attribute a ?

flocreate avatar Sep 24 '20 14:09 flocreate

Right hm I guess you're asking for a takes_field=False argument, for generalized factories?

hynek avatar Sep 28 '20 10:09 hynek