Enable sessions to bypass creating/parsing cookies.

We use requests for some service-to-service communication, neither side
of which utilizes cookies.  When we profiled our services, we see ~13%
of the time spend sending requests is either creating cookie jars or
extracting cookies to jars.

This PR will enable us to disable cookies for a given `requests.Session`
by setting `session.cookies = None`.  If `sessions.cookies` is None, we
won't create any cookies for the request nor will we parse them on the
response.

# [Codecov](https://codecov.io/gh/requests/requests/pull/3998?src=pr&el=h1) Report
> :exclamation: No coverage uploaded for pull request base (`master@5697148`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference#section-missing-base-commit).
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/requests/requests/pull/3998/graphs/tree.svg?src=pr&token=mZGswIiXLa&width=650&height=150)](https://codecov.io/gh/requests/requests/pull/3998?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##             master    #3998   +/-   ##
=========================================
  Coverage          ?   89.79%           
=========================================
  Files             ?       15           
  Lines             ?     1950           
  Branches          ?        0           
=========================================
  Hits              ?     1751           
  Misses            ?      199           
  Partials          ?        0
```


| [Impacted Files](https://codecov.io/gh/requests/requests/pull/3998?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [requests/models.py](https://codecov.io/gh/requests/requests/pull/3998?src=pr&el=tree#diff-cmVxdWVzdHMvbW9kZWxzLnB5) | `93.45% <100%> (ø)` | |
| [requests/sessions.py](https://codecov.io/gh/requests/requests/pull/3998?src=pr&el=tree#diff-cmVxdWVzdHMvc2Vzc2lvbnMucHk=) | `93.97% <100%> (ø)` | |
| [requests/adapters.py](https://codecov.io/gh/requests/requests/pull/3998?src=pr&el=tree#diff-cmVxdWVzdHMvYWRhcHRlcnMucHk=) | `92.92% <100%> (ø)` | |
| [requests/cookies.py](https://codecov.io/gh/requests/requests/pull/3998?src=pr&el=tree#diff-cmVxdWVzdHMvY29va2llcy5weQ==) | `77.68% <100%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/requests/requests/pull/3998?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/requests/requests/pull/3998?src=pr&el=footer). Last update [5697148...8a902cf](https://codecov.io/gh/requests/requests/pull/3998?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Thanks for this suggestion!

So, before I go any further, I'd like to get an understanding of your results. You say that 13% of the time is spent "either creating cookie jars or extracting cookies to jars", but also that your services never use cookies. I find that somewhat tricky to believe: in the event that there are no cookies there will be relatively little processing of the absent cookies.

Firstly, the most expensive things we do in general are related to environment evaluation: checking for proxies, reading `.netrc` files, etc. So I assume you're already setting `trust_env` to `False` to avoid that overhead.

Secondly, as an attempt to reproduce your results, I profiled Requests making 1000 requests through a single Session to a Twisted instance serving a directory on my machine, with `trust_env=False`. This sets no cookies. The total runtime of the test was 4 seconds. Here's the profiling output, sorted by cumulative time, for any function that we spend more than 100ms in:

```

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.018    0.018    4.038    4.038 test.py:1(<module>)
     1000    0.007    0.000    3.942    0.004 sessions.py:492(get)
     1000    0.020    0.000    3.935    0.004 sessions.py:411(request)
     1000    0.029    0.000    3.283    0.003 sessions.py:569(send)
     1000    0.019    0.000    3.007    0.003 adapters.py:375(send)
     1000    0.032    0.000    2.536    0.003 connectionpool.py:446(urlopen)
     1000    0.047    0.000    2.099    0.002 connectionpool.py:321(_make_request)
     1000    0.018    0.000    1.709    0.002 httplib.py:1084(getresponse)
     1000    0.022    0.000    1.674    0.002 httplib.py:431(begin)
     8000    0.053    0.000    1.503    0.000 socket.py:410(readline)
     1000    0.022    0.000    1.477    0.001 httplib.py:392(_read_status)
     1000    1.409    0.001    1.409    0.001 {method 'recv' of '_socket.socket' objects}
     1000    0.020    0.000    0.570    0.001 sessions.py:371(prepare_request)
     1000    0.008    0.000    0.341    0.000 models.py:297(prepare)
     1000    0.005    0.000    0.305    0.000 httplib.py:1040(request)
     1000    0.018    0.000    0.300    0.000 httplib.py:1067(_send_request)
     1000    0.015    0.000    0.291    0.000 adapters.py:240(build_response)
     5001    0.020    0.000    0.203    0.000 structures.py:42(__init__)
     1000    0.013    0.000    0.199    0.000 response.py:437(from_httplib)
     7000    0.020    0.000    0.192    0.000 sessions.py:42(merge_setting)
     1000    0.002    0.000    0.191    0.000 httplib.py:1025(endheaders)
     3009    0.010    0.000    0.190    0.000 socket.py:227(meth)
     1000    0.005    0.000    0.189    0.000 httplib.py:867(_send_output)
     1000    0.007    0.000    0.182    0.000 httplib.py:840(send)
     1000    0.169    0.000    0.169    0.000 {method 'sendall' of '_socket.socket' objects}
     1000    0.010    0.000    0.162    0.000 mimetools.py:24(__init__)
    85456    0.066    0.000    0.155    0.000 {isinstance}
     8004    0.041    0.000    0.140    0.000 _abcoll.py:548(update)
     1000    0.007    0.000    0.137    0.000 models.py:784(content)
     1000    0.004    0.000    0.135    0.000 rfc822.py:88(__init__)
     1000    0.045    0.000    0.132    0.000 httplib.py:271(readheaders)
     2000    0.015    0.000    0.129    0.000 cookies.py:121(extract_cookies_to_jar)
     1000    0.005    0.000    0.129    0.000 connectionpool.py:214(_get_conn)
    19027    0.008    0.000    0.122    0.000 {method 'join' of 'str' objects}
     1000    0.016    0.000    0.122    0.000 _collections.py:307(from_httplib)
     1000    0.006    0.000    0.121    0.000 adapters.py:277(get_connection)
     2000    0.004    0.000    0.114    0.000 models.py:715(generate)
      999    0.004    0.000    0.110    0.000 connection.py:7(is_connection_dropped)
     2000    0.005    0.000    0.109    0.000 response.py:411(stream)
     1000    0.022    0.000    0.109    0.000 models.py:350(prepare_url)
      999    0.002    0.000    0.105    0.000 wait.py:29(wait_for_read)
      999    0.010    0.000    0.102    0.000 wait.py:9(_wait_for_io_events)
     1000    0.019    0.000    0.100    0.000 models.py:591(__init__)
     1000    0.019    0.000    0.100    0.000 response.py:342(read)
```

As you can see, cookies *are* in there, but only in one place (`extract_cookies_to_jar`), with a total runtime of 129ms. Now, admittedly, I suspect the content we're sending back and forth is longer in my case than in yours, so we can assume that you'd spend less time in `socket.recv`, but even if we assumed you spent *zero* time there we don't get anywhere near to cookies being 30% of the overhead. In the case of my test, the other cookie-based code that is not part of `extract_cookies_to_jar` adds maybe another 80ms of overhead, leading to 209ms. To be pessimistic, let's double that estimate to 420ms. 420ms spread across 1000 requests is 420μs: not an entirely unreasonable overhead, given that we spend about 1/4 of that time checking that that the connection is still down.

So, certainly if we were going to add this feature I'd want the code to be *vastly* simpler. I'd also want to ask whether or not it's easier in your case simply to patch the `extract_cookies_to_jar` function to be a no-op.

@Lukasa Thanks a ton for your detailed analysis!  I really appreciate all of the work you put into evaluating this 😄 .

Here's a snippit of flame graph (Generated using https://github.com/uber/pyflame) from one of our services running in production, filtering out to only focus on `requests`:
[only_requests.zip](https://github.com/kennethreitz/requests/files/979777/only_requests.zip)

Here's a screenshot, highlighting all stacks that contain the word `cookies`:
<img width="1203" alt="screen shot 2017-05-05 at 9 53 53 am" src="https://cloud.githubusercontent.com/assets/808798/25755693/d97b28fe-3178-11e7-966a-271f44b6051a.png">

(If you're not comfortable opening random zips, here's the raw output -
 https://gist.github.com/rowillia/e7232fc12e9010c4663811d2166a1228 , run it through https://github.com/brendangregg/FlameGraph to get an SVG)

Thanks for the `trust_env` tip, I noticed how expensive `netrc` and merging environment settings were but haven't had a chance to change things in production.

Any pointers on how we can make the code simpler?  This is my first requests PR so I'm not super familiar with the coding style here beyond what's in the docs.

Making it simpler would focus on moving the complexity into helper functions that deal with this. For example, `extract_cookies_to_jar` could be enhanced to handle `None` arguments, keeping this complexity out of the main code paths.

Thanks for the suggestion @Lukasa, I've simplified the implementation to encapsulate some of the complexity inside of `cookies.py`.

One other thing I wanted to run by you: setting `cookies` to `None` to avoid parsing/creating cookies feels somewhat magic to me.  Do you think it would be better if there were simply a `discard cookies` flag?  The main motivator for structuring like this is the behavior I'd expect if the `session` has no cookies but the `request` does is we would pass through the requests cookies and having `discard cookies` set to `True` while specifying cookies would be illegal (I tend to be a big fan of the idea that illegal states should be unrepresentable when possible).

@Lukasa one additional thought I just had reading your comment - I'm most worried about optimizing CPU consumption, not wall time.  Totally agree that this will have minimal impact on wall time, but for some services that simply farm out a bunch of requests and wait for their response (using gevent) the CPU consumed here is non-trivial.

For servers using threads, I worry that the RLock on the cookiejar is creating contention so it'd be best if we could avoid it all together.

> Do you think it would be better if there were simply a discard cookies flag?

No. =) We strongly, strongly resist adding API surface because it adds to the possible confusion of interacting with Requests. This goes double for situations like yours which are extremely uncommon: adding an entire additional API property for a bit of function that fewer than 0.5% of users will ever use is just complete overkill. I'd much rather add a subtle use of an existing property.

> (I tend to be a big fan of the idea that illegal states should be unrepresentable when possible).

What makes this state illegal? Setting `session.cookies = None` requests that no cookies be *persisted*, but there's no reason to prevent you putting cookie headers in the request. In fact, this is a good example as to why we don't want to add such a flag: it is unclear what exactly it applies to, and there are numerous other flags and options that it *could* apply to, so I'd say it impedes clarity more than anything else.

> For servers using threads, I worry that the RLock on the cookiejar is creating contention so it'd be best if we could avoid it all together.

It probably is, but not by much. After all, we have a lock in the connection pool too, though admittedly it's one in the default configuration that you can't contend on (more a semaphore than a lock in that case). Merely *having* the lock, even if you don't contend on it, is very expensive in Python.

I agree that minimising CPU time is important, but if you want to do that I *strongly* encourage you to look away from Requests. Requests' attempts to provide you with a helpful, intuitive API involve a *lot* of Python code that is strictly overhead. If you can ensure that your HTTP data will always be ready to go and require basically zero transformation you may find urllib3 is a better fit for your needs.

I don't think you pushed your changes. :smile:

@Lukasa yeah jumped the gun on the comments 😄 

Can you rebase this onto the current master for me? It's having some trouble with testing because it's not mergeable.

I really like this suggestion, as well as the API proposed. Very Requests. 

add to whitelist

add to whitelist

add to whitelist

Given that it's been almost a year since we've seen progress on this PR, I'm going to close this out. Please feel free to reopen if you'd like to continue. Thanks!