Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion chess/pgn.py
Original file line number Diff line number Diff line change
Expand Up @@ -1229,7 +1229,12 @@ def begin_variation(self) -> None:

@override
def end_variation(self) -> None:
self.variation_stack.pop()
# Keep the root game node on the stack. A malformed PGN (e.g. an error
# recovery that skips to an unmatched ")") can call end_variation more
# often than begin_variation; popping the root here would empty the
# stack and make the next visit_move raise IndexError.
if len(self.variation_stack) > 1:
self.variation_stack.pop()

@override
def visit_result(self, result: str) -> None:
Expand Down
11 changes: 11 additions & 0 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2358,6 +2358,17 @@ def test_variation_stack(self):
self.assertEqual(game[0].san(), "c4")
self.assertEqual(len(game.errors), 0)

# Survive a closing bracket reached through error recovery. The illegal
# move sends the parser into skip mode, and the unmatched ")" used to
# pop the root off the variation stack, so the following move raised an
# IndexError instead of being parsed.
pgn = io.StringIO("1. e4 Nf3 ) e5 *")
logging.disable(logging.ERROR)
game = chess.pgn.read_game(pgn)
logging.disable(logging.NOTSET)
self.assertEqual([node.san() for node in game.mainline()], ["e4", "e5"])
self.assertEqual(len(game.errors), 1)

def test_game_starting_comment(self):
pgn = io.StringIO("{ Game starting comment } 1. d3")
game = chess.pgn.read_game(pgn)
Expand Down