diff --git a/Lib/netrc.py b/Lib/netrc.py index a28ea297df894b..cb1e3782a01c14 100644 --- a/Lib/netrc.py +++ b/Lib/netrc.py @@ -48,11 +48,12 @@ def _read_char(self): def get_token(self): if self.pushback: return self.pushback.pop(0) - token = "" + token = None fiter = iter(self._read_char, "") for ch in fiter: if ch in self.whitespace: continue + token = "" if ch == '"': for ch in fiter: if ch == '"': @@ -96,9 +97,9 @@ def _parse(self, file, fp, default_netrc): # Look for a machine, default, or macdef top-level keyword saved_lineno = lexer.lineno tt = lexer.get_token() - if not tt: + if tt is None: break - elif tt[0] == '#': + elif tt.startswith('#'): if lexer.lineno == saved_lineno and len(tt) == 1: lexer.instream.readline() continue @@ -135,20 +136,20 @@ def _parse(self, file, fp, default_netrc): while 1: prev_lineno = lexer.lineno tt = lexer.get_token() - if tt.startswith('#'): + if tt is not None and tt.startswith('#'): if lexer.lineno == prev_lineno: lexer.instream.readline() continue - if tt in {'', 'machine', 'default', 'macdef'}: + if tt in {None, 'machine', 'default', 'macdef'}: self.hosts[entryname] = (login, account, password) lexer.push_token(tt) break elif tt == 'login' or tt == 'user': - login = lexer.get_token() + login = lexer.get_token() or '' elif tt == 'account': - account = lexer.get_token() + account = lexer.get_token() or '' elif tt == 'password': - password = lexer.get_token() + password = lexer.get_token() or '' else: raise NetrcParseError("bad follower token %r" % tt, file, lexer.lineno) diff --git a/Lib/test/test_netrc.py b/Lib/test/test_netrc.py index 354081e96213a6..bbb49bf15ce8f1 100644 --- a/Lib/test/test_netrc.py +++ b/Lib/test/test_netrc.py @@ -62,6 +62,9 @@ def test_optional_tokens(self): "machine host.domain.com login", "machine host.domain.com account", "machine host.domain.com password", + "machine host.domain.com login \"\"", + "machine host.domain.com account \"\"", + "machine host.domain.com password \"\"", "machine host.domain.com login \"\" account", "machine host.domain.com login \"\" password", "machine host.domain.com account \"\" password" @@ -74,6 +77,9 @@ def test_optional_tokens(self): "default login", "default account", "default password", + "default login \"\"", + "default account \"\"", + "default password \"\"", "default login \"\" account", "default login \"\" password", "default account \"\" password" @@ -82,6 +88,15 @@ def test_optional_tokens(self): nrc = self.make_nrc(item) self.assertEqual(nrc.hosts['default'], ('', '', '')) + def test_empty_quoted_token_is_not_eof(self): + data = ( + '"" invalid', + 'machine host.domain.com "" invalid', + ) + for item in data: + with self.subTest(item=item): + self.assertRaises(netrc.NetrcParseError, self.make_nrc, item) + def test_invalid_tokens(self): data = ( "invalid host.domain.com", diff --git a/Misc/NEWS.d/next/Library/2026-08-10-15-24-13.gh-issue-155468.W7qL2p.rst b/Misc/NEWS.d/next/Library/2026-08-10-15-24-13.gh-issue-155468.W7qL2p.rst new file mode 100644 index 00000000000000..4d495b9de62fa5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-10-15-24-13.gh-issue-155468.W7qL2p.rst @@ -0,0 +1,2 @@ +Fix :mod:`netrc` to distinguish empty quoted tokens from end-of-file, so +malformed files no longer cause the remaining content to be silently ignored.