Repository: pex-tool/pex

Issue:
Error when parsing a .netrc file with a possibly nonstandard token
My ~/.netrc file has 

```
machine github.com
  login benjyw
  password <redacted>
  protocol https
```

The `protocol` token isn't mentioned in any netrc documentation that I can find, but it is mentioned in some github-specific snippets online. I presumably have it there for that reason, by cargo-culting.   

Anyway, our netrc parsing chokes on this with:

```
  File "pex/auth.py", line 67, in iter_entries
    for machine, (login, _, password) in netrc(netrc_path).hosts.items():
  File "/Users/benjyw/.pyenv/versions/pypy3.7-7.3.9/lib-python/3/netrc.py", line 30, in __init__
    self._parse(file, fp, default_netrc)
  File "/Users/benjyw/.pyenv/versions/pypy3.7-7.3.9/lib-python/3/netrc.py", line 111, in _parse
    file, lexer.lineno)
netrc.NetrcParseError: bad follower token 'protocol' (/Users/benjyw/.netrc, line 4)
```

Removing that `protocol https` line fixes this, and I have yet to see that this has caused any issues accessing github. 

I'm not sure how widespread this sort of issue might be, and therefore whether we want to somehow deal with it or just recommend that people fix up their netrc.

So I suggest that for now we don't do anything, and wait and see how many early adopters hit this. I'm filing this issue just so we have it tracked prior to stabilizing Pants 2.13, which uses this functionality.


Discussion:
Since its likely you won't even need the netrc credentials anyhow (your case), Pex should at most warn and move on. If you do need the credentials, things will fail when they should and you'll have the warning to look back at.

Resolving patch:
diff --git a/pex/auth.py b/pex/auth.py
--- a/pex/auth.py
+++ b/pex/auth.py
@@ -4,8 +4,9 @@
 from __future__ import absolute_import
 
 import os
-from netrc import netrc
+from netrc import NetrcParseError, netrc
 
+from pex import pex_warnings
 from pex.compatibility import urlparse
 from pex.typing import TYPE_CHECKING
 
@@ -58,13 +59,22 @@ def from_netrc(cls, netrc_file="~/.netrc"):
         if not netrc_file:
             return cls()
 
-        netrc_path = os.path.expanduser("~/.netrc")
+        netrc_path = os.path.expanduser(netrc_file)
         if not os.path.isfile(netrc_path):
             return cls()
 
+        try:
+            netrc_database = netrc(netrc_path)
+        except NetrcParseError as e:
+            pex_warnings.warn(
+                "Failed to load netrc credentials: {err}\n"
+                "Continuing without netrc credentials.".format(err=e)
+            )
+            return cls()
+
         def iter_entries():
             # type: () -> Iterator[PasswordEntry]
-            for machine, (login, _, password) in netrc(netrc_path).hosts.items():
+            for machine, (login, _, password) in netrc_database.hosts.items():
                 if password is None:
                     continue
 


Test patch:
diff --git a/tests/integration/cli/commands/test_issue_1753.py b/tests/integration/cli/commands/test_lock_resolve_auth.py
similarity index 82%
rename from tests/integration/cli/commands/test_issue_1753.py
rename to tests/integration/cli/commands/test_lock_resolve_auth.py
--- a/tests/integration/cli/commands/test_issue_1753.py
+++ b/tests/integration/cli/commands/test_lock_resolve_auth.py
@@ -161,16 +161,17 @@ def assert_unauthorized(
     secured_ansicolors_lock,  # type: SecuredLock
     result,  # type: IntegResults
 ):
-    # type: (...) -> None
+    # type: (...) -> IntegResults
     result.assert_failure()
     assert (
         "There was 1 error downloading required artifacts:\n"
         "1. ansicolors 1.1.8 from {repo_url}/ansicolors-1.1.8-py2.py3-none-any.whl\n"
         "    HTTP Error 401: Unauthorized".format(repo_url=secured_ansicolors_lock.repo_url)
     ) in result.error
+    return result
 
 
-def test_authenticated_lock_url(
+def test_authenticated_lock_url_issue_1753(
     tmpdir,  # type: Any
     secured_ansicolors_lock,  # type: SecuredLock
 ):
@@ -226,7 +227,7 @@ def assert_success(result):
     )
 
 
-def test_authenticated_lock_netrc(
+def test_authenticated_lock_netrc_issue_1753(
     tmpdir,  # type: Any
     secured_ansicolors_lock,  # type: SecuredLock
 ):
@@ -294,3 +295,59 @@ def assert_authorized(result):
         )
     safe_rmtree(secured_ansicolors_lock.pex_root)
     assert_authorized(run_pex_command(args=use_lock_command, env=make_env(HOME=home)))
+
+
+def test_bad_netrc_issue_1762(
+    tmpdir,  # type: Any
+    secured_ansicolors_lock,  # type: SecuredLock
+):
+    # type: (...) -> None
+
+    use_lock_command = [
+        "--pex-root",
+        secured_ansicolors_lock.pex_root,
+        "--lock",
+        secured_ansicolors_lock.lock,
+        "--",
+        "-c",
+        "import colors; print(colors.yellow('Welcome'))",
+    ]
+    safe_rmtree(secured_ansicolors_lock.pex_root)
+
+    home = os.path.join(str(tmpdir), "home")
+    os.mkdir(home)
+    netrc_path = os.path.join(home, ".netrc")
+    with open(netrc_path, "w") as fp:
+        print("default login foo password bar", file=fp)
+        print("machine foo login bar password baz", file=fp)
+        print(
+            "machine {host}:{port} login {username} password {password} protocol http".format(
+                host=secured_ansicolors_lock.repo_address.host,
+                port=secured_ansicolors_lock.repo_address.port,
+                username=secured_ansicolors_lock.repo_username,
+                password=secured_ansicolors_lock.repo_password,
+            ),
+            file=fp,
+        )
+
+    def assert_netrc_skipped(result):
+        # type: (IntegResults) -> None
+        assert (
+            "Failed to load netrc credentials: bad follower token 'protocol' ({netrc}, line 3)\n"
+            "Continuing without netrc credentials.".format(netrc=netrc_path)
+        ) in result.error
+
+    result = assert_unauthorized(
+        secured_ansicolors_lock,
+        run_pex_command(args=use_lock_command, env=make_env(HOME=home), quiet=True),
+    )
+    assert_netrc_skipped(result)
+
+    result = run_pex_command(
+        args=["--find-links", secured_ansicolors_lock.repo_url_with_credentials] + use_lock_command,
+        env=make_env(HOME=home),
+        quiet=True,
+    )
+    result.assert_success()
+    assert_netrc_skipped(result)
+    assert colors.yellow("Welcome") == result.output.strip()