2

I'm trying to find a way to represent a file structure as python objects so I can easily get a specific path without having to type out the string everything. This works for my case because I have a static file structure (Not changing).

I thought I could represent directories as class's and files in the directory as class/static variables.

I want to be able to navigate through python objects so that it returns the path I want i.e:

print(FileStructure.details.file1) # root\details\file1.txt
print(FileStructure.details) # root\details

What I get instead from the code below is:

print("{0}".format(FileStructure())) # root
print("{0}".format(FileStructure)) # <class '__main__.FileStructure'>
print("{0}".format(FileStructure.details)) # <class '__main__.FileStructure.details'>
print("{0}".format(FileStructure.details.file1)) # details\file1.txt

The code I have so far is...

import os 

class FileStructure(object): # Root directory
    root = "root"

    class details(object): # details directory
        root = "details"
        file1 = os.path.join(root, "file1.txt") # File in details directory
        file2 = os.path.join(root, "file2.txt") # File in details directory

        def __str__(self):
            return f"{self.root}"

    def __str__(self):
        return f"{self.root}"

I don't want to have to instantiate the class to have this work. My question is:

  1. How can I call the class object and have it return a string instead of the < class ....> text
  2. How can I have nested classes use their parent classes?
tyleax
  • 1,276
  • 2
  • 12
  • 38
  • 1
    Why not instantiate the class? Is it acceptable if the class is instantiated silently, so you never see the instance? – ShadowRanger Jan 17 '19 at 00:09
  • @ShadowRanger I don't want the user to instantiate it everytime explicitly like `usethisclass = TheClass()`. What do you mean instantiate it silently? – tyleax Jan 17 '19 at 00:15
  • 1
    Side-note: [There is no way for the nested class to use its parent class implicitly](https://stackoverflow.com/q/2024566/364696), so your design is already not really going to work. – ShadowRanger Jan 17 '19 at 00:16
  • If you put `@object.__new__` on the line immediately before `class FileStructure...`, it will silently create an instance of `FileStructure` and assign it to the name `FileStructure` (so the class itself will no longer have a global name, only the instance will exist with that name). – ShadowRanger Jan 17 '19 at 00:17
  • @ShadowRanger So it seems like I have the wrong approach. At least I learnt something new with the object decorator. Thanks! – tyleax Jan 17 '19 at 00:23
  • You have noticed that you'll write more using the solution you're asking than writing directly, the str representation of the path, right? – Raydel Miranda Jan 17 '19 at 00:28
  • Just use `pathlib` don't reinvent the wheel – juanpa.arrivillaga Jan 17 '19 at 01:31
  • @ShadowRanger isn't FileObject = FileObject() a much more explicit way to do that? – juanpa.arrivillaga Jan 17 '19 at 01:34
  • @juanpa.arrivillaga: Absolutely. But you have to type it like, three times, and that's *terrible*. [DRY FTW!!!](https://en.wikipedia.org/wiki/Rule_of_three_(computer_programming)) ;-) – ShadowRanger Jan 17 '19 at 01:41

2 Answers2

4

Let's start with: you probably don't actually want this. Python3's pathlib API seems nicer than this and is already in wide support.

root = pathlib.Path('root')
file1 = root / 'details' / 'file1'  # a Path object at that address

if file1.is_file():
    file1.unlink()
else:
    try:
        file1.rmdir()
    except OSError as e:
        # directory isn't empty

But if you're dead set on this for some reason, you'll need to override __getattr__ to create a new FileStructure object and keep track of parents and children.

class FileStructure(object):
    def __init__(self, name, parent):
        self.__name = name
        self.__children = []
        self.__parent = parent

    @property
    def parent(self):
        return self.__parent

    @property
    def children(self):
        return self.__children

    @property
    def name(self):
        return self.__name

    def __getattr__(self, attr):
        # retrieve the existing child if it exists
        fs = next((fs for fs in self.__children if fs.name == attr), None)
        if fs is not None:
            return fs

        # otherwise create a new one, append it to children, and return it.
        new_name = attr
        new_parent = self
        fs = self.__class__(new_name, new_parent)
        self.__children.append(fs)
        return fs

Then use it with:

root = FileStructure("root", None)
file1 = root.details.file1

You can add a __str__ and __repr__ to help your representations. You could even include a path property

# inside FileStructure
@property
def path(self):
    names = [self.name]
    cur = self
    while cur.parent is not None:
        cur = cur.parent
        names.append(cur.name)
    return '/' + '/'.join(names[::-1])

def __str__(self):
    return self.path
Adam Smith
  • 48,602
  • 11
  • 68
  • 105
  • 2
    fun lil tidbit: \_\_getattr\_\_ is only called if the requested attribute isn't found in the usual way, so `if attr in ['parent', ...]` isn't necessary. ref: https://stackoverflow.com/questions/3278077/difference-between-getattr-vs-getattribute – DeeBee Jan 17 '19 at 09:16
  • 1
    @DeeBee oh interesting! Thanks for that -- I had planned on testing that but ran out of time before I had to submit an answer or get back to work. I was pleasantly surprised that my code even ran when I dropped it into an interpreter later :) – Adam Smith Jan 17 '19 at 17:04
1

Up front: This is a bad solution, but it meets your requirements with minimal changes. Basically, you need instances for __str__ to work, so this cheats using the decorator syntax to change your class declaration into a singleton instantiation of the declared class. Since it's impossible to reference outer classes from nested classes implicitly, the reference is performed explicitly. And to reuse __str__, file1 and file2 were made into @propertys so they can use the str form of the details instance to build themselves.

@object.__new__
class FileStructure(object): # Root directory
    root = "root"

    @object.__new__
    class details(object): # details directory
        root = "details"
        @property
        def file1(self):
            return os.path.join(str(self), 'file1')
        @property
        def file2(self):
            return os.path.join(str(self), 'file2')

        def __str__(self):
            return f"{os.path.join(FileStructure.root, self.root)}"

    def __str__(self):
        return f"{self.root}"

Again: While this does produce your desired behavior, this is still a bad solution. I strongly suspect you've got an XY problem here, but this answers the question as asked.

ShadowRanger
  • 124,179
  • 11
  • 158
  • 228