test_v1.1

This commit is contained in:
2023-05-23 17:03:23 +08:00
parent 663edbb5e2
commit 4968e6400e
17 changed files with 1 additions and 210 deletions
View File
+824
View File
@@ -0,0 +1,824 @@
"""Represent SGF games.
This is intended for use with SGF FF[4]; see http://www.red-bean.com/sgf/
Adapted from gomill by Matthew Woodcraft, https://github.com/mattheww/gomill
"""
from __future__ import absolute_import
import datetime
import six
from . import sgf_grammar
from . import sgf_properties
__all__ = [
'Node',
'Sgf_game',
'Tree_node',
]
class Node:
"""An SGF node.
Instantiate with a raw property map (see sgf_grammar) and an
sgf_properties.Presenter.
A Node doesn't belong to a particular game (cf Tree_node below), but it
knows its board size (in order to interpret move values) and the encoding
to use for the raw property strings.
Changing the SZ property isn't allowed.
"""
def __init__(self, property_map, presenter):
# Map identifier (PropIdent) -> nonempty list of raw values
self._property_map = property_map
self._presenter = presenter
def get_size(self):
"""Return the board size used to interpret property values."""
return self._presenter.size
def get_encoding(self):
"""Return the encoding used for raw property values.
Returns a string (a valid Python codec name, eg "UTF-8").
"""
return self._presenter.encoding
def get_presenter(self):
"""Return the node's sgf_properties.Presenter."""
return self._presenter
def has_property(self, identifier):
"""Check whether the node has the specified property."""
return identifier in self._property_map
def properties(self):
"""Find the properties defined for the node.
Returns a list of property identifiers, in unspecified order.
"""
return list(self._property_map.keys())
def get_raw_list(self, identifier):
"""Return the raw values of the specified property.
Returns a nonempty list of 8-bit strings, in the raw property encoding.
The strings contain the exact bytes that go between the square brackets
(without interpreting escapes or performing any whitespace conversion).
Raises KeyError if there was no property with the given identifier.
(If the property is an empty elist, this returns a list containing a
single empty string.)
"""
return self._property_map[identifier]
def get_raw(self, identifier):
"""Return a single raw value of the specified property.
Returns an 8-bit string, in the raw property encoding.
The string contains the exact bytes that go between the square brackets
(without interpreting escapes or performing any whitespace conversion).
Raises KeyError if there was no property with the given identifier.
If the property has multiple values, this returns the first (if the
value is an empty elist, this returns an empty string).
"""
return self._property_map[identifier][0]
def get_raw_property_map(self):
"""Return the raw values of all properties as a dict.
Returns a dict mapping property identifiers to lists of raw values
(see get_raw_list()).
Returns the same dict each time it's called.
Treat the returned dict as read-only.
"""
return self._property_map
def _set_raw_list(self, identifier, values):
if identifier == b"SZ" and \
values != [str(self._presenter.size).encode(self._presenter.encoding)]:
raise ValueError("changing size is not permitted")
self._property_map[identifier] = values
def unset(self, identifier):
"""Remove the specified property.
Raises KeyError if the property isn't currently present.
"""
if identifier == b"SZ" and self._presenter.size != 19:
raise ValueError("changing size is not permitted")
del self._property_map[identifier]
def set_raw_list(self, identifier, values):
"""Set the raw values of the specified property.
identifier -- ascii string passing is_valid_property_identifier()
values -- nonempty iterable of 8-bit strings in the raw property
encoding
The values specify the exact bytes to appear between the square
brackets in the SGF file; you must perform any necessary escaping
first.
(To specify an empty elist, pass a list containing a single empty
string.)
"""
if not sgf_grammar.is_valid_property_identifier(identifier):
raise ValueError("ill-formed property identifier")
values = list(values)
if not values:
raise ValueError("empty property list")
for value in values:
if not sgf_grammar.is_valid_property_value(value):
raise ValueError("ill-formed raw property value")
self._set_raw_list(identifier, values)
def set_raw(self, identifier, value):
"""Set the specified property to a single raw value.
identifier -- ascii string passing is_valid_property_identifier()
value -- 8-bit string in the raw property encoding
The value specifies the exact bytes to appear between the square
brackets in the SGF file; you must perform any necessary escaping
first.
"""
if not sgf_grammar.is_valid_property_identifier(identifier):
raise ValueError("ill-formed property identifier")
if not sgf_grammar.is_valid_property_value(value):
raise ValueError("ill-formed raw property value")
self._set_raw_list(identifier, [value])
def get(self, identifier):
"""Return the interpreted value of the specified property.
Returns the value as a suitable Python representation.
Raises KeyError if the node does not have a property with the given
identifier.
Raises ValueError if it cannot interpret the value.
See sgf_properties.Presenter.interpret() for details.
"""
return self._presenter.interpret(
identifier, self._property_map[identifier])
def set(self, identifier, value):
"""Set the value of the specified property.
identifier -- ascii string passing is_valid_property_identifier()
value -- new property value (in its Python representation)
For properties with value type 'none', use value True.
Raises ValueError if it cannot represent the value.
See sgf_properties.Presenter.serialise() for details.
"""
self._set_raw_list(
identifier, self._presenter.serialise(identifier, value))
def get_raw_move(self):
"""Return the raw value of the move from a node.
Returns a pair (colour, raw value)
colour is 'b' or 'w'.
Returns None, None if the node contains no B or W property.
"""
values = self._property_map.get(b"B")
if values is not None:
colour = "b"
else:
values = self._property_map.get(b"W")
if values is not None:
colour = "w"
else:
return None, None
return colour, values[0]
def get_move(self):
"""Retrieve the move from a node.
Returns a pair (colour, move)
colour is 'b' or 'w'.
move is (row, col), or None for a pass.
Returns None, None if the node contains no B or W property.
"""
colour, raw = self.get_raw_move()
if colour is None:
return None, None
return (colour,
sgf_properties.interpret_go_point(raw, self._presenter.size))
def get_setup_stones(self):
"""Retrieve Add Black / Add White / Add Empty properties from a node.
Returns a tuple (black_points, white_points, empty_points)
Each value is a set of pairs (row, col).
"""
try:
bp = self.get(b"AB")
except KeyError:
bp = set()
try:
wp = self.get(b"AW")
except KeyError:
wp = set()
try:
ep = self.get(b"AE")
except KeyError:
ep = set()
return bp, wp, ep
def has_setup_stones(self):
"""Check whether the node has any AB/AW/AE properties."""
d = self._property_map
return (b"AB" in d or b"AW" in d or b"AE" in d)
def set_move(self, colour, move):
"""Set the B or W property.
colour -- 'b' or 'w'.
move -- (row, col), or None for a pass.
Replaces any existing B or W property in the node.
"""
if colour not in ('b', 'w'):
raise ValueError
if b'B' in self._property_map:
del self._property_map[b'B']
if b'W' in self._property_map:
del self._property_map[b'W']
self.set(colour.upper().encode('ascii'), move)
def set_setup_stones(self, black, white, empty=None):
"""Set Add Black / Add White / Add Empty properties.
black, white, empty -- list or set of pairs (row, col)
Removes any existing AB/AW/AE properties from the node.
"""
if b'AB' in self._property_map:
del self._property_map[b'AB']
if b'AW' in self._property_map:
del self._property_map[b'AW']
if b'AE' in self._property_map:
del self._property_map[b'AE']
if black:
self.set(b'AB', black)
if white:
self.set(b'AW', white)
if empty:
self.set(b'AE', empty)
def add_comment_text(self, text):
"""Add or extend the node's comment.
If the node doesn't have a C property, adds one with the specified
text.
Otherwise, adds the specified text to the existing C property value
(with two newlines in front).
"""
if self.has_property(b'C'):
self.set(b'C', self.get(b'C') + b"\n\n" + text)
else:
self.set(b'C', text)
def __str__(self):
encoding = self.get_encoding()
def format_property(ident, values):
return ident.decode(encoding) + "".join(
"[%s]" % s.decode(encoding) for s in values)
return "\n".join(
format_property(ident, values)
for (ident, values) in sorted(self._property_map.items())) \
+ "\n"
class Tree_node(Node):
"""A node embedded in an SGF game.
A Tree_node is a Node that also knows its position within an Sgf_game.
Do not instantiate directly; retrieve from an Sgf_game or another Tree_node.
A Tree_node is a list-like container of its children: it can be indexed,
sliced, and iterated over like a list, and supports index().
A Tree_node with no children is treated as having truth value false.
Public attributes (treat as read-only):
owner -- the node's Sgf_game
parent -- the nodes's parent Tree_node (None for the root node)
"""
def __init__(self, parent, properties):
self.owner = parent.owner
self.parent = parent
self._children = []
Node.__init__(self, properties, parent._presenter)
def _add_child(self, node):
self._children.append(node)
def __len__(self):
return len(self._children)
def __getitem__(self, key):
return self._children[key]
def index(self, child):
return self._children.index(child)
def new_child(self, index=None):
"""Create a new Tree_node and add it as this node's last child.
If 'index' is specified, the new node is inserted in the child list at
the specified index instead (behaves like list.insert).
Returns the new node.
"""
child = Tree_node(self, {})
if index is None:
self._children.append(child)
else:
self._children.insert(index, child)
return child
def delete(self):
"""Remove this node from its parent."""
if self.parent is None:
raise ValueError("can't remove the root node")
self.parent._children.remove(self)
def reparent(self, new_parent, index=None):
"""Move this node to a new place in the tree.
new_parent -- Tree_node from the same game.
Raises ValueError if the new parent is this node or one of its
descendants.
If 'index' is specified, the node is inserted in the new parent's child
list at the specified index (behaves like list.insert); otherwise it's
placed at the end.
"""
if new_parent.owner != self.owner:
raise ValueError("new parent doesn't belong to the same game")
n = new_parent
while True:
if n == self:
raise ValueError("would create a loop")
n = n.parent
if n is None:
break
# self.parent is not None because moving the root would create a loop.
self.parent._children.remove(self)
self.parent = new_parent
if index is None:
new_parent._children.append(self)
else:
new_parent._children.insert(index, self)
def find(self, identifier):
"""Find the nearest ancestor-or-self containing the specified property.
Returns a Tree_node, or None if there is no such node.
"""
node = self
while node is not None:
if node.has_property(identifier):
return node
node = node.parent
return None
def find_property(self, identifier):
"""Return the value of a property, defined at this node or an ancestor.
This is intended for use with properties of type 'game-info', and with
properties with the 'inherit' attribute.
This returns the interpreted value, in the same way as get().
It searches up the tree, in the same way as find().
Raises KeyError if no node defining the property is found.
"""
node = self.find(identifier)
if node is None:
raise KeyError
return node.get(identifier)
class _Root_tree_node(Tree_node):
"""Variant of Tree_node used for a game root."""
def __init__(self, property_map, owner):
self.owner = owner
self.parent = None
self._children = []
Node.__init__(self, property_map, owner.presenter)
class _Unexpanded_root_tree_node(_Root_tree_node):
"""Variant of _Root_tree_node used with 'loaded' Sgf_games."""
def __init__(self, owner, coarse_tree):
_Root_tree_node.__init__(self, coarse_tree.sequence[0], owner)
self._coarse_tree = coarse_tree
def _expand(self):
sgf_grammar.make_tree(
self._coarse_tree, self, Tree_node, Tree_node._add_child)
delattr(self, '_coarse_tree')
self.__class__ = _Root_tree_node
def __len__(self):
self._expand()
return self.__len__()
def __getitem__(self, key):
self._expand()
return self.__getitem__(key)
def index(self, child):
self._expand()
return self.index(child)
def new_child(self, index=None):
self._expand()
return self.new_child(index)
def _main_sequence_iter(self):
presenter = self._presenter
for properties in sgf_grammar.main_sequence_iter(self._coarse_tree):
yield Node(properties, presenter)
class Sgf_game:
"""An SGF game tree.
The complete game tree is represented using Tree_nodes. The various methods
which return Tree_nodes will always return the same object for the same
node.
Instantiate with
size -- int (board size), in range 1 to 26
encoding -- the raw property encoding (default "UTF-8")
'encoding' must be a valid Python codec name.
The following root node properties are set for newly-created games:
FF[4]
GM[1]
SZ[size]
CA[encoding]
Changing FF and GM is permitted (but this library will carry on using the
FF[4] and GM[1] rules). Changing SZ is not permitted (unless the change
leaves the effective value unchanged). Changing CA is permitted; this
controls the encoding used by serialise().
"""
def __new__(cls, size, encoding="UTF-8", *args, **kwargs):
# To complete initialisation after this, you need to set 'root'.
if not 1 <= size <= 26:
raise ValueError("size out of range: %s" % size)
game = super(Sgf_game, cls).__new__(cls)
game.size = size
game.presenter = sgf_properties.Presenter(size, encoding)
return game
def __init__(self, *args, **kwargs):
self.root = _Root_tree_node({}, self)
self.root.set_raw(b'FF', b"4")
self.root.set_raw(b'GM', b"1")
self.root.set_raw(b'SZ', str(self.size).encode(self.presenter.encoding))
# Read the encoding back so we get the normalised form
self.root.set_raw(b'CA', self.presenter.encoding.encode('ascii'))
@classmethod
def from_coarse_game_tree(cls, coarse_game, override_encoding=None):
"""Alternative constructor: create an Sgf_game from the parser output.
coarse_game -- Coarse_game_tree
override_encoding -- encoding name, eg "UTF-8" (optional)
The nodes' property maps (as returned by get_raw_property_map()) will
be the same dictionary objects as the ones from the Coarse_game_tree.
The board size and raw property encoding are taken from the SZ and CA
properties in the root node (defaulting to 19 and "ISO-8859-1",
respectively).
If override_encoding is specified, the source data is assumed to be in
the specified encoding (no matter what the CA property says), and the
CA property is set to match.
"""
try:
size_s = coarse_game.sequence[0][b'SZ'][0]
except KeyError:
size = 19
else:
try:
size = int(size_s)
except ValueError:
raise ValueError("bad SZ property: %s" % size_s)
if override_encoding is None:
try:
encoding = coarse_game.sequence[0][b'CA'][0]
except KeyError:
encoding = b"ISO-8859-1"
else:
encoding = override_encoding
game = cls.__new__(cls, size, encoding)
game.root = _Unexpanded_root_tree_node(game, coarse_game)
if override_encoding is not None:
game.root.set_raw(b"CA", game.presenter.encoding.encode('ascii'))
return game
@classmethod
def from_string(cls, s, override_encoding=None):
"""Alternative constructor: read a single Sgf_game from a string.
s -- 8-bit string
Raises ValueError if it can't parse the string. See parse_sgf_game()
for details.
See from_coarse_game_tree for details of size and encoding handling.
"""
if not isinstance(s, six.binary_type):
s = s.encode('ascii')
coarse_game = sgf_grammar.parse_sgf_game(s)
return cls.from_coarse_game_tree(coarse_game, override_encoding)
def serialise(self, wrap=79):
"""Serialise the SGF data as a string.
wrap -- int (default 79), or None
Returns an 8-bit string, in the encoding specified by the CA property
in the root node (defaulting to "ISO-8859-1").
If the raw property encoding and the target encoding match (which is
the usual case), the raw property values are included unchanged in the
output (even if they are improperly encoded.)
Otherwise, if any raw property value is improperly encoded,
UnicodeDecodeError is raised, and if any property value can't be
represented in the target encoding, UnicodeEncodeError is raised.
If the target encoding doesn't identify a Python codec, ValueError is
raised. Behaviour is unspecified if the target encoding isn't
ASCII-compatible (eg, UTF-16).
If 'wrap' is not None, makes some effort to keep output lines no longer
than 'wrap'.
"""
try:
encoding = self.get_charset()
except ValueError:
raise ValueError("unsupported charset: %r" %
self.root.get_raw_list(b"CA"))
coarse_tree = sgf_grammar.make_coarse_game_tree(
self.root, lambda node: node, Node.get_raw_property_map)
serialised = sgf_grammar.serialise_game_tree(coarse_tree, wrap)
if encoding == self.root.get_encoding():
return serialised
else:
return serialised.decode(self.root.get_encoding()).encode(encoding)
def get_property_presenter(self):
"""Return the property presenter.
Returns an sgf_properties.Presenter.
This can be used to customise how property values are interpreted and
serialised.
"""
return self.presenter
def get_root(self):
"""Return the root node (as a Tree_node)."""
return self.root
def get_last_node(self):
"""Return the last node in the 'leftmost' variation (as a Tree_node)."""
node = self.root
while node:
node = node[0]
return node
def get_main_sequence(self):
"""Return the 'leftmost' variation.
Returns a list of Tree_nodes, from the root to a leaf.
"""
node = self.root
result = [node]
while node:
node = node[0]
result.append(node)
return result
def get_main_sequence_below(self, node):
"""Return the 'leftmost' variation below the specified node.
node -- Tree_node
Returns a list of Tree_nodes, from the first child of 'node' to a leaf.
"""
if node.owner is not self:
raise ValueError("node doesn't belong to this game")
result = []
while node:
node = node[0]
result.append(node)
return result
def get_sequence_above(self, node):
"""Return the partial variation leading to the specified node.
node -- Tree_node
Returns a list of Tree_nodes, from the root to the parent of 'node'.
"""
if node.owner is not self:
raise ValueError("node doesn't belong to this game")
result = []
while node.parent is not None:
node = node.parent
result.append(node)
result.reverse()
return result
def main_sequence_iter(self):
"""Provide the 'leftmost' variation as an iterator.
Returns an iterator providing Node instances, from the root to a leaf.
The Node instances may or may not be Tree_nodes.
It's OK to use these Node instances to modify properties: even if they
are not the same objects as returned by the main tree navigation
methods, they share the underlying property maps.
If you know the game has no variations, or you're only interested in
the 'leftmost' variation, you can use this function to retrieve the
nodes without building the entire game tree.
"""
if isinstance(self.root, _Unexpanded_root_tree_node):
return self.root._main_sequence_iter()
return iter(self.get_main_sequence())
def extend_main_sequence(self):
"""Create a new Tree_node and add to the 'leftmost' variation.
Returns the new node.
"""
return self.get_last_node().new_child()
def get_size(self):
"""Return the board size as an integer."""
return self.size
def get_charset(self):
"""Return the effective value of the CA root property.
This applies the default, and returns the normalised form.
Raises ValueError if the CA property doesn't identify a Python codec.
"""
try:
s = self.root.get(b"CA")
except KeyError:
return "ISO-8859-1"
try:
return sgf_properties.normalise_charset_name(s)
except LookupError:
raise ValueError("no codec available for CA %s" % s)
def get_komi(self):
"""Return the komi as a float.
Returns 0.0 if the KM property isn't present in the root node.
Raises ValueError if the KM property is malformed.
"""
try:
return self.root.get(b"KM")
except KeyError:
return 0.0
def get_handicap(self):
"""Return the number of handicap stones as a small integer.
Returns None if the HA property isn't present, or has (illegal) value
zero.
Raises ValueError if the HA property is otherwise malformed.
"""
try:
handicap = self.root.get(b"HA")
except KeyError:
return None
if handicap == 0:
handicap = None
elif handicap == 1:
raise ValueError
return handicap
def get_player_name(self, colour):
"""Return the name of the specified player.
Returns None if there is no corresponding 'PB' or 'PW' property.
"""
try:
return self.root.get(
{'b': b'PB', 'w': b'PW'}[colour]).decode(self.presenter.encoding)
except KeyError:
return None
def get_winner(self):
"""Return the colour of the winning player.
Returns None if there is no RE property, or if neither player won.
"""
try:
colour = self.root.get(b"RE").decode(self.presenter.encoding)[0].lower()
except LookupError:
return None
if colour not in ("b", "w"):
return None
return colour
def set_date(self, date=None):
"""Set the DT property to a single date.
date -- datetime.date (defaults to today)
(SGF allows dates to be rather more complicated than this, so there's
no corresponding get_date() method.)
"""
if date is None:
date = datetime.date.today()
self.root.set('DT', date.strftime("%Y-%m-%d"))
+533
View File
@@ -0,0 +1,533 @@
"""Parse and serialise SGF data.
This is intended for use with SGF FF[4]; see http://www.red-bean.com/sgf/
Nothing in this module is Go-specific.
This module is encoding-agnostic: it works with 8-bit strings in an arbitrary
'ascii-compatible' encoding.
In the documentation below, a _property map_ is a dict mapping a PropIdent to a
nonempty list of raw property values.
A raw property value is an 8-bit string containing a PropValue without its
enclosing brackets, but with backslashes and line endings left untouched.
So a property map's keys should pass is_valid_property_identifier(), and its
values should pass is_valid_property_value().
Adapted from gomill by Matthew Woodcraft, https://github.com/mattheww/gomill
"""
from __future__ import absolute_import
import re
import string
import six
_propident_re = re.compile(r"\A[A-Z]{1,8}\Z".encode('ascii'))
_propvalue_re = re.compile(r"\A [^\\\]]* (?: \\. [^\\\]]* )* \Z".encode('ascii'),
re.VERBOSE | re.DOTALL)
_find_start_re = re.compile(r"\(\s*;".encode('ascii'))
_tokenise_re = re.compile(r"""
\s*
(?:
\[ (?P<V> [^\\\]]* (?: \\. [^\\\]]* )* ) \] # PropValue
|
(?P<I> [A-Z]{1,8} ) # PropIdent
|
(?P<D> [;()] ) # delimiter
)
""".encode('ascii'), re.VERBOSE | re.DOTALL)
def is_valid_property_identifier(s):
"""Check whether 's' is a well-formed PropIdent.
s -- 8-bit string
This accepts the same values as the tokeniser.
Details:
- it doesn't permit lower-case letters (these are allowed in some ancient
SGF variants)
- it accepts at most 8 letters (there is no limit in the spec; no standard
property has more than 2)
"""
return bool(_propident_re.search(s))
def is_valid_property_value(s):
"""Check whether 's' is a well-formed PropValue.
s -- 8-bit string
This accepts the same values as the tokeniser: any string that doesn't
contain an unescaped ] or end with an unescaped \ .
"""
return bool(_propvalue_re.search(s))
def tokenise(s, start_position=0):
"""Tokenise a string containing SGF data.
s -- 8-bit string
start_position -- index into 's'
Skips leading junk.
Returns a list of pairs of strings (token type, contents), and also the
index in 's' of the start of the unprocessed 'tail'.
token types and contents:
I -- PropIdent: upper-case letters
V -- PropValue: raw value, without the enclosing brackets
D -- delimiter: ';', '(', or ')'
Stops when it has seen as many closing parens as open ones, at the end of
the string, or when it first finds something it can't tokenise.
The first two tokens are always '(' and ';' (otherwise it won't find the
start of the content).
"""
result = []
m = _find_start_re.search(s, start_position)
if not m:
return [], 0
i = m.start()
depth = 0
while True:
m = _tokenise_re.match(s, i)
if not m:
break
group = m.lastgroup
token = m.group(m.lastindex)
result.append((group, token))
i = m.end()
if group == 'D':
if token == b'(':
depth += 1
elif token == b')':
depth -= 1
if depth == 0:
break
return result, i
class Coarse_game_tree:
"""An SGF GameTree.
This is a direct representation of the SGF parse tree. It's 'coarse' in the
sense that the objects in the tree structure represent node sequences, not
individual nodes.
Public attributes
sequence -- nonempty list of property maps
children -- list of Coarse_game_trees
The sequence represents the nodes before the variations.
"""
def __init__(self):
self.sequence = [] # must be at least one node
self.children = [] # may be empty
def _parse_sgf_game(s, start_position):
"""Common implementation for parse_sgf_game and parse_sgf_games."""
tokens, end_position = tokenise(s, start_position)
if not tokens:
return None, None
stack = []
game_tree = None
sequence = None
properties = None
index = 0
try:
while True:
token_type, token = tokens[index]
index += 1
if token_type == 'V':
raise ValueError("unexpected value")
if token_type == 'D':
if token == b';':
if sequence is None:
raise ValueError("unexpected node")
properties = {}
sequence.append(properties)
else:
if sequence is not None:
if not sequence:
raise ValueError("empty sequence")
game_tree.sequence = sequence
sequence = None
if token == b'(':
stack.append(game_tree)
game_tree = Coarse_game_tree()
sequence = []
else:
# token == ')'
variation = game_tree
game_tree = stack.pop()
if game_tree is None:
break
game_tree.children.append(variation)
properties = None
else:
# token_type == 'I'
prop_ident = token
prop_values = []
while True:
token_type, token = tokens[index]
if token_type != 'V':
break
index += 1
prop_values.append(token)
if not prop_values:
raise ValueError("property with no values")
try:
if prop_ident in properties:
properties[prop_ident] += prop_values
else:
properties[prop_ident] = prop_values
except TypeError:
raise ValueError("property value outside a node")
except IndexError:
raise ValueError("unexpected end of SGF data")
assert index == len(tokens)
return variation, end_position
def parse_sgf_game(s):
"""Read a single SGF game from a string, returning the parse tree.
s -- 8-bit string
Returns a Coarse_game_tree.
Applies the rules for FF[4].
Raises ValueError if can't parse the string.
If a property appears more than once in a node (which is not permitted by
the spec), treats it the same as a single property with multiple values.
Identifies the start of the SGF content by looking for '(;' (with possible
whitespace between); ignores everything preceding that. Ignores everything
following the first game.
"""
game_tree, _ = _parse_sgf_game(s, 0)
if game_tree is None:
raise ValueError("no SGF data found")
return game_tree
def parse_sgf_collection(s):
"""Read an SGF game collection, returning the parse trees.
s -- 8-bit string
Returns a nonempty list of Coarse_game_trees.
Raises ValueError if no games were found in the string.
Raises ValueError if there is an error parsing a game. See
parse_sgf_game() for details.
Ignores non-SGF data before the first game, between games, and after the
final game. Identifies the start of each game in the same way as
parse_sgf_game().
"""
position = 0
result = []
while True:
try:
game_tree, position = _parse_sgf_game(s, position)
except ValueError as e:
raise ValueError("error parsing game %d: %s" % (len(result), e))
if game_tree is None:
break
result.append(game_tree)
if not result:
raise ValueError("no SGF data found")
return result
def block_format(pieces, width=79):
"""Concatenate bytestrings, adding newlines.
pieces -- iterable of strings
width -- int (default 79)
Returns "".join(pieces), with added newlines between pieces as necessary to
avoid lines longer than 'width'.
Leaves newlines inside 'pieces' untouched, and ignores them in its width
calculation. If a single piece is longer than 'width', it will become a
single long line in the output.
"""
lines = []
line = b""
for s in pieces:
if len(line) + len(s) > width:
lines.append(line)
line = b""
line += s
if line:
lines.append(line)
return b"\n".join(lines)
def serialise_game_tree(game_tree, wrap=79):
"""Serialise an SGF game as a string.
game_tree -- Coarse_game_tree
wrap -- int (default 79), or None
Returns an 8-bit string, ending with a newline.
If 'wrap' is not None, makes some effort to keep output lines no longer
than 'wrap'.
"""
l = []
to_serialise = [game_tree]
while to_serialise:
game_tree = to_serialise.pop()
if game_tree is None:
l.append(b")")
continue
l.append(b"(")
for properties in game_tree.sequence:
l.append(b";")
# Force FF to the front, largely to work around a Quarry bug which
# makes it ignore the first few bytes of the file.
for prop_ident, prop_values in sorted(
list(properties.items()),
key=lambda pair: (-(pair[0] == b"FF"), pair[0])):
# Make a single string for each property, to get prettier
# block_format output.
m = [prop_ident]
for value in prop_values:
m.append(b"[" + value + b"]")
l.append(b"".join(m))
to_serialise.append(None)
to_serialise.extend(reversed(game_tree.children))
l.append(b"\n")
if wrap is None:
return b"".join(l)
else:
return block_format(l, wrap)
def make_tree(game_tree, root, node_builder, node_adder):
"""Construct a node tree from a Coarse_game_tree.
game_tree -- Coarse_game_tree
root -- node
node_builder -- function taking parameters (parent node, property map)
returning a node
node_adder -- function taking a pair (parent node, child node)
Builds a tree of nodes corresponding to this GameTree, calling
node_builder() to make new nodes and node_adder() to add child nodes to
their parent.
Makes no further assumptions about the node type.
"""
to_build = [(root, game_tree, 0)]
while to_build:
node, game_tree, index = to_build.pop()
if index < len(game_tree.sequence) - 1:
child = node_builder(node, game_tree.sequence[index + 1])
node_adder(node, child)
to_build.append((child, game_tree, index + 1))
else:
node._children = []
for child_tree in game_tree.children:
child = node_builder(node, child_tree.sequence[0])
node_adder(node, child)
to_build.append((child, child_tree, 0))
def make_coarse_game_tree(root, get_children, get_properties):
"""Construct a Coarse_game_tree from a node tree.
root -- node
get_children -- function taking a node, returning a sequence of nodes
get_properties -- function taking a node, returning a property map
Returns a Coarse_game_tree.
Walks the node tree based at 'root' using get_children(), and uses
get_properties() to extract the raw properties.
Makes no further assumptions about the node type.
Doesn't check that the property maps have well-formed keys and values.
"""
result = Coarse_game_tree()
to_serialise = [(result, root)]
while to_serialise:
game_tree, node = to_serialise.pop()
while True:
game_tree.sequence.append(get_properties(node))
children = get_children(node)
if len(children) != 1:
break
node = children[0]
for child in children:
child_tree = Coarse_game_tree()
game_tree.children.append(child_tree)
to_serialise.append((child_tree, child))
return result
def main_sequence_iter(game_tree):
"""Provide the 'leftmost' complete sequence of a Coarse_game_tree.
game_tree -- Coarse_game_tree
Returns an iterable of property maps.
If the game has no variations, this provides the complete game. Otherwise,
it chooses the first variation each time it has a choice.
"""
while True:
for properties in game_tree.sequence:
yield properties
if not game_tree.children:
break
game_tree = game_tree.children[0]
_split_compose_re = re.compile(
r"( (?: [^\\:] | \\. )* ) :".encode('ascii'),
re.VERBOSE | re.DOTALL)
def parse_compose(s):
"""Split the parts of an SGF Compose value.
If the value is a well-formed Compose, returns a pair of strings.
If it isn't (ie, there is no delimiter), returns the complete string and
None.
Interprets backslash escapes in order to find the delimiter, but leaves
backslash escapes unchanged in the returned strings.
"""
m = _split_compose_re.match(s)
if not m:
return s, None
return m.group(1), s[m.end():]
def compose(s1, s2):
"""Construct a value of Compose value type.
s1, s2 -- serialised form of a property value
(This is only needed if the type of the first value permits colons.)
"""
return s1.replace(b":", b"\\:") + b":" + s2
_newline_re = re.compile(r"\n\r|\r\n|\n|\r".encode('ascii'))
if six.PY2:
_binary_maketrans = string.maketrans
else:
_binary_maketrans = bytes.maketrans
_whitespace_table = _binary_maketrans(b"\t\f\v", b" ")
_chunk_re = re.compile(r" [^\n\\]+ | [\n\\] ".encode('ascii'), re.VERBOSE)
def simpletext_value(s):
"""Convert a raw SimpleText property value to the string it represents.
Returns an 8-bit string, in the encoding of the original SGF string.
This interprets escape characters, and does whitespace mapping:
- backslash followed by linebreak (LF, CR, LFCR, or CRLF) disappears
- any other linebreak is replaced by a space
- any other whitespace character is replaced by a space
- other backslashes disappear (but double-backslash -> single-backslash)
"""
s = _newline_re.sub(b"\n", s)
s = s.translate(_whitespace_table)
is_escaped = False
result = []
for chunk in _chunk_re.findall(s):
if is_escaped:
if chunk != b"\n":
result.append(chunk)
is_escaped = False
elif chunk == b"\\":
is_escaped = True
elif chunk == b"\n":
result.append(b" ")
else:
result.append(chunk)
return b"".join(result)
def text_value(s):
"""Convert a raw Text property value to the string it represents.
Returns an 8-bit string, in the encoding of the original SGF string.
This interprets escape characters, and does whitespace mapping:
- linebreak (LF, CR, LFCR, or CRLF) is converted to \n
- any other whitespace character is replaced by a space
- backslash followed by linebreak disappears
- other backslashes disappear (but double-backslash -> single-backslash)
"""
s = _newline_re.sub(b"\n", s)
s = s.translate(_whitespace_table)
is_escaped = False
result = []
for chunk in _chunk_re.findall(s):
if is_escaped:
if chunk != b"\n":
result.append(chunk)
is_escaped = False
elif chunk == b"\\":
is_escaped = True
else:
result.append(chunk)
return b"".join(result)
def escape_text(s):
"""Convert a string to a raw Text property value that represents it.
s -- 8-bit string, in the desired output encoding.
Returns an 8-bit string which passes is_valid_property_value().
Normally text_value(escape_text(s)) == s, but there are the following
exceptions:
- all linebreaks are are normalised to \n
- whitespace other than line breaks is converted to a single space
"""
return s.replace(b"\\", b"\\\\").replace(b"]", b"\\]")
+763
View File
@@ -0,0 +1,763 @@
"""Interpret SGF property values.
This is intended for use with SGF FF[4]; see http://www.red-bean.com/sgf/
This supports all general properties and Go-specific properties, but not
properties for other games. Point, Move and Stone values are interpreted as Go
points.
Adapted from gomill by Matthew Woodcraft, https://github.com/mattheww/gomill
"""
from __future__ import absolute_import
import codecs
from math import isinf, isnan
import six
from tugo.gosgf import sgf_grammar
from six.moves import range
# In python 2, indexing a str gives one-character strings.
# In python 3, indexing a bytes gives ints.
if six.PY2:
_bytestring_ord = ord
else:
def identity(x):
return x
_bytestring_ord = identity
def normalise_charset_name(s):
"""Convert an encoding name to the form implied in the SGF spec.
In particular, normalises to 'ISO-8859-1' and 'UTF-8'.
Raises LookupError if the encoding name isn't known to Python.
"""
if not isinstance(s, six.text_type):
s = s.decode('ascii')
return (codecs.lookup(s).name.replace("_", "-").upper()
.replace("ISO8859", "ISO-8859"))
def interpret_go_point(s, size):
"""Convert a raw SGF Go Point, Move, or Stone value to coordinates.
s -- 8-bit string
size -- board size (int)
Returns a pair (row, col), or None for a pass.
Raises ValueError if the string is malformed or the coordinates are out of
range.
Only supports board sizes up to 26.
The returned coordinates are in the GTP coordinate system (as in the rest
of gomill), where (0, 0) is the lower left.
"""
if s == b"" or (s == b"tt" and size <= 19):
return None
# May propagate ValueError
col_s, row_s = s
col = _bytestring_ord(col_s) - 97 # 97 == ord("a")
row = size - _bytestring_ord(row_s) + 96
if not ((0 <= col < size) and (0 <= row < size)):
raise ValueError
return row, col
def serialise_go_point(move, size):
"""Serialise a Go Point, Move, or Stone value.
move -- pair (row, col), or None for a pass
Returns an 8-bit string.
Only supports board sizes up to 26.
The move coordinates are in the GTP coordinate system (as in the rest of
gomill), where (0, 0) is the lower left.
"""
if not 1 <= size <= 26:
raise ValueError
if move is None:
# Prefer 'tt' where possible, for the sake of older code
if size <= 19:
return b"tt"
else:
return b""
row, col = move
if not ((0 <= col < size) and (0 <= row < size)):
raise ValueError
col_s = "abcdefghijklmnopqrstuvwxy"[col].encode('ascii')
row_s = "abcdefghijklmnopqrstuvwxy"[size - row - 1].encode('ascii')
return col_s + row_s
class _Context:
def __init__(self, size, encoding):
self.size = size
self.encoding = encoding
def interpret_none(s, context=None):
"""Convert a raw None value to a boolean.
That is, unconditionally returns True.
"""
return True
def serialise_none(b, context=None):
"""Serialise a None value.
Ignores its parameter.
"""
return b""
def interpret_number(s, context=None):
"""Convert a raw Number value to the integer it represents.
This is a little more lenient than the SGF spec: it permits leading and
trailing spaces, and spaces between the sign and the numerals.
"""
return int(s, 10)
def serialise_number(i, context=None):
"""Serialise a Number value.
i -- integer
"""
return ("%d" % i).encode('ascii')
def interpret_real(s, context=None):
"""Convert a raw Real value to the float it represents.
This is more lenient than the SGF spec: it accepts strings accepted as a
float by the platform libc. It rejects infinities and NaNs.
"""
result = float(s)
if isinf(result):
raise ValueError("infinite")
if isnan(result):
raise ValueError("not a number")
return result
def serialise_real(f, context=None):
"""Serialise a Real value.
f -- real number (int or float)
If the absolute value is too small to conveniently express as a decimal,
returns "0" (this currently happens if abs(f) is less than 0.0001).
"""
f = float(f)
try:
i = int(f)
except OverflowError:
# infinity
raise ValueError
if f == i:
# avoid trailing '.0'; also avoid scientific notation for large numbers
return str(i).encode('ascii')
s = repr(f)
if 'e-' in s:
return "0".encode('ascii')
return s.encode('ascii')
def interpret_double(s, context=None):
"""Convert a raw Double value to an integer.
Returns 1 or 2 (unknown values are treated as 1).
"""
if s.strip() == b"2":
return 2
else:
return 1
def serialise_double(i, context=None):
"""Serialise a Double value.
i -- integer (1 or 2)
(unknown values are treated as 1)
"""
if i == 2:
return "2"
return "1"
def interpret_colour(s, context=None):
"""Convert a raw Color value to a gomill colour.
Returns 'b' or 'w'.
"""
colour = s.decode('ascii').lower()
if colour not in ('b', 'w'):
raise ValueError
return colour
def serialise_colour(colour, context=None):
"""Serialise a Colour value.
colour -- 'b' or 'w'
"""
if colour not in ('b', 'w'):
raise ValueError
return colour.upper().encode('ascii')
def _transcode(s, encoding):
"""Common implementation for interpret_text and interpret_simpletext."""
# If encoding is UTF-8, we don't need to transcode, but we still want to
# report an error if it's not properly encoded.
u = s.decode(encoding)
if encoding == "UTF-8":
return s
else:
return u.encode("utf-8")
def interpret_simpletext(s, context):
"""Convert a raw SimpleText value to a string.
See sgf_grammar.simpletext_value() for details.
s -- raw value
Returns an 8-bit utf-8 string.
"""
return _transcode(sgf_grammar.simpletext_value(s), context.encoding)
def serialise_simpletext(s, context):
"""Serialise a SimpleText value.
See sgf_grammar.escape_text() for details.
s -- 8-bit utf-8 string
"""
if context.encoding != "UTF-8":
s = s.decode("utf-8").encode(context.encoding)
return sgf_grammar.escape_text(s)
def interpret_text(s, context):
"""Convert a raw Text value to a string.
See sgf_grammar.text_value() for details.
s -- raw value
Returns an 8-bit utf-8 string.
"""
return _transcode(sgf_grammar.text_value(s), context.encoding)
def serialise_text(s, context):
"""Serialise a Text value.
See sgf_grammar.escape_text() for details.
s -- 8-bit utf-8 string
"""
if context.encoding != "UTF-8":
s = s.decode("utf-8").encode(context.encoding)
return sgf_grammar.escape_text(s)
def interpret_point(s, context):
"""Convert a raw SGF Point or Stone value to coordinates.
See interpret_go_point() above for details.
Returns a pair (row, col).
"""
result = interpret_go_point(s, context.size)
if result is None:
raise ValueError
return result
def serialise_point(point, context):
"""Serialise a Point or Stone value.
point -- pair (row, col)
See serialise_go_point() above for details.
"""
if point is None:
raise ValueError
return serialise_go_point(point, context.size)
def interpret_move(s, context):
"""Convert a raw SGF Move value to coordinates.
See interpret_go_point() above for details.
Returns a pair (row, col), or None for a pass.
"""
return interpret_go_point(s, context.size)
def serialise_move(move, context):
"""Serialise a Move value.
move -- pair (row, col), or None for a pass
See serialise_go_point() above for details.
"""
return serialise_go_point(move, context.size)
def interpret_point_list(values, context):
"""Convert a raw SGF list of Points to a set of coordinates.
values -- list of strings
Returns a set of pairs (row, col).
If 'values' is empty, returns an empty set.
This interprets compressed point lists.
Doesn't complain if there is overlap, or if a single point is specified as
a 1x1 rectangle.
Raises ValueError if the data is otherwise malformed.
"""
result = set()
for s in values:
# No need to use parse_compose(), as \: would always be an error.
p1, is_rectangle, p2 = s.partition(b":")
if is_rectangle:
top, left = interpret_point(p1, context)
bottom, right = interpret_point(p2, context)
if not (bottom <= top and left <= right):
raise ValueError
for row in range(bottom, top + 1):
for col in range(left, right + 1):
result.add((row, col))
else:
pt = interpret_point(p1, context)
result.add(pt)
return result
def serialise_point_list(points, context):
"""Serialise a list of Points, Moves, or Stones.
points -- iterable of pairs (row, col)
Returns a list of strings.
If 'points' is empty, returns an empty list.
Doesn't produce a compressed point list.
"""
result = [serialise_point(point, context) for point in points]
result.sort()
return result
def interpret_AP(s, context):
"""Interpret an AP (application) property value.
Returns a pair of strings (name, version number)
Permits the version number to be missing (which is forbidden by the SGF
spec), in which case the second returned value is an empty string.
"""
application, version = sgf_grammar.parse_compose(s)
if version is None:
version = b""
return (interpret_simpletext(application, context),
interpret_simpletext(version, context))
def serialise_AP(value, context):
"""Serialise an AP (application) property value.
value -- pair (application, version)
application -- string
version -- string
Note this takes a single parameter (which is a pair).
"""
application, version = value
return sgf_grammar.compose(serialise_simpletext(application, context),
serialise_simpletext(version, context))
def interpret_ARLN_list(values, context):
"""Interpret an AR (arrow) or LN (line) property value.
Returns a list of pairs (point, point), where point is a pair (row, col)
"""
result = []
for s in values:
p1, p2 = sgf_grammar.parse_compose(s)
result.append((interpret_point(p1, context),
interpret_point(p2, context)))
return result
def serialise_ARLN_list(values, context):
"""Serialise an AR (arrow) or LN (line) property value.
values -- list of pairs (point, point), where point is a pair (row, col)
"""
return [b":".join((serialise_point(p1, context), serialise_point(p2, context)))
for p1, p2 in values]
def interpret_FG(s, context):
"""Interpret an FG (figure) property value.
Returns a pair (flags, string), or None.
flags is an integer; see http://www.red-bean.com/sgf/properties.html#FG
"""
if s == b"":
return None
flags, name = sgf_grammar.parse_compose(s)
return int(flags), interpret_simpletext(name, context)
def serialise_FG(value, context):
"""Serialise an FG (figure) property value.
value -- pair (flags, name), or None
flags -- int
name -- string
Use serialise_FG(None) to produce an empty value.
"""
if value is None:
return b""
flags, name = value
return str(flags).encode('ascii') + b":" + serialise_simpletext(name, context)
def interpret_LB_list(values, context):
"""Interpret an LB (label) property value.
Returns a list of pairs ((row, col), string).
"""
result = []
for s in values:
point, label = sgf_grammar.parse_compose(s)
result.append((interpret_point(point, context),
interpret_simpletext(label, context)))
return result
def serialise_LB_list(values, context):
"""Serialise an LB (label) property value.
values -- list of pairs ((row, col), string)
"""
return [b":".join((serialise_point(point, context), serialise_simpletext(text, context)))
for point, text in values]
class Property_type:
"""Description of a property type."""
def __init__(self, interpreter, serialiser, uses_list,
allows_empty_list=False):
self.interpreter = interpreter
self.serialiser = serialiser
self.uses_list = bool(uses_list)
self.allows_empty_list = bool(allows_empty_list)
def _make_property_type(type_name, allows_empty_list=False):
return Property_type(
globals()["interpret_" + type_name],
globals()["serialise_" + type_name],
uses_list=(type_name.endswith("_list")),
allows_empty_list=allows_empty_list)
_property_types_by_name = {
'none': _make_property_type('none'),
'number': _make_property_type('number'),
'real': _make_property_type('real'),
'double': _make_property_type('double'),
'colour': _make_property_type('colour'),
'simpletext': _make_property_type('simpletext'),
'text': _make_property_type('text'),
'point': _make_property_type('point'),
'move': _make_property_type('move'),
'point_list': _make_property_type('point_list'),
'point_elist': _make_property_type('point_list', allows_empty_list=True),
'stone_list': _make_property_type('point_list'),
'AP': _make_property_type('AP'),
'ARLN_list': _make_property_type('ARLN_list'),
'FG': _make_property_type('FG'),
'LB_list': _make_property_type('LB_list'),
}
P = _property_types_by_name
_property_types_by_ident = {
b'AB': P['stone_list'], # setup Add Black
b'AE': P['point_list'], # setup Add Empty
b'AN': P['simpletext'], # game-info Annotation
b'AP': P['AP'], # root Application
b'AR': P['ARLN_list'], # - Arrow
b'AW': P['stone_list'], # setup Add White
b'B': P['move'], # move Black
b'BL': P['real'], # move Black time left
b'BM': P['double'], # move Bad move
b'BR': P['simpletext'], # game-info Black rank
b'BT': P['simpletext'], # game-info Black team
b'C': P['text'], # - Comment
b'CA': P['simpletext'], # root Charset
b'CP': P['simpletext'], # game-info Copyright
b'CR': P['point_list'], # - Circle
b'DD': P['point_elist'], # - [inherit] Dim points
b'DM': P['double'], # - Even position
b'DO': P['none'], # move Doubtful
b'DT': P['simpletext'], # game-info Date
b'EV': P['simpletext'], # game-info Event
b'FF': P['number'], # root Fileformat
b'FG': P['FG'], # - Figure
b'GB': P['double'], # - Good for Black
b'GC': P['text'], # game-info Game comment
b'GM': P['number'], # root Game
b'GN': P['simpletext'], # game-info Game name
b'GW': P['double'], # - Good for White
b'HA': P['number'], # game-info Handicap
b'HO': P['double'], # - Hotspot
b'IT': P['none'], # move Interesting
b'KM': P['real'], # game-info Komi
b'KO': P['none'], # move Ko
b'LB': P['LB_list'], # - Label
b'LN': P['ARLN_list'], # - Line
b'MA': P['point_list'], # - Mark
b'MN': P['number'], # move set move number
b'N': P['simpletext'], # - Nodename
b'OB': P['number'], # move OtStones Black
b'ON': P['simpletext'], # game-info Opening
b'OT': P['simpletext'], # game-info Overtime
b'OW': P['number'], # move OtStones White
b'PB': P['simpletext'], # game-info Player Black
b'PC': P['simpletext'], # game-info Place
b'PL': P['colour'], # setup Player to play
b'PM': P['number'], # - [inherit] Print move mode
b'PW': P['simpletext'], # game-info Player White
b'RE': P['simpletext'], # game-info Result
b'RO': P['simpletext'], # game-info Round
b'RU': P['simpletext'], # game-info Rules
b'SL': P['point_list'], # - Selected
b'SO': P['simpletext'], # game-info Source
b'SQ': P['point_list'], # - Square
b'ST': P['number'], # root Style
b'SZ': P['number'], # root Size
b'TB': P['point_elist'], # - Territory Black
b'TE': P['double'], # move Tesuji
b'TM': P['real'], # game-info Timelimit
b'TR': P['point_list'], # - Triangle
b'TW': P['point_elist'], # - Territory White
b'UC': P['double'], # - Unclear pos
b'US': P['simpletext'], # game-info User
b'V': P['real'], # - Value
b'VW': P['point_elist'], # - [inherit] View
b'W': P['move'], # move White
b'WL': P['real'], # move White time left
b'WR': P['simpletext'], # game-info White rank
b'WT': P['simpletext'], # game-info White team
}
_text_property_type = P['text']
del P
class Presenter(_Context):
"""Convert property values between Python and SGF-string representations.
Instantiate with:
size -- board size (int)
encoding -- encoding for the SGF strings
Public attributes (treat as read-only):
size -- int
encoding -- string (normalised form)
See the _property_types_by_ident table above for a list of properties
initially known, and their types.
Initially, treats unknown (private) properties as if they had type Text.
"""
def __init__(self, size, encoding):
try:
encoding = normalise_charset_name(encoding)
except LookupError:
raise ValueError("unknown encoding: %s" % encoding)
_Context.__init__(self, size, encoding)
self.property_types_by_ident = _property_types_by_ident.copy()
self.default_property_type = _text_property_type
def get_property_type(self, identifier):
"""Return the Property_type for the specified PropIdent.
Rasies KeyError if the property is unknown.
"""
return self.property_types_by_ident[identifier]
def register_property(self, identifier, property_type):
"""Specify the Property_type for a PropIdent."""
self.property_types_by_ident[identifier] = property_type
def deregister_property(self, identifier):
"""Forget the type for the specified PropIdent."""
del self.property_types_by_ident[identifier]
def set_private_property_type(self, property_type):
"""Specify the Property_type to use for unknown properties.
Pass property_type = None to make unknown properties raise an error.
"""
self.default_property_type = property_type
def _get_effective_property_type(self, identifier):
try:
return self.property_types_by_ident[identifier]
except KeyError:
result = self.default_property_type
if result is None:
raise ValueError("unknown property")
return result
def interpret_as_type(self, property_type, raw_values):
"""Variant of interpret() for explicitly specified type.
property_type -- Property_type
"""
if not raw_values:
raise ValueError("no raw values")
if property_type.uses_list:
if raw_values == [b""]:
raw = []
else:
raw = raw_values
else:
if len(raw_values) > 1:
raise ValueError("multiple values")
raw = raw_values[0]
return property_type.interpreter(raw, self)
def interpret(self, identifier, raw_values):
"""Return a Python representation of a property value.
identifier -- PropIdent
raw_values -- nonempty list of 8-bit strings in the presenter's encoding
See the interpret_... functions above for details of how values are
represented as Python types.
Raises ValueError if it cannot interpret the value.
Note that in some cases the interpret_... functions accept values which
are not strictly permitted by the specification.
elist handling: if the property's value type is a list type and
'raw_values' is a list containing a single empty string, passes an
empty list to the interpret_... function (that is, this function treats
all lists like elists).
Doesn't enforce range restrictions on values with type Number.
"""
return self.interpret_as_type(
self._get_effective_property_type(identifier), raw_values)
def serialise_as_type(self, property_type, value):
"""Variant of serialise() for explicitly specified type.
property_type -- Property_type
"""
serialised = property_type.serialiser(value, self)
if property_type.uses_list:
if serialised == []:
if property_type.allows_empty_list:
return [b""]
else:
raise ValueError("empty list")
return serialised
else:
return [serialised]
def serialise(self, identifier, value):
"""Serialise a Python representation of a property value.
identifier -- PropIdent
value -- corresponding Python value
Returns a nonempty list of 8-bit strings in the presenter's encoding,
suitable for use as raw PropValues.
See the serialise_... functions above for details of the acceptable
values for each type.
elist handling: if the property's value type is an elist type and the
serialise_... function returns an empty list, this returns a list
containing a single empty string.
Raises ValueError if it cannot serialise the value.
In general, the serialise_... functions try not to produce an invalid
result, but do not try to prevent garbage input happening to produce a
valid result.
"""
return self.serialise_as_type(
self._get_effective_property_type(identifier), value)