Repository: encode/httpx

Issue:
Public API tightening
Following on from #1065

There's a few attributes that are set in `BaseClient.__init__` that look like contenders here for moving into the private API space. Here's what's remaining there...

- [x] `self.base_url` - Potentially modify this to a property setter / getter style, but not 100% obvious?
- [ ] `self.auth` - Potentially modify this to a property setter / getter style, but not 100% obvious? - _See #1185_
- [x] `self.timeout` - Looks like it should *definitely* move to a property getter / setter style, as per params/headers/cookies.
- [x] `self.max_redirects` - I think this one is okay as a public attribute, since it's just a plain int.
- [x] `self.trust_env` - Setting this after `__init__` isn't really valid (eg. the transport is already initialized) so we could move it to a read-only property? _See #1112_
- [x] `self.netrc` - Ought to be private. _See #1071_


Discussion:
Dropping the milestone on this from 0.14 to 1.0, since the remaining items here are pretty minor, but we should still track them.

Resolving patch:
diff --git a/httpx/_client.py b/httpx/_client.py
--- a/httpx/_client.py
+++ b/httpx/_client.py
@@ -71,7 +71,7 @@ def __init__(
     ):
         self._base_url = self._enforce_trailing_slash(URL(base_url))
 
-        self.auth = auth
+        self._auth = self._build_auth(auth)
         self._params = QueryParams(params)
         self._headers = Headers(headers)
         self._cookies = Cookies(cookies)
@@ -117,6 +117,21 @@ def timeout(self) -> Timeout:
     def timeout(self, timeout: TimeoutTypes) -> None:
         self._timeout = Timeout(timeout)
 
+    @property
+    def auth(self) -> typing.Optional[Auth]:
+        """
+        Authentication class used when none is passed at the request-level.
+
+        See also [Authentication][0].
+
+        [0]: /quickstart/#authentication
+        """
+        return self._auth
+
+    @auth.setter
+    def auth(self, auth: AuthTypes) -> None:
+        self._auth = self._build_auth(auth)
+
     @property
     def base_url(self) -> URL:
         """
@@ -284,19 +299,25 @@ def _merge_queryparams(
             return merged_queryparams
         return params
 
-    def _build_auth(
+    def _build_auth(self, auth: AuthTypes) -> typing.Optional[Auth]:
+        if auth is None:
+            return None
+        elif isinstance(auth, tuple):
+            return BasicAuth(username=auth[0], password=auth[1])
+        elif isinstance(auth, Auth):
+            return auth
+        elif callable(auth):
+            return FunctionAuth(func=auth)
+        else:
+            raise TypeError('Invalid "auth" argument.')
+
+    def _build_request_auth(
         self, request: Request, auth: typing.Union[AuthTypes, UnsetType] = UNSET
     ) -> Auth:
-        auth = self.auth if isinstance(auth, UnsetType) else auth
+        auth = self._auth if isinstance(auth, UnsetType) else self._build_auth(auth)
 
         if auth is not None:
-            if isinstance(auth, tuple):
-                return BasicAuth(username=auth[0], password=auth[1])
-            elif isinstance(auth, Auth):
-                return auth
-            elif callable(auth):
-                return FunctionAuth(func=auth)
-            raise TypeError('Invalid "auth" argument.')
+            return auth
 
         username, password = request.url.username, request.url.password
         if username or password:
@@ -667,7 +688,7 @@ def send(
         """
         timeout = self.timeout if isinstance(timeout, UnsetType) else Timeout(timeout)
 
-        auth = self._build_auth(request, auth)
+        auth = self._build_request_auth(request, auth)
 
         response = self._send_handling_redirects(
             request, auth=auth, timeout=timeout, allow_redirects=allow_redirects,
@@ -1269,7 +1290,7 @@ async def send(
         """
         timeout = self.timeout if isinstance(timeout, UnsetType) else Timeout(timeout)
 
-        auth = self._build_auth(request, auth)
+        auth = self._build_request_auth(request, auth)
 
         response = await self._send_handling_redirects(
             request, auth=auth, timeout=timeout, allow_redirects=allow_redirects,


Test patch:
diff --git a/tests/client/test_auth.py b/tests/client/test_auth.py
--- a/tests/client/test_auth.py
+++ b/tests/client/test_auth.py
@@ -9,6 +9,7 @@
     URL,
     AsyncClient,
     Auth,
+    BasicAuth,
     Client,
     DigestAuth,
     ProtocolError,
@@ -310,14 +311,34 @@ async def test_auth_hidden_header() -> None:
 
 
 @pytest.mark.asyncio
-async def test_auth_invalid_type() -> None:
+async def test_auth_property() -> None:
+    client = AsyncClient(transport=AsyncMockTransport())
+    assert client.auth is None
+
+    client.auth = ("tomchristie", "password123")  # type: ignore
+    assert isinstance(client.auth, BasicAuth)
+
     url = "https://example.org/"
-    client = AsyncClient(
-        transport=AsyncMockTransport(),
-        auth="not a tuple, not a callable",  # type: ignore
-    )
+    response = await client.get(url)
+    assert response.status_code == 200
+    assert response.json() == {"auth": "Basic dG9tY2hyaXN0aWU6cGFzc3dvcmQxMjM="}
+
+
+@pytest.mark.asyncio
+async def test_auth_invalid_type() -> None:
+    with pytest.raises(TypeError):
+        client = AsyncClient(
+            transport=AsyncMockTransport(),
+            auth="not a tuple, not a callable",  # type: ignore
+        )
+
+    client = AsyncClient(transport=AsyncMockTransport())
+
+    with pytest.raises(TypeError):
+        await client.get(auth="not a tuple, not a callable")  # type: ignore
+
     with pytest.raises(TypeError):
-        await client.get(url)
+        client.auth = "not a tuple, not a callable"  # type: ignore
 
 
 @pytest.mark.asyncio