Skip to content

Commit

Permalink
Merge branch '0.23.x'
Browse files Browse the repository at this point in the history
  • Loading branch information
niklasf committed Dec 7, 2018
2 parents 5396f1c + 4d8eb17 commit 61badb6
Show file tree
Hide file tree
Showing 3 changed files with 103 additions and 73 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -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
--------------

Expand Down
155 changes: 82 additions & 73 deletions chess/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
12 changes: 12 additions & 0 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit 61badb6

Please sign in to comment.