Repository: encode/httpx

Issue:
aiter_lines() doesn't return full lines that span multiple chunks
<https://gist.github.com/scr-oath/aa76d200222a0409d09a0d6feb1a13e2> shows an example setup using cherry.py as server that just outputs two lines - the json is big enough to be sent in two chunks; httpx aiter_lines() gets confused and sends data from the middle of the json line - seems to skip the starting part - which was most likely sent in a chunk without a newline

### test-httpx.py
```python
import asyncio
import json

import httpx


class TestHttpx:
    def __init__(self):
        pass

    async def __call__(self):
        http_client = httpx.AsyncClient()
        async with http_client.stream(method="GET", url='http://localhost:8080/lines') as response:
            is_message = True
            async for line in response.aiter_lines():
                is_message = not is_message
                if is_message:
                    message = json.loads(line)
                    print(message)


def main():
    test_httpx = TestHttpx()
    asyncio.run(test_httpx())


if __name__ == '__main__':
    main()
```

Discussion:

Patch:
diff --git a/httpx/_client.py b/httpx/_client.py
index 1ee9352..d4ec7aa 100644
--- a/httpx/_client.py
+++ b/httpx/_client.py
@@ -80,7 +80,7 @@ class BaseClient:
         self.timeout = Timeout(timeout)
         self.max_redirects = max_redirects
         self.trust_env = trust_env
-        self.netrc = NetRCInfo()
+        self._netrc = NetRCInfo()
 
     def _get_proxy_map(
         self, proxies: typing.Optional[ProxiesTypes], trust_env: bool,
@@ -269,7 +269,7 @@ class BaseClient:
             return BasicAuth(username=username, password=password)
 
         if self.trust_env and "Authorization" not in request.headers:
-            credentials = self.netrc.get_credentials(request.url.authority)
+            credentials = self._netrc.get_credentials(request.url.authority)
             if credentials is not None:
                 return BasicAuth(username=credentials[0], password=credentials[1])
 
diff --git a/httpx/_decoders.py b/httpx/_decoders.py
index 1ea47b0..d1c60fb 100644
--- a/httpx/_decoders.py
+++ b/httpx/_decoders.py
@@ -233,12 +233,18 @@ class LineDecoder:
     def decode(self, text: str) -> typing.List[str]:
         lines = []
 
-        if text.startswith("\n") and self.buffer and self.buffer[-1] == "\r":
-            # Handle the case where we have an "\r\n" split across
-            # our previous input, and our new chunk.
-            lines.append(self.buffer[:-1] + "\n")
-            self.buffer = ""
-            text = text[1:]
+        if text and self.buffer and self.buffer[-1] == "\r":
+            if text.startswith("\n"):
+                # Handle the case where we have an "\r\n" split across
+                # our previous input, and our new chunk.
+                lines.append(self.buffer[:-1] + "\n")
+                self.buffer = ""
+                text = text[1:]
+            else:
+                # Handle the case where we have "\r" at the end of our
+                # previous input.
+                lines.append(self.buffer[:-1] + "\n")
+                self.buffer = ""
 
         while text:
             num_chars = len(text)
diff --git a/httpx/_models.py b/httpx/_models.py
index 892a959..dca3eff 100644
--- a/httpx/_models.py
+++ b/httpx/_models.py
@@ -87,10 +87,6 @@ class URL:
             if not self.host:
                 raise InvalidURL("No host included in URL.")
 
-        # Allow setting full_path to custom attributes requests
-        # like OPTIONS, CONNECT, and forwarding proxy requests.
-        self._full_path: typing.Optional[str] = None
-
     @property
     def scheme(self) -> str:
         return self._uri_reference.scheme or ""
@@ -138,17 +134,11 @@ class URL:
 
     @property
     def full_path(self) -> str:
-        if self._full_path is not None:
-            return self._full_path
         path = self.path
         if self.query:
             path += "?" + self.query
         return path
 
-    @full_path.setter
-    def full_path(self, value: typing.Optional[str]) -> None:
-        self._full_path = value
-
     @property
     def fragment(self) -> str:
         return self._uri_reference.fragment or ""


Test patch:
diff --git a/tests/models/test_url.py b/tests/models/test_url.py
index f9a568a..7910a8e 100644
--- a/tests/models/test_url.py
+++ b/tests/models/test_url.py
@@ -177,13 +177,6 @@ def test_url_set():
     assert all(url in urls for url in url_set)
 
 
-def test_url_full_path_setter():
-    url = URL("http://example.org")
-
-    url.full_path = "http://example.net"
-    assert url.full_path == "http://example.net"
-
-
 def test_origin_from_url_string():
     origin = Origin("https://example.com")
     assert origin.scheme == "https"
diff --git a/tests/test_decoders.py b/tests/test_decoders.py
index 89c545b..6b79931 100644
--- a/tests/test_decoders.py
+++ b/tests/test_decoders.py
@@ -247,14 +247,13 @@ def test_line_decoder_cr():
     assert decoder.flush() == ["c\n"]
 
     # Issue #1033
-    # TODO: This seems like another bug; fix expectations and results.
     decoder = LineDecoder()
     assert decoder.decode("") == []
     assert decoder.decode("12345\r") == []
-    assert decoder.decode("foo ") == []
+    assert decoder.decode("foo ") == ["12345\n"]
     assert decoder.decode("bar ") == []
     assert decoder.decode("baz\r") == []
-    assert decoder.flush() == ["12345\rfoo bar baz\n"]
+    assert decoder.flush() == ["foo bar baz\n"]
 
 
 def test_line_decoder_crnl():