2

I'm trying to import all the Frames I have inside a folder as a package and then initialize all those frames so I can just raise them on a button press.

This is my folder structure:

+-projectfolder
|--bargraphtutor.py
|-- __init__.py (empty)
|--pages
   |--startpage.py
   |-- .
   |-- .
   |--aboutpage.py
   |--__init__.py

The __init__.py in the pages folder has the following code to package all the .py files in that folder into pages. That was taken from this question.

from os.path import dirname, basename, isfile, join
import glob

pages = glob.glob(join(dirname(__file__), "*.py"))
__all__ = [ basename(f)[:-3] for f in pages if isfile(f) and not f.endswith('__init__.py')]
from . import *

And then I get to do import pages as p. The issue is that, each frame is a class in the file and to initialize each frame I have to know the name of each file and class name:

Example: Part of bargraphtutor.py

self.frames = {}
for F in (p.aboutpage.AboutPage, p.startpage.StartPage): # This is where I'd like to make as 
    page_name = F.__name__                               # automated as possible. 
    frame = F(parent=container, controller=self)
    self.frames[page_name] = frame

    # put all of the pages in the same location;
    # the one on the top of the stacking order
    # will be the one that is visible.
    frame.grid(row=0, column=0, sticky="nsew")

Example: startpage.py:

import tkinter as tk   # python 3
from tkinter import ttk
from tkinter.ttk import Label, Button
from tkinter import font


class StartPage(tk.Frame):   # Each Frame is defined via a class

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        titleLabel= ttk.Label(self, text='This is the Startpage')
        titleLabel.pack()

So how can I import the package and then iterate over all the Frames in bargraphtutor.py but without knowing all the class names? I got as far as using p.__all__ which returns the names of all the files in the package but I don't know how to move forward from there.

Edit: If I named the file the same as the class, would I run into problems with namespaces?

Community
  • 1
  • 1
SRR
  • 1,608
  • 1
  • 21
  • 51
  • The simplest would be you name all your `class (...` same as your `module` file name.Then you can do: `for F in <__all__>:`. Note the `<...>` are placeholders. – stovfl Mar 20 '20 at 19:46
  • @stovfl I was thinking about that but I wasn't sure if would cause some kind of conflict with namespaces? Even if I named the class the same name as the file, wouldn't I still have to do . ? – SRR Mar 20 '20 at 19:48
  • ***"problems with namespaces?"***: No, it's common used. For example, `from dateteim import datetime`. The module filename is `datetime.py` and inside a `class datetime`. – stovfl Mar 20 '20 at 19:55
  • @stovfl okay I understand. Won't I still have to use `..` though? Or will naming the classes the same as the filename allow for `.` – SRR Mar 20 '20 at 20:11

2 Answers2

2

Overview

The way I would solve this is to have all of my pages inherit from a base class so that I could use the __subclasses__ function to get a list of all subclasses once they've been imported. To import them I would use python's glob module to find the files and the importlib module to import the files by their paths.

Example file structure

For example, let's start with this simple folder structure:

.
├── main.py
└── pages
    ├── PageOne.py
    ├── PageTwo.py
    ├── __init__.py
    └── basepage.py

__init__.py is empty, but lets us treat pages as a module.

basepage.py defines the class BasePage, from which all other pages inherit from. It doesn't need to be much, since each page will be responsible for everything inside the page. It might look something like this:

import tkinter as tk

class BasePage(tk.Frame):
    def __init__(self, master, controller):
        self.master = master
        self.controller = controller
        super().__init__(master)

PageOne.py and PageTwo.py contain the pages. They have a similar structure. For example, PageOne.py might look something like this:

import tkinter as tk
from .basepage import BasePage

class PageOne(BasePage):
    def __init__(self, parent, controller):
        super().__init__(parent, controller)
        label = tk.Label(self, text="This is page one")
        label.pack(padx=20, pady=20)

Page two is identical, except it says "two" instead of "one" in the obvious places. Notice how this page inherits from BasePage. This is important, as you'll see in a minute.

Getting a list of files to import

You can use the glob module to get a list of all files in the "pages" subdirectory. It would look something like this:

import glob
for filename in glob.glob("./pages/*.py"):
    ...

If your actual folder has files that are pages and files that are not, you can use a naming convention so that you only import page files. For example, you could change the pattern to ".pages/Page*.py".

Importing a file by its filename

Python's importlib module has functions which allow us to import files by filename.

For example, given a filename in filename, we can import that file like this:

import importlib.util
from pathlib import Path

path = Path(filename)
module_name = f"pages.{path.stem}"
spec = importlib.util.spec_from_file_location(module_name, path)
spec.loader.exec_module(module)

If we pass in something like ./pages/PageOne.py it will load the module under the name pages.PageOne.

Getting the page classes

Once we've imported one or more pages, here's where we use the BasePage class. Remember how every page inherits from this class? We can use the __subclasses__ method of a subclass to give us a list of all subclasses.

Putting that all together, here's a function that imports all files matching "Page*.py" in the "pages" subfolder and then returns the classes:

def get_pages(self):
    for filename in glob.glob("./pages/*.py"):
        path = Path(filename)
        module_name = f"pages.{path.stem}"
        print(f"module name: {module_name}")
        spec = importlib.util.spec_from_file_location(module_name, path)
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)

    return BasePage.__subclasses__()

Creating the page instances

At this point, creating the page instances is pretty easy. Your main program could do something like this:

    self.frames = {}
    for page_class in self.get_pages():
        page = page_class(parent=page_container, controller=self)
        page_name = page_class.__name__
        self.frames[page_name] = page
        page.grid(row=0, column=0, sticky="nsew")
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • Thank you for the very thorough explanation! Just one thing, isn't using `exec` considered bad practice? – SRR Mar 20 '20 at 23:31
  • 1
    @S.Ramjit: this example doesn't use `exec`. At least, not in the way you're thinking of it. You're asking about the built-in function [exec](https://docs.python.org/3/library/functions.html?highlight=exec#exec), but this code uses the [exec_module](https://docs.python.org/3/library/importlib.html#importlib.abc.Loader.exec_module) method of the loader object. They are two different functions. And yes, the built-in `exec` method should be avoided. However, "avoided" isn't the same as "never use". There are right ways and wrong ways to use both of these functions. – Bryan Oakley Mar 20 '20 at 23:40
1

Question: Dynamically initialize Frames in a folder in Tkinter

Core Point

Get the module from pages.__init__, then get the page class.

cls = getattr(getattr(pages, module_name), page_name)

Reference


Note: I don't pass a controller reference as it's the same as class App.


import tkinter as tk
import pages


class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.geometry("200x200")

        menubar = tk.Menu(self, tearoff=0)
        self.configure(menu=menubar)

        self.grid_rowconfigure(0, weight=1)
        self.grid_columnconfigure(0, weight=1)

        self.pages = {}
        for filename in pages.__all__:
            module_name, page_name = filename, filename
            cls = getattr(getattr(pages, module_name), page_name)

            self.pages[page_name] = frame = cls(self)

            frame.grid(row=0, column=0, sticky="nsew")
            menubar.add_command(label=page_name, command=frame.tkraise)

        # The following statements are equivalent 
        self.pages['StartPage'].tkraise()

        # From within a page object
        # self.master.pages['StartPage'].tkraise()


if __name__ == "__main__":
    App().mainloop()

Tested with Python: 3.5 - 'TclVersion': 8.6 'TkVersion': 8.6

stovfl
  • 14,998
  • 7
  • 24
  • 51