Plugin registries
Some parts of an application are open-ended: export formats, payment methods, file importers. Instead of an if chain that grows with each one, keep a registry that maps a name to an implementation, and let each plugin register itself with a decorator.
class Registry:
def __init__(self):
self.items = {}
def register(self, name):
def deco(fn):
self.items[name] = fn
return fn
return deco
exporters = Registry()
@exporters.register("csv")
def to_csv(rows):
lines = [",".join(r) for r in rows]
return "\n".join(lines)
print(exporters.items["csv"]([["a", "b"]]))Code that uses the registry asks for a name and never imports the plugins, so adding a format means adding a function, not editing a dispatcher.
Make the registry an object rather than a module-level dict, so each test can build its own. Installed packages use the same idea at a larger scale, through entry points.
Your task
Write Registry with register(name), a decorator that returns the function unchanged and raises ValueError if the name is taken, get(name) raising a LookupError whose message lists the available names, and names() returning them sorted. Register "csv" and "json" exporters on a module-level exporters registry and write export(fmt, rows) using it.
The explanation opens here once every check passes.