3

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,556
  • 2
  • 17
  • 45
  • 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

3 Answers3

6

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
  • 52,157
  • 12
  • 73
  • 112
  • 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
  • 143,180
  • 12
  • 188
  • 271
0

There exists such class in HTMLgen:

import HTMLutil

class Directory(UserList)
  def __cmp__(self, item)
  def __init__(self, name='root', data=None)
  def add_object(self, pathlist, object)
  def ls(self, pad='')
  def tree(self)

  # Methods inherited by Directory from UserList
  def __add__(self, list)
  def __delitem__(self, i)
  def __delslice__(self, i, j)
  def __getitem__(self, i)
  def __getslice__(self, i, j)
  def __len__(self)
  def __mul__(self, n)
  def __mul__(self, n)
  def __radd__(self, list)
  def __repr__(self)
  def __setitem__(self, i, item)
  def __setslice__(self, i, j, list)
  def append(self, item)
  def count(self, item)
  def index(self, item)
  def insert(self, i, item)
  def remove(self, item)
  def reverse(self)
  def sort(self, *args)

Unfortunately, this package is very old. When I tried to understand code for Directory.__cmp__, I failed. It is supposed to compare directory structures, but it calls a function cmp for that, and where that function comes from is unclear to me (types in Python 3 have no such function). I'm not sure it works any longer.

I thought about creating such package myself to test directory hierarchies (I created a file synchronization program). However, as suggested in answers above, pathlib might be a better approach, as well as dircmp from filecmp and rmtree from shutil.

The downside of these standard modules is that they are low-level, so if someone creates a library to deal with all these tasks in one place, that would be great.

Yaroslav Nikitenko
  • 1,695
  • 2
  • 23
  • 31