diff --git a/chess/pgn.py b/chess/pgn.py index 36cac2c5..50984a64 100644 --- a/chess/pgn.py +++ b/chess/pgn.py @@ -290,8 +290,8 @@ class GameNode(abc.ABC): """ @property - def comment(self) -> GameNodeComment: - return self._comment + def comment(self) -> str: + return " ".join(self._comment) @comment.setter def comment(self, new_comment: Union[str, list[str], GameNodeComment]) -> None: @@ -300,11 +300,19 @@ def comment(self, new_comment: Union[str, list[str], GameNodeComment]) -> None: else: self._comment = GameNodeComment(new_comment) + @property + def comments(self) -> GameNodeComment: + return self._comment + + @comments.setter + def comments(self, new_comment: Union[str, list[str], GameNodeComment]) -> None: + self.comment = new_comment + _starting_comment: GameNodeComment @property - def starting_comment(self) -> GameNodeComment: - return self._starting_comment + def starting_comment(self) -> str: + return " ".join(self._starting_comment) @starting_comment.setter def starting_comment(self, new_comment: Union[str, list[str], GameNodeComment]) -> None: @@ -313,6 +321,14 @@ def starting_comment(self, new_comment: Union[str, list[str], GameNodeComment]) else: self._starting_comment = GameNodeComment(new_comment) + @property + def starting_comments(self) -> GameNodeComment: + return self._starting_comment + + @starting_comments.setter + def starting_comments(self, new_comment: Union[str, list[str], GameNodeComment]) -> None: + self._starting_comment = new_comment + nags: Set[int] def __init__(self, *, comment: Union[str, list[str]] = "") -> None: @@ -539,7 +555,7 @@ def add_line(self, moves: Iterable[chess.Move], *, comment: Union[str, list[str] starting_comment = "" # Merge comment and NAGs. - node.comment.append(comment) + node.comments.append(comment) node.nags.update(nags) return node @@ -551,7 +567,7 @@ def eval(self) -> Optional[chess.engine.PovScore]: Complexity is `O(n)`. """ - match = EVAL_REGEX.search(" ".join(self.comment)) + match = EVAL_REGEX.search(self.comment) if not match: return None @@ -577,7 +593,7 @@ def eval_depth(self) -> Optional[int]: Complexity is `O(1)`. """ - match = EVAL_REGEX.search(" ".join(self.comment)) + match = EVAL_REGEX.search(self.comment) return int(match.group("depth")) if match and match.group("depth") else None def set_eval(self, score: Optional[chess.engine.PovScore], depth: Optional[int] = None) -> None: @@ -595,15 +611,15 @@ def set_eval(self, score: Optional[chess.engine.PovScore], depth: Optional[int] eval = f"[%eval #{score.white().mate()}{depth_suffix}]" found = 0 - for index in range(len(self.comment)): - self.comment[index], found = EVAL_REGEX.subn(_condense_affix(eval), self.comment[index], count=1) + for index in range(len(self.comments)): + self.comments[index], found = EVAL_REGEX.subn(_condense_affix(eval), self.comments[index], count=1) if found: break - self.comment.remove_empty() + self.comments.remove_empty() if not found and eval: - self.comment.append(eval) + self.comments.append(eval) def arrows(self) -> List[chess.svg.Arrow]: """ @@ -613,7 +629,7 @@ def arrows(self) -> List[chess.svg.Arrow]: Returns a list of :class:`arrows `. """ arrows = [] - for match in ARROWS_REGEX.finditer(" ".join(self.comment)): + for match in ARROWS_REGEX.finditer(self.comment): for group in match.group("arrows").split(","): arrows.append(chess.svg.Arrow.from_pgn(group)) @@ -635,10 +651,10 @@ def set_arrows(self, arrows: Iterable[Union[chess.svg.Arrow, Tuple[Square, Squar pass (csl if arrow.tail == arrow.head else cal).append(arrow.pgn()) # type: ignore - for index in range(len(self.comment)): - self.comment[index] = ARROWS_REGEX.sub(_condense_affix(""), self.comment[index]) + for index in range(len(self.comments)): + self.comments[index] = ARROWS_REGEX.sub(_condense_affix(""), self.comments[index]) - self.comment.remove_empty() + self.comments.remove_empty() prefix = "" if csl: @@ -647,7 +663,7 @@ def set_arrows(self, arrows: Iterable[Union[chess.svg.Arrow, Tuple[Square, Squar prefix += f"[%cal {','.join(cal)}]" if prefix: - self.comment.insert(0, prefix) + self.comments.insert(0, prefix) def clock(self) -> Optional[float]: """ @@ -657,7 +673,7 @@ def clock(self) -> Optional[float]: Returns the player's remaining time to the next time control after this move, in seconds. """ - match = CLOCK_REGEX.search(" ".join(self.comment)) + match = CLOCK_REGEX.search(self.comment) if match is None: return None return int(match.group("hours")) * 3600 + int(match.group("minutes")) * 60 + float(match.group("seconds")) @@ -677,15 +693,15 @@ def set_clock(self, seconds: Optional[float]) -> None: clk = f"[%clk {hours:d}:{minutes:02d}:{seconds_part}]" found = 0 - for index in range(len(self.comment)): - self.comment[index], found = CLOCK_REGEX.subn(_condense_affix(clk), self.comment[index], count=1) + for index in range(len(self.comments)): + self.comments[index], found = CLOCK_REGEX.subn(_condense_affix(clk), self.comments[index], count=1) if found: break - self.comment.remove_empty() + self.comments.remove_empty() if not found and clk: - self.comment.append(clk) + self.comments.append(clk) def emt(self) -> Optional[float]: """ @@ -695,7 +711,7 @@ def emt(self) -> Optional[float]: Returns the player's elapsed move time use for the comment of this move, in seconds. """ - match = EMT_REGEX.search(" ".join(self.comment)) + match = EMT_REGEX.search(" ".join(self.comments)) if match is None: return None return int(match.group("hours")) * 3600 + int(match.group("minutes")) * 60 + float(match.group("seconds")) @@ -715,15 +731,15 @@ def set_emt(self, seconds: Optional[float]) -> None: emt = f"[%emt {hours:d}:{minutes:02d}:{seconds_part}]" found = 0 - for index in range(len(self.comment)): - self.comment[index], found = EMT_REGEX.subn(_condense_affix(emt), self.comment[index], count=1) + for index in range(len(self.comments)): + self.comments[index], found = EMT_REGEX.subn(_condense_affix(emt), self.comments[index], count=1) if found: break - self.comment.remove_empty() + self.comments.remove_empty() if not found and emt: - self.comment.append(emt) + self.comments.append(emt) @abc.abstractmethod def accept(self, visitor: BaseVisitor[ResultT]) -> ResultT: @@ -868,7 +884,7 @@ def end(self) -> ChildNode: def _accept_node(self, parent_board: chess.Board, visitor: BaseVisitor[ResultT]) -> None: if self.starting_comment: - visitor.visit_comment(self.starting_comment) + visitor.visit_comment(self.starting_comments) visitor.visit_move(parent_board, self.move) @@ -879,8 +895,8 @@ def _accept_node(self, parent_board: chess.Board, visitor: BaseVisitor[ResultT]) for nag in sorted(self.nags): visitor.visit_nag(nag) - if self.comment: - visitor.visit_comment(self.comment) + if self.comments: + visitor.visit_comment(self.comments) def _accept(self, parent_board: chess.Board, visitor: BaseVisitor[ResultT], *, sidelines: bool = True) -> None: stack = [_AcceptFrame(self, sidelines=sidelines)] @@ -1011,8 +1027,8 @@ def accept(self, visitor: BaseVisitor[ResultT]) -> ResultT: board = self.board() visitor.visit_board(board) - if self.comment: - visitor.visit_comment(self.comment) + if self.comments: + visitor.visit_comment(self.comments) if self.variations: self.variations[0]._accept(board, visitor) @@ -1347,14 +1363,14 @@ def visit_comment(self, comment: GameNodeComment) -> None: # Add as a comment for the current node if in the middle of # a variation. Add as a comment for the game if the comment # starts before any move. - new_comment = self.variation_stack[-1].comment + comment - new_comment.remove_empty() - self.variation_stack[-1].comment = new_comment + new_comments = self.variation_stack[-1].comments + comment + new_comments.remove_empty() + self.variation_stack[-1].comments = new_comments else: # Otherwise, it is a starting comment. - new_comment = self.starting_comment + comment - new_comment.remove_empty() - self.starting_comment = new_comment + new_comments = self.starting_comment + comment + new_comments.remove_empty() + self.starting_comment = new_comments def visit_move(self, board: chess.Board, move: chess.Move) -> None: self.variation_stack[-1] = self.variation_stack[-1].add_variation(move) diff --git a/test.py b/test.py index ee770de0..aa89cd2c 100755 --- a/test.py +++ b/test.py @@ -2220,19 +2220,24 @@ def test_read_game_with_multicomment_move(self): game = chess.pgn.read_game(pgn) first_move = game.variation(0) self.assertEqual(first_move.comment, "A common opening") + self.assertEqual(first_move.comments, "A common opening") + self.assertEqual(first_move.comments, ["A common opening"]) second_move = first_move.variation(0) - self.assertEqual(second_move.comment, ["A common response", "An uncommon comment"]) - second_move.comment.pop() - self.assertEqual(second_move.comment, ["A common response"]) + self.assertEqual(second_move.comments, ["A common response", "An uncommon comment"]) + second_move.comments.pop() + self.assertEqual(second_move.comments, ["A common response"]) + self.assertEqual(second_move.comments, "A common response") self.assertEqual(second_move.comment, "A common response") - second_move.comment.append("A replaced comment") + second_move.comments.append("A replaced comment") multiple_comments = ["A common response", "A replaced comment"] - self.assertEqual(second_move.comment, multiple_comments) - for move_comment, test_comment in zip(second_move.comment, multiple_comments): + self.assertEqual(second_move.comments, multiple_comments) + for move_comment, test_comment in zip(second_move.comments, multiple_comments): self.assertEqual(move_comment, test_comment) - self.assertEqual(list(second_move.comment), multiple_comments) - second_move.comment.pop(0) - self.assertEqual(second_move.comment, ["A replaced comment"]) + self.assertEqual(list(second_move.comments), multiple_comments) + self.assertEqual(second_move.comment, " ".join(multiple_comments)) + second_move.comments.pop(0) + self.assertEqual(second_move.comments, ["A replaced comment"]) + self.assertEqual(second_move.comments, "A replaced comment") self.assertEqual(second_move.comment, "A replaced comment") def test_comment_at_eol(self): @@ -2845,22 +2850,24 @@ def test_annotations(self): self.assertTrue(game.clock() is None) clock = 12345 game.set_clock(clock) - self.assertEqual(game.comment, ["foo [%bar] baz", "[%clk 3:25:45]"]) + self.assertEqual(game.comment, "foo [%bar] baz [%clk 3:25:45]") + self.assertEqual(game.comments, ["foo [%bar] baz", "[%clk 3:25:45]"]) self.assertEqual(game.clock(), clock) self.assertTrue(game.eval() is None) game.set_eval(chess.engine.PovScore(chess.engine.Cp(-80), chess.WHITE)) - self.assertEqual(game.comment, ["foo [%bar] baz", "[%clk 3:25:45]", "[%eval -0.80]"]) + self.assertEqual(game.comments, ["foo [%bar] baz", "[%clk 3:25:45]", "[%eval -0.80]"]) self.assertEqual(game.eval().white().score(), -80) self.assertEqual(game.eval_depth(), None) game.set_eval(chess.engine.PovScore(chess.engine.Mate(1), chess.WHITE), 5) - self.assertEqual(game.comment, ["foo [%bar] baz", "[%clk 3:25:45]", "[%eval #1,5]"]) + self.assertEqual(game.comments, ["foo [%bar] baz", "[%clk 3:25:45]", "[%eval #1,5]"]) self.assertEqual(game.eval().white().mate(), 1) self.assertEqual(game.eval_depth(), 5) self.assertEqual(game.arrows(), []) game.set_arrows([(chess.A1, chess.A1), chess.svg.Arrow(chess.A1, chess.H1, color="red"), chess.svg.Arrow(chess.B1, chess.B8)]) - self.assertEqual(game.comment, ["[%csl Ga1][%cal Ra1h1,Gb1b8]", "foo [%bar] baz", "[%clk 3:25:45]", "[%eval #1,5]"]) + self.assertEqual(game.comments, ["[%csl Ga1][%cal Ra1h1,Gb1b8]", "foo [%bar] baz", "[%clk 3:25:45]", "[%eval #1,5]"]) + self.assertEqual(game.comment, "[%csl Ga1][%cal Ra1h1,Gb1b8] foo [%bar] baz [%clk 3:25:45] [%eval #1,5]") arrows = game.arrows() self.assertEqual(len(arrows), 3) self.assertEqual(arrows[0].color, "green") @@ -2870,17 +2877,21 @@ def test_annotations(self): self.assertTrue(game.emt() is None) emt = 321 game.set_emt(emt) - self.assertEqual(game.comment, ["[%csl Ga1][%cal Ra1h1,Gb1b8]", "foo [%bar] baz", "[%clk 3:25:45]", "[%eval #1,5]", "[%emt 0:05:21]"]) + self.assertEqual(game.comments, ["[%csl Ga1][%cal Ra1h1,Gb1b8]", "foo [%bar] baz", "[%clk 3:25:45]", "[%eval #1,5]", "[%emt 0:05:21]"]) + self.assertEqual(game.comment, "[%csl Ga1][%cal Ra1h1,Gb1b8] foo [%bar] baz [%clk 3:25:45] [%eval #1,5] [%emt 0:05:21]") self.assertEqual(game.emt(), emt) game.set_eval(None) - self.assertEqual(game.comment, ["[%csl Ga1][%cal Ra1h1,Gb1b8]", "foo [%bar] baz", "[%clk 3:25:45]", "[%emt 0:05:21]"]) + self.assertEqual(game.comments, ["[%csl Ga1][%cal Ra1h1,Gb1b8]", "foo [%bar] baz", "[%clk 3:25:45]", "[%emt 0:05:21]"]) + self.assertEqual(game.comment, "[%csl Ga1][%cal Ra1h1,Gb1b8] foo [%bar] baz [%clk 3:25:45] [%emt 0:05:21]") game.set_emt(None) - self.assertEqual(game.comment, ["[%csl Ga1][%cal Ra1h1,Gb1b8]", "foo [%bar] baz", "[%clk 3:25:45]"]) + self.assertEqual(game.comments, ["[%csl Ga1][%cal Ra1h1,Gb1b8]", "foo [%bar] baz", "[%clk 3:25:45]"]) + self.assertEqual(game.comment, "[%csl Ga1][%cal Ra1h1,Gb1b8] foo [%bar] baz [%clk 3:25:45]") game.set_clock(None) game.set_arrows([]) + self.assertEqual(game.comments, "foo [%bar] baz") self.assertEqual(game.comment, "foo [%bar] baz") def test_eval(self):