0

In my Python programm I want to dynamically load modules and access variables of the module by converting a string parameter into a variable name.

Use Case

I have different Fonts on SD Card which are python files, and a display function which loads the font when needed to display the caracters.

Examples for my font file:

# arial14.py
# ch_(ASCII) = (widht), (height), [bitmask]
ch_33 = 3, 16, [0,0,0,0,0,0,0,0,0,1,1,1,1,1 ........
ch_34 = 5, 16, [0,0,0,0,0,0,0,0,0,0,0,0,0,0 ........
....

# arial20.py
ch_33 = 4, 22, [0,0,0,0,0,0,0,0,0,1,1,1,1,1 ........
ch_34 = 8, 22, [0,0,0,0,0,0,0,0,0,0,0,0,0,0 ........

Further, there is a Writer class which renders the fonts to the display:

class Writer(object):
    def __init__(self):
       try:
           global arial14
           import arial14
           self.current_module = "arial14"
           self.imported_fonts = []
           self.imported_fonds.append(self.current_module)
      except ImportError:
           print("Error loading Font")
   def setfont(self, fontname, fontsize):
           modname = fontname+str(fontsize)
           if modname not in self.importedfonts:
               try:
                    exec("global" + modname)
                    exec("import" + modname)      #import the font (works)
                    self.importedfonts.append(modname)
               except ImportError:
                    print("Error loading Font")
                    return
           else:
               print(modname+" was already imported")
           self.current_module = modname
           print("Font is set now to: "+ self.current_module

## HERE COMES THE NON WORKING PART:
    def putc_ascii(self, ch, xpos, ypos):

           execline = "width, height, bitmap = "+ self.cur_mod+".ch_"+str(ch)
           print(execline) 
           #this example.: width, height,bitmap = arial14.ch_55


           width, height,bitmap = arial14.ch_32
           print(width, height, bitmap) # values of arial14.ch_32 -> correct

           exec (execline)
           print(width, height, bitmap) # values of arial14.ch_32
                                        # BUT VALUES OF arial14.ch_55 EXPECTED

Has anybody an idea how can I accomplish to save the correct values of the queried character of the correct font into the variables width, height and bitmap?

I want to load the Fonts dynamically only if needed, and offer the possibility to add new fonts by putting new .py font files into the folder.

Thanks in advance.

Antikhippe
  • 6,316
  • 2
  • 28
  • 43
A. L
  • 131
  • 2
  • 12

1 Answers1

2

EDIT

The OP is actually using micropython, which doesn't implement importlib...

Possible (untested) solution (yes, using exec - if someone knows a better solution please chime in).

def import_module(name):
    name = name.strip().split(
    statement = "import {}"
    exec(statement, globals()) 
    return sys.modules[name]


class Writer(object):
    def __init__(self):
       # reference to the current module
       # WARNING : this is the `module` object itself, 
       # not it's name       
       self.current_module = None

       # {name:module} mapping
       # not sure it's still useful since we
       # now have a reference to the module itself
       # and `sys.modules` already caches imported 
       # modules... 
       self.imported_fonts = {}

       # set default font
       self.setfont("arial", "14")

    def setfont(self, fontname, fontsize):
        self.current_module = self._loadfont(fontname, fontsize)

    def _loadfont(self, fontname, fontsize):
        name = fontname+str(fontsize)
        if name not in self.imported_fonts:
            self.imported_fonts[name] = load_module(name)
        return self.imported_font[name]

    def putc_ascii(self, ch, xpos, ypos):
        width, height, bitmap = self._get_char(ch)
        print("{}.{}: {} - {} - {}".format(
            self.current_module, ch, width, height, bitmap
            )

    def _get_char(self, chrnum):
        # assume the font modules have been rewritten
        # using dicts (cf lower) and all chars are defined
        # in all fonts modules
        return self.current_module[ch]

        # alternate implementation using the current
        # fonts definitions
        # return getattr(self.current_module, "ch_{}".format(ch)) 

TL:DR :

You want importlib.import_module and eventually getattr(). But you should still read the longer answer, really, it will save you a lot of time and frustration.

Longer answer:

First point about your "font" files format - this:

ch_33 = 3, 16, [0,0,0,0,0,0,0,0,0,1,1,1,1,1 ........
ch_34 = 5, 16, [0,0,0,0,0,0,0,0,0,0,0,0,0,0 ........

is a huge design smell. You want lists or dicts instead (or possibly an ordered dict), ie:

characters = {
    33: (3, 16,  [0,0,0,0,0,0,0,0,0,1,1,1,1,1...]),
    34: (5, 16,  [0,0,0,0,0,0,0,0,0,0,0,0,0,0...]),
    # etc
    }

As a general rule, when you start having some "var1", "var2", "var3" etc pattern then you know you want some kind of container instead.

Second point - your error handling, ie:

 try:
       global arial14
       import arial14
       self.current_module = "arial14"
       self.imported_fonts = []
       self.imported_fonds.append(self.current_module)
  except ImportError:
       print("Error loading Font")

is worse than useless, it's actually harmful. First because it doesn't stop program execution in the case of an unrecoverable error (you dont want your program to continue when something totally wrong happened), then because it replaces all the incredibly useful informations you'd either get from the error message and traceback with a plain useless "An error happened" message.

Only catch exceptions that you can properly manage one way or another, let everything else propagate (maybe someone upper in the call stack - usually in the UI part - might be able to handle it properly).

Third point: do not use globals. I mean: do not mutate nor rebind globals (read-only (pseudo-constant) globals are ok of course). Which means you really never have to use the "global" keyword in your code.

When you need to share state between a group a functions, use a class, store the state as attributes and make your functions methods. That's what objects are for (well, not only but that's part of there raison d'être).

EDIT : this part is still true for complete Python implementation and probably still partly true for micropython, except for parts that are not implemented in micropython (like importlib - I don't know what else is missing)

And finally: never ever use exec nor eval. You don't need them, whatever you're trying to do there's a better, safer specific solution.

In your case, to import a module by it's name, you have importlib.import_module, and to get an object's attribute by it's name you have getattr() (but if you use a proper container, cf first point, you don't even need getattr here).

bruno desthuilliers
  • 75,974
  • 6
  • 88
  • 118
  • Thanks for the reply. With the error handling part you are fully right, I forgot to insert code there. In my original code there is a return statement there. The other points I will consider and adjust the implementation. The problem with importlib is, that I am developping on an embedded device with Micropython, and I think there does not exist this module. But I will try it tomorrow when i'm at my workplace. Is there a way without this module to load modules and variables with not predetermined names at runtime (at the moment I use the REPL to send the command to load and draw fonts). – A. L Mar 22 '18 at 18:12
  • Looks like there's no importlib in micropython indeed(judging from this : https://pypi.python.org/pypi/micropython-importlib/0.0.0) . You should really have specified you were working on micropython actually. – bruno desthuilliers Mar 23 '18 at 11:27
  • I edited my answer with a possible cleaner implementation, still using `exec` to import fonts (well yes if you don't have `importlib` nor any replacement you can hardly avoid `exec` here) – bruno desthuilliers Mar 23 '18 at 12:04
  • Merci beacoup pour votre aide! You´re right, i should have said that i´m developping on Micropython, but I will consider the points in your reply though. Now i figured it out how to solve the problem. Instead of exec which normally returns None (in my case it returns undetermined values) I had to use eval, which is working now. At the moment I have to set the fontname with exec("global "+fontname) to global, but I will try around a little to keep the variables local. – A. L Mar 23 '18 at 12:06
  • De rien - and if this solves your problem then feel free to upvote and accept the answer ;) – bruno desthuilliers Mar 23 '18 at 12:07
  • Das ist kein problem ;) – bruno desthuilliers Mar 23 '18 at 12:21