4

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
truthling
  • 134
  • 10
  • This is not really what `attrs` is for. What exactly are you trying to accomplish? – roeen30 Nov 14 '18 at 22:16
  • @roeen30 I made significant changes to the code examples, which I hope more clearly illustrates the problem I was trying to solve. – truthling Nov 15 '18 at 22:47
  • It is clearer what you're trying to do, but not why. Idiomatic path operations are provided by the excellent `pathlib` - take a look at it if you haven't already. As the main goal of `attrs` is creating quick record classes, it doesn't really help you here, where you have a class with one pre-determined attribute (`root`). Looks like trying to fit a square peg through a round hole. – roeen30 Nov 16 '18 at 19:09
  • 1
    As a side note, if you can't avoid using `__get/setattribute__()`, it is clearer to just use the `getattr()` and `setattr()` builtins. – roeen30 Nov 16 '18 at 19:11

2 Answers2

3

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')
hynek
  • 3,647
  • 1
  • 18
  • 26
0

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.

Cheche
  • 1,456
  • 10
  • 27