Repository: encode/httpx

Issue:
Cache NETRC logins after first lookup
As noted in #364, looking up NETRC logins is eating 25% of request time (!) — see `get_netrc_login()` below (times are absolute for a series of 1000 requests).

The `.netrc` file typically won't change across a session. So while we can't avoid paying that performance cost on the first request, we can definitely cache the NETRC credentials on the client once we looked them up (even if they don't exist).

This improvement would give us a 25% performance boost (on localhost, so ignoring network latency), getting us closer to aiohttp speeds [according to this benchmark](https://github.com/sonic182/aiosonic#aiosonic).

![](https://user-images.githubusercontent.com/15911462/65360506-7efaff00-dc00-11e9-97d7-146c2f5776cb.png)


Discussion:
Could you assign me this topic if someone else is not interested?
Sure @cansarigol, go for it!

Resolving patch:
diff --git a/httpx/client.py b/httpx/client.py
--- a/httpx/client.py
+++ b/httpx/client.py
@@ -1,5 +1,6 @@
 import functools
 import inspect
+import netrc
 import typing
 from types import TracebackType
 
@@ -48,7 +49,7 @@
     ResponseContent,
     URLTypes,
 )
-from .utils import ElapsedTimer, get_environment_proxies, get_netrc_login
+from .utils import ElapsedTimer, get_environment_proxies, get_netrc
 
 
 class BaseClient:
@@ -277,13 +278,20 @@ def _get_auth_middleware(
             )
 
         if trust_env:
-            netrc_login = get_netrc_login(request.url.authority)
-            if netrc_login:
-                username, _, password = netrc_login
-                return BasicAuthMiddleware(username=username, password=password)
+            netrc_info = self._get_netrc()
+            if netrc_info:
+                netrc_login = netrc_info.authenticators(request.url.authority)
+                if netrc_login:
+                    username, _, password = netrc_login
+                    assert password is not None
+                    return BasicAuthMiddleware(username=username, password=password)
 
         return None
 
+    @functools.lru_cache(1)
+    def _get_netrc(self) -> typing.Optional[netrc.netrc]:
+        return get_netrc()
+
     def _dispatcher_for_request(
         self, request: AsyncRequest, proxies: typing.Dict[str, AsyncDispatcher]
     ) -> AsyncDispatcher:
diff --git a/httpx/utils.py b/httpx/utils.py
--- a/httpx/utils.py
+++ b/httpx/utils.py
@@ -94,7 +94,7 @@ def guess_json_utf(data: bytes) -> typing.Optional[str]:
 NETRC_STATIC_FILES = (Path("~/.netrc"), Path("~/_netrc"))
 
 
-def get_netrc_login(host: str) -> typing.Optional[typing.Tuple[str, str, str]]:
+def get_netrc() -> typing.Optional[netrc.netrc]:
     NETRC_FILES = (Path(os.getenv("NETRC", "")),) + NETRC_STATIC_FILES
     netrc_path = None
 
@@ -106,9 +106,7 @@ def get_netrc_login(host: str) -> typing.Optional[typing.Tuple[str, str, str]]:
 
     if netrc_path is None:
         return None
-
-    netrc_info = netrc.netrc(str(netrc_path))
-    return netrc_info.authenticators(host)  # type: ignore
+    return netrc.netrc(str(netrc_path))
 
 
 def get_ca_bundle_from_env() -> typing.Optional[str]:


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -10,7 +10,7 @@
     ElapsedTimer,
     get_ca_bundle_from_env,
     get_environment_proxies,
-    get_netrc_login,
+    get_netrc,
     guess_json_utf,
     obfuscate_sensitive_headers,
     parse_header_links,
@@ -54,24 +54,24 @@ def test_guess_by_bom(encoding, expected):
 
 
 def test_bad_get_netrc_login():
-    assert get_netrc_login("url") is None
-
     os.environ["NETRC"] = "tests/.netrc"
-    assert get_netrc_login("url") is None
-
-    os.environ["NETRC"] = "wrongpath"
-    assert get_netrc_login("url") is None
+    assert str(get_netrc()) is not None
 
     from httpx import utils
 
     utils.NETRC_STATIC_FILES = ()
+
+    os.environ["NETRC"] = "wrongpath"
+    assert utils.get_netrc() is None
+
     os.environ["NETRC"] = ""
-    assert utils.get_netrc_login("url") is None
+    assert utils.get_netrc() is None
 
 
 def test_get_netrc_login():
     os.environ["NETRC"] = "tests/.netrc"
-    assert get_netrc_login("netrcexample.org") == (
+    netrc = get_netrc()
+    assert netrc.authenticators("netrcexample.org") == (
         "example-username",
         None,
         "example-password",