Taming Class Scopes
This is part of the [tracer series](/tag/tracer), so have a look at its [introduction](/post/tracer-tricks) first. ![pause](/media/images/pause.png) I once worked on a project where configurations were managed in classes. Personally, I prefer dictionaries, or even external JSON files; but I figured, when in Rome, right? ```python class config: root = '/app/' foo_dir = root + 'foo/' bar_dir = root + 'bar/' assert config.foo_dir == '/app/foo/' assert config.bar_dir == '/app/bar/' ``` Jolly. Let's add a log, because logging is fabulous. ```python class config: root = '/app' foo_dir = os.path.join(root, 'foo') bar_dir = os.path.join(root, 'bar') class log: path = os.path.join(root, 'log.txt') level = 'DEBUG' ``` Except, this code ``NameError``s with ``name 'root' is not defined``. In the gentle words of Gary Bernhardt: [wat](https://www.destroyallsoftware.com/talks/wat). Turns out that for some obscure reason, scoping doesn't work the way you'd expect it to when it comes to classes. The reasonable thing to do, of course, would be to flatten the class, like so: ```python class config: root = '/app/' foo_dir = root + 'foo/' bar_dir = root + 'bar/' log_path = root + 'log.txt' log_level = 'DEBUG' ``` Or maybe move the necessary attributes to the global scope, which is available to everyone, like so: ```python root = '/app/' class config: foo_dir = root + 'foo/' bar_dir = root + 'bar/' class log: path = root + 'log.txt' level = 'DEBUG' ``` But if we were a reasonable species, we probably wouldn't have landed on the moon, or funded the Fidget Cube's kickstarter. So let's do what humans do: solve this problem by any means necessary. And then probably use it to wage war on one another. ![pause](/media/images/pause.png) Remember the [cenum](/post/reclaiming-enums) and [ordered](/post/taming-class-attributes) decorators? Let's use the same trick again, except this time a little differently: we'll register a tracer right before the outermost class is evaluated, and keep it on until that class and all its nested classes are evaluated, at which point we'll unregister it. During that time, whenever a ``call`` event happens - that is, whenever we enter a new scope - that is, whenever a new nested class is defined - we'll manually take whatever's defined in its parent scope, and shove it up its, uh, locals dictionary. ```python class nestedclasses(object): def __init__(self): self.old = sys.gettrace() sys.settrace(self.tracer) def tracer(self, frame, event, arg): if event == 'call': for key, value in frame.f_back.f_locals.items(): if key not in frame.f_locals: frame.f_locals[key] = value return self.tracer def __call__(self, cls): sys.settrace(self.old) return cls ``` And there you have it: we create a ``nestedclasses`` instance, and in its ``__init__`` we register ``tracer``, which is thereafter invoked on every single line. Namely, it's invoked on every class definition (in the confusingly named ``call`` event), gets its frame before any evaluation happens, and pre-populates it with everything that was defined so far. When the class evaluation is complete, its object is passed to ``__call__`` for decoration, and there we unregister ``tracer`` and return the class as-is. ```python @nestedclasses() class config: root = '/app/' foo_dir = root + 'foo/' bar_dir = root + 'bar/' class log: path = root + 'log.txt' level = 'DEBUG' assert config.log.path == '/app/log.txt' ``` One small step for man, one giant leap for mankind. You can get the code on [GitHub](http://github.com/dan-gittik/nestedclasses).