diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3440b586e..cdd655101 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,15 @@ Changelog for python-chess ========================== +New in v0.23.11 +--------------- + +Bugfixes: + +* Fix `chess.Board.set_epd()` and `chess.Board.from_epd()` with semicolon + in string operand. Thanks @jdart1. +* `chess.pgn.GameNode.uci()` was always raising an exception. + New in v0.24.0 -------------- diff --git a/chess/__init__.py b/chess/__init__.py index d2694a77b..bc611d79f 100644 --- a/chess/__init__.py +++ b/chess/__init__.py @@ -2316,7 +2316,7 @@ def _epd_operations(self, operations): # Append as escaped string. epd.append(" \"") - epd.append(str(operand).replace("\r", "").replace("\n", " ").replace("\\", "\\\\").replace(";", "\\s")) + epd.append(str(operand).replace("\r", "").replace("\n", " ").replace("\\", "\\\\").replace("\"", "\\\"")) epd.append("\";") return "".join(epd) @@ -2361,90 +2361,99 @@ def epd(self, *, shredder=False, en_passant="legal", promoted=None, **operations def _parse_epd_ops(self, operation_part, make_board): operations = {} - - if not operation_part: - return operations - - operation_part += ";" - + state = "opcode" opcode = "" operand = "" - in_operand = False - in_quotes = False - escape = False - position = None - for c in operation_part: - if not in_operand: - if c == ";": - operations[opcode] = None - opcode = "" - elif c == " ": + for ch in itertools.chain(operation_part, [None]): + if state == "opcode": + if ch == " ": + if opcode: + state = "after_opcode" + elif ch in [";", None]: if opcode: - in_operand = True + operations[opcode] = None + opcode = "" else: - opcode += c - else: - if c == "\"": - if not operand and not in_quotes: - in_quotes = True - elif escape: - operand += c - elif c == "\\": - if escape: - operand += c - else: - escape = True - elif c == "s": - if escape: - operand += ";" - else: - operand += c - elif c == ";": - if escape: - operand += "\\" - - if in_quotes: - # A string operand. - operations[opcode] = operand + opcode += ch + elif state == "after_opcode": + if ch == " ": + pass + elif ch in "+-.0123456789": + operand = ch + state = "numeric" + elif ch == "\"": + state = "string" + elif ch in [";", None]: + if opcode: + operations[opcode] = None + opcode = "" + state = "opcode" + else: + operand = ch + state = "san" + elif state == "numeric": + if ch in [";", None]: + operations[opcode] = float(operand) + try: + operations[opcode] = int(operand) + except: + pass + opcode = "" + operand = "" + state = "opcode" + else: + operand += ch + elif state == "string": + if ch in ["\"", None]: + operations[opcode] = operand + opcode = "" + operand = "" + state = "opcode" + elif ch == "\\": + state = "string_escape" + else: + operand += ch + elif state == "string_escape": + if ch is None: + operations[opcode] = operand + opcode = "" + operand = "" + state = "opcode" + else: + operand += ch + state = "string" + elif state == "san": + if ch in [";", None]: + if position is None: + position = make_board() + + if opcode == "pv": + # A variation. + operations[opcode] = [] + for token in operand.split(): + move = position.parse_san(token) + operations[opcode].append(move) + position.push(move) + + # Reset the position. + while position.move_stack: + position.pop() + elif opcode in ["bm", "am"]: + # A set of moves. + operations[opcode] = [position.parse_san(token) for token in operand.split()] else: - try: - # An integer. - operations[opcode] = int(operand) - except ValueError: - try: - # A float. - operations[opcode] = float(operand) - except ValueError: - if position is None: - position = make_board() - if opcode == "pv": - # A variation. - operations[opcode] = [] - for token in operand.split(): - move = position.parse_san(token) - operations[opcode].append(move) - position.push(move) - - # Reset the position. - while position.move_stack: - position.pop() - elif opcode in ("bm", "am"): - # A set of moves. - operations[opcode] = [position.parse_san(token) for token in operand.split()] - else: - # A single move. - operations[opcode] = position.parse_san(operand) + # A single move. + operations[opcode] = position.parse_san(operand) opcode = "" operand = "" - in_operand = False - in_quotes = False - escape = False + state = "opcode" else: - operand += c + operand += ch + assert state == "opcode" return operations def set_epd(self, epd): diff --git a/test.py b/test.py index 099bd912b..b7e3b58d6 100755 --- a/test.py +++ b/test.py @@ -932,6 +932,18 @@ def test_epd(self): self.assertEqual(operations["pv"][1], chess.Move.from_uci("g8f8")) self.assertEqual(operations["pv"][2], chess.Move.from_uci("h7f7")) + # Test EPD with semicolon. + board = chess.Board() + operations = board.set_epd("r2qk2r/ppp1b1pp/2n1p3/3pP1n1/3P2b1/2PB1NN1/PP4PP/R1BQK2R w KQkq - bm Nxg5; c0 \"ERET.095; Queen sacrifice\";") + self.assertEqual(operations["bm"], [chess.Move.from_uci("f3g5")]) + self.assertEqual(operations["c0"], "ERET.095; Queen sacrifice") + + # Test an EPD with string escaping. + board = chess.Board() + operations = board.set_epd(r"""4k3/8/8/8/8/8/8/4K3 w - - a "foo\"bar";; ; b "foo\\\\";""") + self.assertEqual(operations["a"], "foo\"bar") + self.assertEqual(operations["b"], "foo\\\\") + def test_null_moves(self): self.assertEqual(str(chess.Move.null()), "0000") self.assertEqual(chess.Move.null().uci(), "0000")