Dynamically define attributes that depend on earlier defined attributes

337 views Asked by At

I want an object that represents a path root and an arbitrary number of subdirectories that are constructed with os.path.join(root). I want to access these paths with the form self.root, self.path_a, self.path_b, etc... In addition to accessing them directly via self.path_a, I want to be able to iterate over them. Unfortunately, the approach below does not allow iterating over them via attr.astuple(paths)

The first bit of code below is what I came up with. It works but feels a little hacky to me. Since this is my first use of attrs I am wondering if there is a more intuitive/idiomatic way to approach this. It took me quite a while to figure out how to write the fairly simple class below, so I thought I might be missing something obvious.

My approach

@attr.s
class Paths(object):
    subdirs = attr.ib()
    root = attr.ib(default=os.getcwd())
    def __attrs_post_init__(self):
        for name in self.subdirs:
            subdir = os.path.join(self.root, name)
            object.__setattr__(self, name, subdir)

    def mkdirs(self):
        """Create `root` and `subdirs` if they don't already exist."""
        if not os.path.isdir(self.root):
            os.mkdir(self.root)
        for subdir in self.subdirs:
            path = self.__getattribute__(subdir)
            if not os.path.isdir(path):
                os.mkdir(path)

Output

>>> p = Paths(subdirs=['a', 'b', 'c'], root='/tmp')
>>> p
Paths(subdirs=['a', 'b', 'c'], root='/tmp')
>>> p.a
'/tmp/a'
>>> p.b
'/tmp/b'
>>> p.c
'/tmp/c'

The following was my first attempt, which doesn't work.

Failed attempt

@attr.s
class Paths(object):
    root = attr.ib(default=os.getcwd())
    subdir_1= attr.ib(os.path.join(root, 'a'))
    subdir_2= attr.ib(os.path.join(root, 'b'))

Output

------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-31-71f19d55e4c3> in <module>()
    1 @attr.s
----> 2 class Paths(object):
    3     root = attr.ib(default=os.getcwd())
    4     subdir_1= attr.ib(os.path.join(root, 'a'))
    5     subdir_2= attr.ib(os.path.join(root, 'b'))

<ipython-input-31-71f19d55e4c3> in Paths()
    2 class Paths(object):
    3     root = attr.ib(default=os.getcwd())
--> 4     subdir_1= attr.ib(os.path.join(root, 'a'))
    5     subdir_2= attr.ib(os.path.join(root, 'b'))
    6

~/miniconda3/lib/python3.6/posixpath.py in join(a, *p)
    76     will be discarded.  An empty last part will result in a path that
    77     ends with a separator."""
--> 78     a = os.fspath(a)
    79     sep = _get_sep(a)
    80     path = a

TypeError: expected str, bytes or os.PathLike object, not _CountingAttr
2

There are 2 answers

1
Cheche On

Can't guess why would you like to access as self.paths.path. But, here is what i'd do:

class D(object):
    root = os.getcwd()
    paths = dict()

    def __init__(self, paths=[]):
        self.paths.update({'root': self.root})
        for path in paths:
            self.paths.update({path: os.path.join(self.root, path)})

    def __str__(self):
        return str(self.paths)    

d = D(paths=['static', 'bin', 'source'])
print(d)
print(d.paths['bin'])

output

{'root': '/home/runner', 'static': '/home/runner/static', 'bin': '/home/runner/bin', 'source': '/home/runner/source'}
/home/runner/bin

You can make this more complex. Just an example. Hope it helps.

0
hynek On

First attempt: you can't just attach random data to the class and hope that attrs (in this case astuple) will pick it up. attrs specifically tries to avoid magic and guessing, which means that you have to indeed define your attributes on the class.

Second attempt: you cannot use attribute name in the class scope (i.e. within class Paths: but outside of a method because – as Python tells you – at this point they are still internal data that is used by @attr.s.

The most elegant approach I can think of is a generic factory that takes the path as an argument and builds the full path:

In [1]: import attr

In [2]: def make_path_factory(path):
   ...:     def path_factory(self):
   ...:         return os.path.join(self.root, path)
   ...:     return attr.Factory(path_factory, takes_self=True)

Which you can use like this:

In [7]: @attr.s
   ...: class C(object):
   ...:     root = attr.ib()
   ...:     a = attr.ib(make_path_factory("a"))
   ...:     b = attr.ib(make_path_factory("b"))

In [10]: C("/tmp")
Out[10]: C(root='/tmp', a='/tmp/a', b='/tmp/b')

In [11]: attr.astuple(C("/tmp"))
Out[11]: ('/tmp', '/tmp/a', '/tmp/b')

attrs being attrs, you can of course go further and define your own attr.ib wrapper:

In [12]: def path(p):
    ...:     return attr.ib(make_path_factory(p))

In [13]: @attr.s
    ...: class D(object):
    ...:     root = attr.ib()
    ...:     a = path("a")
    ...:     b = path("b")
    ...:

In [14]: D("/tmp")
Out[14]: D(root='/tmp', a='/tmp/a', b='/tmp/b')