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.
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.