Mike - SMT
Mike - SMT

Reputation: 15236

Load py files and do something with the class in a for loop from directory without knowing how many files exist?

I am attempting to use a Directory to hold my growing list of frames and when my MAIN.py file is executed I want the code to load each frame into a Tkinter notebook widget.

I can do this manually by importing each file from the sub directory in my MAIN folder however I am attempting to have this load them pragmatically without having to know each file name and not having to import them myself.

I have made some progress but I got stuck at actually loading the class in the files. I can get a list of all files and I believe I am importing them just fine I just cannot get them to load as it keeps telling me that a module does not exist.

I must be misunderstanding something here as I cannot figure out how to properly call a class from the imported file.

Error:

C:\Users\USER\PycharmProjects\TEST\venv\Scripts\python.exe C:/Users/USER/PycharmProjects/TEST/MAIN/MAIN.py
# This below list is a result of my print statement to see if I got all the file names.
['TaskFrame', 'TestFrame1', 'TestFrame2', 'TestFrame3']
Traceback (most recent call last):
  File "C:/Users/MCDOMI3/PycharmProjects/TEST/MAIN/MAIN.py", line 46, in <module>
    ScriptManager().mainloop()
  File "C:/Users/MCDOMI3/PycharmProjects/TEST/MAIN/MAIN.py", line 39, in __init__
    self.add_frame_to_book(foo.tab_title)
AttributeError: module 'ScriptFrames.TaskFrame' has no attribute 'tab_title'

MAIN Code:

import tkinter as tk
import tkinter.ttk as ttk
from os.path import dirname, basename, isfile, join
import glob
import importlib.util

modules = glob.glob(join(dirname('.\ScriptFrames\\'), "*.py"))
__all__ = [basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')]
print(__all__)


class ScriptManager(tk.Tk):

    def __init__(self):
        super().__init__()
        self.book = ttk.Notebook(self)
        self.book.grid(row=0, column=0, sticky='nsew')
        for fn in __all__:
            spec = importlib.util.spec_from_file_location('ScriptFrames.{}'.format(fn),
                                                          '.\ScriptFrames\\{}.py'.format(fn))
            foo = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(foo)
            # Here is my problem I do not know if I am attempting to get the class 
            # attribute correctly here. I am sure that I am calling this wrong.
            self.add_frame_to_book(foo.tab_title)

    def add_frame_to_book(self, fname):
        self.book.add(fname, text='test')


if __name__ == '__main__':
    ScriptManager().mainloop()

Each test file is a simple tkinter frame.

Test Frame Code:

import tkinter as tk
import tkinter.ttk as ttk


class TabFrame(ttk.Frame):

    def __init__(self):
        super().__init__()
        self.tab_title = 'Test Frame 1'
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)
        tk.Label(self, text='test').grid(row=0, column=0, sticky='nsew')

If it helps here is my file structure:

enter image description here

Upvotes: 3

Views: 443

Answers (2)

CristiFati
CristiFati

Reputation: 41167

You're trying to access tab_title for the module, and not from the (Frame) class inside it.

Once the module is loaded, get its attributes using [Python 3.Docs]: Built-in Functions:

  • Get module attribute names (using dir)
    • For each name, get the corresponding attribute (using getattr)
    • Test whether the attribute is a ttk.Frame (as the module has other attributes as well)
      • If it is a frame class, instantiate it in order to access tab_title, otherwise just discard it

Example:

# ...
# The rest of your code

spec.loader.exec_module(foo)

# At this point, 'foo' module is loaded. Get its attributes
module_attrs = [getattr(foo, attr_name, None) for attr_name in dir(foo)]

# Filter out the attributes only keeping the ones that we care about
frame_classes = [attr for attr in module_attrs if isinstance(attr, (type,)) and issubclass(attr, (ttk.Frame,))]

# Then handle the module classes (it will probably be only one)
for frame_class in frame_classes:
    if frame_class.__name__.startswith("Tab"):  # Or any other filtering
    # It would have been much more efficient to filter names when computing attr_names, but it's more consistent to be here (especially if other filtering beside name is needed)
        frame_class_instance = frame_class()  # Instantiate the class, as tab_title is an instance attribute
        print(frame_class.__name__, frame_class_instance.tab_title)

Upvotes: 2

Daniel Huckson
Daniel Huckson

Reputation: 1247

Here is what I do.

def load(self, file=None):
    file_type = path.splitext(file)[1].lstrip('.').lower()

    if file_type == 'py' and path.exists(file):
        spec = spec_from_file_location("module.name", file)
        module = module_from_spec(spec)
        spec.loader.exec_module(module)
        self.nodes = module.config

Upvotes: 0

Related Questions