ionelmc / python-fields

A totally different take on container boilerplate.
BSD 2-Clause "Simplified" License
138 stars 8 forks source link

Allow callable defaults for any sealer #6

Open dimitern opened 7 years ago

dimitern commented 7 years ago

First let me say - great library! :) I've been evaluating it as well as attrs for a while now.

I'd like to suggest a possible feature extension: allow callable defaults, which will let you do things like this:

from datetime import datetime
import fields
class Record(fields.Tuple.Username[str].Created[datetime.now].Revision[long].Active[bool]):
    pass

Using types as defaults IMO makes the code more readable. Also assuming the callable takes no arguments, it allows simple initialization at run-time (like with datetime.now).

It's easy to do that with a simple sealer wrapper, like the one below:

from functools import wraps
from fields import factory, tuple_sealer

def with_callable_defaults(sealer):
    """
    Wraps the given `sealer` to support `callable` for any specified default.
    """
    @wraps(sealer)
    def wrapped_sealer(fields, defaults):
        for field, default in defaults.iteritems():
            if callable(default):
                try:
                    defaults[field] = default()
                except TypeError:
                    pass
        return sealer(fields, defaults)
    return wrapped_sealer

Tuple = factory(with_callable_defaults(tuple_sealer))

What do you think?

ionelmc commented 7 years ago

Another way, that can also keep backwards compat a bit is to (ab)use slices for defaults as callables, eg:

class Record(fields.Tuple.Username[""].Created[:datetime.now].Revision[None].Active[False):
    pass

or even with strings for more possibilities (the init function is made from a string, alike namedtuple, so this is possible):

class Record(fields.Tuple.Username[:"getpass.getuser()"].Created[:"datetime.now()"].Revision[None].Active["Revision!=None"):
    pass
ionelmc commented 7 years ago

Actually both forms could be supported.

Anyway, doesn't attrs already support default values from callables?

dimitern commented 7 years ago

I was thinking of abusing the slice syntax at first :) Could be useful for things like validators, converters, or even docstrings, esp. if combined with your suggestion about supporting strings:


class Record(fields.Tuple
             .WithoutDefault
             .WithConstDefault["foo"]
             .WithCallableDefault[:"datetime.now().isoformat()"]
             .WithDocstring[None, """The doc"""]
             .AllInOne["bar", """Bar doc"""]): 
    pass

r = Record(1)
assert r.WithDocString.__doc__ == """The Doc"""
assert r.WithCallableDefault == "2000-01-02T03:04:05"

Yeah, attrs supports callable defaults and lots more, but I like the concise declarative style of fields better I think :)

I'll look into it some more, cheers!

ionelmc commented 7 years ago

Well yes, fields is not meant to replace attrs, or even compete with it. As a matter of fact it precedes attrs (it's practically a jab at characteristic). You may have guessed it, attrs probably wouldn't exist if I hadn't done fields xD

Intended use is very small classes (5 fields or less). If you can't fit it on one line then probably you shouldn't use fields at all - the whole idea of extreme brevity become a moot point if you split the definition on multiple lines in ugly jquery chain style.