|
80 | 80 |
|
81 | 81 | TAG_REGEX = re.compile(r"^\[([A-Za-z0-9_]+)\s+\"(.*)\"\]\s*$") |
82 | 82 |
|
| 83 | +TAG_NAME_REGEX = re.compile(r"^[A-Za-z9-9_]+\Z") |
| 84 | + |
83 | 85 | MOVETEXT_REGEX = re.compile(r""" |
84 | 86 | ( |
85 | 87 | [NBKRQ]?[a-h]?[1-8]?[\-x]?[a-h][1-8](?:=?[nbrqkNBRQK])? |
|
99 | 101 | """, re.DOTALL | re.VERBOSE) |
100 | 102 |
|
101 | 103 |
|
| 104 | +TAG_ROSTER = ["Event", "Site", "Date", "Round", "White", "Black", "Result"] |
| 105 | + |
| 106 | + |
102 | 107 | class GameNode(object): |
103 | 108 |
|
104 | 109 | def __init__(self): |
@@ -389,18 +394,9 @@ class Game(GameNode): |
389 | 394 | :class:`~chess.pgn.GameNode`. |
390 | 395 | """ |
391 | 396 |
|
392 | | - def __init__(self): |
| 397 | + def __init__(self, headers=None): |
393 | 398 | super(Game, self).__init__() |
394 | | - |
395 | | - self.headers = collections.OrderedDict() |
396 | | - self.headers["Event"] = "?" |
397 | | - self.headers["Site"] = "?" |
398 | | - self.headers["Date"] = "????.??.??" |
399 | | - self.headers["Round"] = "?" |
400 | | - self.headers["White"] = "?" |
401 | | - self.headers["Black"] = "?" |
402 | | - self.headers["Result"] = "*" |
403 | | - |
| 399 | + self.headers = Headers(headers) |
404 | 400 | self.errors = [] |
405 | 401 |
|
406 | 402 | def board(self, _cache=False): |
@@ -504,11 +500,70 @@ def from_board(cls, board): |
504 | 500 | @classmethod |
505 | 501 | def without_tag_roster(cls): |
506 | 502 | """Creates an empty game without the default 7 tag roster.""" |
507 | | - game = cls.__new__(cls) |
508 | | - super(Game, game).__init__() |
509 | | - game.headers = collections.OrderedDict() |
510 | | - game.errors = [] |
511 | | - return game |
| 503 | + return cls(headers=Headers({})) |
| 504 | + |
| 505 | + |
| 506 | +class Headers(collections.MutableMapping): |
| 507 | + def __init__(self, data=None, **kwargs): |
| 508 | + self._tag_roster = {} |
| 509 | + self._others = {} |
| 510 | + |
| 511 | + if data is None: |
| 512 | + data = { |
| 513 | + "Event": "?", |
| 514 | + "Site": "?", |
| 515 | + "Date": "????.??.??", |
| 516 | + "Round": "?", |
| 517 | + "White": "?", |
| 518 | + "Black": "?", |
| 519 | + "Result": "*" |
| 520 | + } |
| 521 | + |
| 522 | + self.update(data, **kwargs) |
| 523 | + |
| 524 | + def __setitem__(self, key, value): |
| 525 | + if key in TAG_ROSTER: |
| 526 | + self._tag_roster[key] = value |
| 527 | + elif not TAG_NAME_REGEX.match(key): |
| 528 | + raise ValueError("non-alphanumeric pgn header tag: {0}".format(repr(key))) |
| 529 | + elif "\n" in value or "\r" in value: |
| 530 | + raise ValueError("line break in pgn header {0}: {1}".format(key, repr(value))) |
| 531 | + else: |
| 532 | + self._others[key] = value |
| 533 | + |
| 534 | + def __getitem__(self, key): |
| 535 | + if key in TAG_ROSTER: |
| 536 | + return self._tag_roster[key] |
| 537 | + else: |
| 538 | + return self._others[key] |
| 539 | + |
| 540 | + def __delitem__(self, key): |
| 541 | + if key in TAG_ROSTER: |
| 542 | + del self._tag_roster[key] |
| 543 | + else: |
| 544 | + del self._others[key] |
| 545 | + |
| 546 | + def __iter__(self): |
| 547 | + for key in TAG_ROSTER: |
| 548 | + if key in self._tag_roster: |
| 549 | + yield key |
| 550 | + |
| 551 | + for key in sorted(self._others): |
| 552 | + yield key |
| 553 | + |
| 554 | + def __len__(self): |
| 555 | + return len(self._tag_roster) + len(self._others) |
| 556 | + |
| 557 | + def copy(self): |
| 558 | + return type(self)(self) |
| 559 | + |
| 560 | + def __copy__(self): |
| 561 | + return self.copy() |
| 562 | + |
| 563 | + def __repr__(self): |
| 564 | + return "{0}({1})".format( |
| 565 | + type(self).__name__, |
| 566 | + ", ".join("{0}={1}".format(key, repr(value)) for key, value in self.items())) |
512 | 567 |
|
513 | 568 |
|
514 | 569 | class BaseVisitor(object): |
@@ -1003,7 +1058,7 @@ def scan_headers(handle): |
1003 | 1058 | Scan a PGN file opened in text mode for game offsets and headers. |
1004 | 1059 |
|
1005 | 1060 | Yields a tuple for each game. The first element is the offset and the |
1006 | | - second element is an ordered dictionary of game headers. |
| 1061 | + second element is a mapping of game headers. |
1007 | 1062 |
|
1008 | 1063 | Since actually parsing many games from a big file is relatively expensive, |
1009 | 1064 | this is a better way to look only for specific games and then seek and |
@@ -1057,7 +1112,7 @@ def scan_headers(handle): |
1057 | 1112 | tag_match = TAG_REGEX.match(line) |
1058 | 1113 | if tag_match: |
1059 | 1114 | if game_pos is None: |
1060 | | - game_headers = Game().headers |
| 1115 | + game_headers = Headers() |
1061 | 1116 | game_pos = last_pos |
1062 | 1117 |
|
1063 | 1118 | game_headers[tag_match.group(1)] = tag_match.group(2) |
|
0 commit comments