diff --git a/Doc/library/turtle.rst b/Doc/library/turtle.rst index 49099aea4152c1..5dc083c12ccce2 100644 --- a/Doc/library/turtle.rst +++ b/Doc/library/turtle.rst @@ -8,11 +8,13 @@ **Source code:** :source:`Lib/turtle.py` .. testsetup:: default + :skipif: _tkinter is None from turtle import * turtle = Turtle() .. testcleanup:: + :skipif: _tkinter is None import os os.remove("my_drawing.ps") @@ -510,6 +512,7 @@ Turtle motion turtle is headed. Do not change the turtle's heading. .. doctest:: + :skipif: _tkinter is None :hide: >>> turtle.goto(0, 0) @@ -852,6 +855,7 @@ Turtle motion last *n* stamps. .. doctest:: + :skipif: _tkinter is None >>> for i in range(8): ... unused_stamp_id = turtle.stamp() @@ -1409,8 +1413,11 @@ More drawing control font. If *move* is true, the pen is moved to the bottom-right corner of the text. By default, *move* is ``False``. - >>> turtle.write("Home = ", True, align="center") - >>> turtle.write((0,0), True) + .. doctest:: + :skipif: _tkinter is None + + >>> turtle.write("Home = ", True, align="center") + >>> turtle.write((0,0), True) Turtle state @@ -1447,12 +1454,15 @@ Visibility Return ``True`` if the Turtle is shown, ``False`` if it's hidden. - >>> turtle.hideturtle() - >>> turtle.isvisible() - False - >>> turtle.showturtle() - >>> turtle.isvisible() - True + .. doctest:: + :skipif: _tkinter is None + + >>> turtle.hideturtle() + >>> turtle.isvisible() + False + >>> turtle.showturtle() + >>> turtle.isvisible() + True Appearance @@ -1983,6 +1993,9 @@ Window control method, one can make visible those parts of a drawing which were outside the canvas before. + .. doctest:: + :skipif: _tkinter is None + >>> screen.screensize() (400, 300) >>> screen.screensize(2000,1500) diff --git a/Lib/netrc.py b/Lib/netrc.py index e9b5538d2c4399..67a287237cbf8b 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.