Search code examples
pythonpython-3.xintrospection

From a list, dynamically create methods with name-awareness


I'm trying to write a simple console color utility that takes a class full of ANSI codes and generates some helper methods on my console utility, so that instead of doing console.add('text', 'blue'), I can do console.blue('text').

I know I can define all of these statically (e.g. def blue(self, s):), but that doesn't really scale well if I want to add 100 or so more helpers (not that I would, but if...)

Here's the simple ANSI map:

class _AnsiColors:
    def __init__(self):
        self.green = 35
        self.red = 1
        self.blue = 32
        self.yellow = 214
        self.amber = 208
        self.olive = 106
        self.orange = 166
        self.purple = 18
        self.pink = 197
        self.gray = 243
        self.dark_gray = 238
        self.light_gray = 248
        self.black = 0
        self.white = 255
        self.debug = 24

ansi = _AnsiColors()

And the console utility (which proxies methods to pyfancy and uses colors):

import copy 

from colors import color    
from pyfancy import *

from ansi import ansi

class console(object):
    def __init__(self, s):
        self._s = pyfancy(s)

    def add(self, s, c='white'):
        if hasattr(ansi, self.add.__name__):
            c = self.add.__name__
        self._s.add(color(s, fg=getattr(ansi, c)))
        return self

    def bold(self, s):
        self._s.bold(s)
        return self

    def raw(self, s):
        self._s.raw(s)  
        return self

    def dim(self, s):
        self._s.dim(s)      
        return self

    def print(self):
        self._s.output()

# Inject ansi color convenience methods
for c in vars(ansi):
    setattr(console, c, copy.deepcopy(console.add))
    getattr(console, c).__name__ = c

Then I can use it like so:

console('raw').bold(' bold').raw(' raw').blue(' blue').red(' red').print()

You can see that the helper methods blue and red at least execute, so my copying of add() works, but what's happening here (even though I thought I could solve it with copy.deepcopy), is that when I try and set the __name__ property of each method copy, it's setting the reference to add instead, and I end up with all of the colored output being the same color (ansi.debug).

Is there a way to do what I'm trying to do without statically defining each helper?


MCVE without colors/pyfancy:

import copy 

from ansi import ansi

class console(object):
    def __init__(self, s):
        self._s = s

    def add(self, s, c='white'):
        if hasattr(ansi, self.add.__name__):
            c = self.add.__name__
        self._s += '%s(%s)' % (s, c)
        return self

    def print(self):
        print(self._s)

# Inject ansi color convenience methods
for c in vars(ansi):
    setattr(console, c, copy.deepcopy(console.add))
    getattr(console, c).__name__ = c

console('white').blue(' blue').red(' red').print()

# white blue(debug) red(debug)

Solution

  • I would solve it with a closure:

    class console(object):
        def __init__(self, s):
            self._s = s
            for color in vars(ansi):
                self._colorizer(color)
    
        def _colorizer(self, c):        
            def add(s):
                self._s += '%s(%s)' % (s, c)
                return self
    
            self.__setattr__(c, add)
    
        def print(self):
            print(self._s)
    
    console('white').blue(' blue').red(' red').print()
    # white blue(blue) red(red)