A cautionary tale of unexpectedly defined attributes
I recently made a change in which I cleaned up the way a class, QzEnvironment, computes a thing. It was cleanly implemented and with comprehensive test coverage, and though it added functionality I was sure it didn't modify the existing public API of the classes. Once released, this had the strangest effect: users reported problems in existing production code, with a traceback like
Traceback (most recent call last): ... File "/qz/grid/hugs_cfg.py", line 81, in resolve self.srcdb = env.srcdbPath(env.stage) or '' AttributeError: 'QzEnvironment' object has no attribute 'stage'
This was bizarre. QzEnvironment never did have an attribute .stage – that was the preserve of related class QzEnvURI. Yet there lay the production code, clearly accessing the .stage attribute of a QzEnvironment. Had I somehow triggered a codepath that hadn't been triggered before? No. At the version before my change, the debugger showed that env.stage was 'prod', which is a sensible value for a stage – but not something this class would know.
In frustration, I read the diff of my change again. Absolutely did not touch an attribute called self.stage. Never was an attribute called stage.
I hunted high and low – was something external to our code rudely assigning a .stage to the QzEnvironment ? I couldn't find it with a project search. I even made .stage a read-only property, in the hope that it would throw and the traceback would reveal how the stage attribute had been assigned.
@property def stage(self): raise AttributeError("QzEnvironment has no attribute 'stage'")
Alas, this merely threw the exception again at the same line – indicating that QzEnvironment did indeed have no attribute .stage – it had never been assigned, nor indeed accessed. Even more confusing!
I read the diff of my change again. And again.

Finally, I saw something. One of the changes was to pull a lot of the configuration out of class attributes into module-level constants at the top of the file, for easier reading and maintainability. These lines were removed from the class definition:
--- qzenv.py 1.105 +++ qzenv.py 1.106 @@ -37,7 +37,4 @@ class QzEnvironment(object): ... - __AREA_DBS = dict([ (stage, ('quackPermissionedHotfix', 'legacyHotfix', 'prod', 'source') if stage == 'prod' else ('stage', 'source')) - for stage in qz_cfg.QZ_SRC_ALLOWED_ENVIRONMENTS ]) - def __init__(self, ...):
And it hit me.

Comments
Comments powered by Disqus