ionrock/cachecontrol — pull request #41 by bukzor (2014-09-16 22:59:16+00:00)
Title: avoid infinite regress in __getattr__

After a large amount of debugging a deadlock in pip's test suite, I found an infinite recursion in the below __getattr__ method. I've fixed it and regression-tested it as best I could.

It's not clear to me how an attribute of this class is referenced after its __fp is deleted, but it did happen.

For historicity's sake, this is the stack that caused the inifinite recursion:

```python
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/workspace/venv/bin/pip", line 9, in
load_entry_point('pip==1.6.dev1', 'console_scripts', 'pip')()
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/__init__.py", line 198, in main
return command.main(cmd_args)
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/basecommand.py", line 212, in main
status = self.run(options, args)
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/commands/install.py", line 317, in run
requirement_set.prepare_files(finder)
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/req/req_set.py", line 238, in prepare_files
req_to_install, self.upgrade)
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/index.py", line 284, in find_requirement
for page in self._get_pages(url_locations, req):
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/index.py", line 391, in _get_pages
page = self._get_page(location, req)
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/index.py", line 611, in _get_page
result = HTMLPage.get_page(link, req, session=self.session)
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/index.py", line 693, in get_page
"Cache-Control": "max-age=600",
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/requests/sessions.py", line 463, in get
return self.request('GET', url, **kwargs)
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/download.py", line 286, in request
result = super(PipSession, self).request(method, url, *args, **kwargs)
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/requests/sessions.py", line 451, in request
resp = self.send(prep, **send_kwargs)
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/requests/sessions.py", line 557, in send
r = adapter.send(request, **kwargs)
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/cachecontrol/adapter.py", line 38, in send
return self.build_response(request, cached_response, from_cache=True)
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/cachecontrol/adapter.py", line 95, in build_response
request, response
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/requests/adapters.py", line 203, in build_response
response.headers = CaseInsensitiveDict(getattr(resp, 'headers', {}))
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/requests/structures.py", line 46, in __init__
self.update(data, **kwargs)
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/workspace/venv/lib/python2.7/_abcoll.py", line 541, in update
for key in other:
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/requests/packages/urllib3/response.py", line 304, in closed
elif hasattr(self._fp, 'isclosed'): # Python 2
File "/tmp/pytest-29/test_upgrade_user_conflict_in_globalsite0/pip_src/pip/_vendor/cachecontrol/filewrapper.py", line 27, in __getattr__
f.write(''.join(traceback.format_stack()))
```

I believe that gc is activating during Mapping.update, and jumping to response.closed. Why it would touch a property, I don't know. Further, this is happening after something (the gc I assume) has deleted __fp from the filewrapper. Because filewrapper references self.__fp in __getattr__, and __fp isn't present, it recurses there. Why I don't get a StackOverflowError rather than a busy deadlock, I also don't know.

But, this fixes it :D