Changeset - d57d0d4ede15
[Not reviewed]
default
0 13 0
Laman - 3 years ago 2022-03-05 18:37:25

changed the naming convention to follow standards
13 files changed with 222 insertions and 221 deletions:
0 comments (0 inline, 0 general)
src/diana/config.py
Show inline comments
 
import os
 
from argparse import ArgumentParser
 

	
 

	
 
progName = "DianaXO"
 
version = (0, 1, 0)
 
PROG_NAME = "DianaXO"
 
VERSION = (0, 1, 0)
 

	
 
curDir = os.path.dirname(__file__)
 
cur_dir = os.path.dirname(__file__)
 

	
 
parser = ArgumentParser()
 
parser.add_argument("--encoding")
 
parser.add_argument("-s", "--suffix")
 
parser.add_argument("-r", "--recursive")
 
parser.add_argument("-i", "--input", nargs="+", required=True)
 
parser.add_argument("-o", "--output")
 
parser.add_argument("--format", choices={"svg"})
 
parser.add_argument("-k", "--keep-broken")
 
parser.add_argument("--moves-p-d", type=int)
 
parser.add_argument("--min-p-d", type=int)
 
parser.add_argument("--version", action="version", version="{0} {1}.{2}.{3}".format(progName, *version))
 
parser.add_argument("--version", action="version", version="{0} {1}.{2}.{3}".format(PROG_NAME, *VERSION))
 

	
 

	
 
inputFiles = []
 
input_files = []
 
encoding = "utf-8-sig"
 
sgfSuffix = True
 
sgf_suffix = True
 
recursive = False
 
outputDir = curDir
 
outputFormat = "svg"
 
keepBroken = False
 
output_dir = cur_dir
 
output_format = "svg"
 
keep_broken = False
 

	
 
movesPerDiagram = 100
 
minMovesPerDiagram = 10
 
moves_per_diagram = 100
 
min_moves_per_diagram = 10
 

	
 

	
 
def parseArgs():
 
	global inputFiles, encoding, sgfSuffix, recursive, outputDir, outputFormat, keepBroken, movesPerDiagram
 
	global minMovesPerDiagram
 
def parse_args():
 
	global input_files, encoding, sgf_suffix, recursive, output_dir, output_format, keep_broken, moves_per_diagram
 
	global min_moves_per_diagram
 

	
 
	args=parser.parse_args()
 
	args = parser.parse_args()
 

	
 
	inputFiles=args.input
 
	input_files = args.input
 
	if args.encoding: encoding = args.encoding
 
	if args.suffix: sgfSuffix = True
 
	if args.suffix: sgf_suffix = True
 
	if args.recursive: recursive = True
 
	if args.output: outputDir = args.output
 
	if args.format: outputFormat = args.format
 
	if args.keep_broken is not None: keepBroken = True
 
	if args.output: output_dir = args.output
 
	if args.format: output_format = args.format
 
	if args.keep_broken is not None: keep_broken = True
 

	
 
	if args.moves_p_d: movesPerDiagram = args.moves_p_d
 
	if args.min_p_d: minMovesPerDiagram = args.min_p_d
 
	if args.moves_p_d: moves_per_diagram = args.moves_p_d
 
	if args.min_p_d: min_moves_per_diagram = args.min_p_d
src/diana/diana.py
Show inline comments
 
import os
 
import re
 

	
 
from . import config as cfg
 
from . import go
 
from .go import BLACK,WHITE,EMPTY
 
from .go import BLACK, WHITE, EMPTY
 
from .sgfParser import ParserError
 
from .sgfParser.collection import Collection
 
from .drawer.svg import Svg
 
from .drawer.tikz import Tikz
 

	
 

	
 
def collectMoves(root):
 
def collect_moves(root):
 
	node = root
 
	while len(node.children) > 0:
 
		b = node.getProp("B")
 
		w = node.getProp("W")
 
		b = node.get_prop("B")
 
		w = node.get_prop("W")
 
		if b is not None:
 
			yield ("b", b)
 
		elif w is not None:
 
			yield ("w", w)
 
		# else: yield None # !! not really robust
 

	
 
		node = node.children[0]
 

	
 

	
 
class SourceFile:
 
	def __init__(self, fileName):
 
		self.fileName = fileName
 
		self._shortName = "".join(re.split(r'[/\\]', fileName)[-1].split('.')[:-1])
 
	def __init__(self, file_name):
 
		self.file_name = file_name
 
		self._short_name = "".join(re.split(r'[/\\]', file_name)[-1].split('.')[:-1])
 
		self._game = go.Go()
 

	
 
		with open(self.fileName, 'r', encoding=cfg.encoding) as f:
 
			games = Collection(f.read()).listGames()
 
		with open(self.file_name, 'r', encoding=cfg.encoding) as f:
 
			games = Collection(f.read()).list_games()
 
		self._record = list(games)[0]
 
		self._moves = list(collectMoves(self._record.root))
 
		self._moves = list(collect_moves(self._record.root))
 

	
 
	def process(self):
 
		print("{0}... ".format(self.fileName), end="")
 
		print("{0}... ".format(self.file_name), end="")
 

	
 
		i = 1
 
		for k in range(0,len(self._moves), cfg.movesPerDiagram):
 
			filename = os.path.join(cfg.outputDir, "{0}-{1}".format(self._shortName, i))
 
			self.createDiagram(k, k+cfg.movesPerDiagram).save(filename, "templ-pleb.svg")
 
		for k in range(0, len(self._moves), cfg.moves_per_diagram):
 
			filename = os.path.join(cfg.output_dir, "{0}-{1}".format(self._short_name, i))
 
			self.create_diagram(k, k + cfg.moves_per_diagram).save(filename, "templ-pleb.svg")
 
			i += 1
 

	
 
		infoStr = """{GN}
 
		info_str = """{GN}
 
B: {PB} {BR}
 
W: {PW} {WR}
 
{DT}
 
{RE}""".format(**self.fetchGameInfo(["GN","PB","BR","PW","WR","DT","RE"], ""))
 
		notes = open(os.path.join(cfg.outputDir, "{0}.txt".format(self._shortName)), 'w')
 
		notes.write(infoStr)
 
{RE}""".format(**self.fetch_game_info(["GN", "PB", "BR", "PW", "WR", "DT", "RE"], ""))
 
		notes = open(os.path.join(cfg.output_dir, "{0}.txt".format(self._short_name)), 'w')
 
		notes.write(info_str)
 
		notes.close()
 
		print("done")
 

	
 
	def createDiagram(self, start, end):
 
	def create_diagram(self, start, end):
 
		# initialize the diagram
 
		template = Svg()
 

	
 
		self._setMove(start)
 
		self._set_move(start)
 

	
 
		# draw current state
 
		for lineNumber, line in enumerate(self._game.board):
 
			for itemNumber, item in enumerate(line):
 
		for (line_number, line) in enumerate(self._game.board):
 
			for (item_number, item) in enumerate(line):
 
				if item != EMPTY:
 
					template.addStone(itemNumber, lineNumber, "b" if item==BLACK else "w")
 
					template.add_stone(item_number, line_number, "b" if item == BLACK else "w")
 

	
 
		# draw the moves
 
		for k in range(start, end):
 
			if k >= len(self._moves):
 
				break
 

	
 
			color, move = self._moves[k]
 
			if move == tuple():
 
				template.overlays.append((k, "pass")) # !!
 
				template.overlays.append((k, "pass"))  # !!
 
				continue
 
			else:
 
				(c, r) = move
 

	
 
			if not self._move(color, c, r):
 
				if cfg.keepBroken:
 
				if cfg.keep_broken:
 
					continue
 
				else:
 
					return False
 

	
 
			# draw the move
 
			template.addMove(c, r, color, k+1)
 
			template.add_move(c, r, color, k + 1)
 

	
 
		return template
 

	
 
	def fetchGameInfo(self, fieldNames, default=None):
 
		return {k: self._record.get(k, default) for k in fieldNames}
 
	def fetch_game_info(self, field_names, default=None):
 
		return {k: self._record.get(k, default) for k in field_names}
 

	
 
	def _setMove(self, k):
 
	def _set_move(self, k):
 
		self._game = go.Go()
 

	
 
		blackStones = self._record.root.getProp("AB")
 
		whiteStones = self._record.root.getProp("AW")
 
		if blackStones:
 
			for p in blackStones:
 
		black_stones = self._record.root.get_prop("AB")
 
		white_stones = self._record.root.get_prop("AW")
 
		if black_stones:
 
			for p in black_stones:
 
				self._game.board[p.r][p.c] = BLACK
 
		if whiteStones:
 
			for p in whiteStones:
 
		if white_stones:
 
			for p in white_stones:
 
				self._game.board[p.r][p.c] = WHITE
 

	
 
		for i in range(k):
 
			(color, move) = self._moves[i]
 
			if move == tuple():
 
				continue # pass
 
			self._move(color, *move)
 

	
 
	def _move(self, color, c, r):
 
		if not self._game.move(BLACK if color=='b' else WHITE, c, r):
 
			# !! we do not honor http://red-bean.com/sgf/ff5/m_vs_ax.htm at the moment
 
			msg = "illegal move: {0} at {1},{2}".format(self._game.moveCount+1, c, r)
 
			if cfg.keepBroken:
 
			msg = "illegal move: {0} at {1},{2}".format(self._game.move_count + 1, c, r)
 
			if cfg.keep_broken:
 
				print(msg)
 
			else:
 
				msg += ". aborted"
 
				print(msg)
 
				return False
 
		return True
 

	
 

	
 
def main():
 
	cfg.parseArgs()
 
	cfg.parse_args()
 
	print("processing:")
 
	files = cfg.inputFiles[:]
 
	files = cfg.input_files[:]
 

	
 
	for item in files:
 
		if os.path.isfile(item):
 
			try:
 
				f = SourceFile(item)
 
				f.process()
 
			except ParserError as e:
 
				print("Couldn't parse {0}, following error occured: {1}".format(item, e))
 
		elif os.path.isdir(item):
 
			files += [os.path.join(item,child) for child in os.listdir(item)]
 
			print("contents of the '{0}' directory added to the queue".format(item))
 
		else:
src/diana/drawer/base.py
Show inline comments
 
@@ -7,51 +7,51 @@ from jinja2 import Environment, FileSyst
 
class DiagramPoint:
 
	def __init__(self, x, y, color="", label=""):
 
		self.x = x
 
		self.y = y
 
		self.color = color
 
		self.label = label
 

	
 
	def __repr__(self):
 
		return 'DiagramPoint({0},{1},"{2}","{3}")'.format(self.x, self.y, self.color, self.label)
 

	
 

	
 
class Base:
 
	highNumbers = True
 
	high_numbers = True
 

	
 
	def __init__(self, start=0):
 
		self.overlays = []
 
		self._letter = "a"
 

	
 
		self._index = dict()
 
		self._indexGen = count(start)
 
		self._index_gen = count(start)
 

	
 
		curDir = os.path.dirname(__file__)
 
		templateDir = os.path.join(curDir, "..", "templ")
 
		self._env = Environment(loader=FileSystemLoader(templateDir))
 
		cur_dir = os.path.dirname(__file__)
 
		template_dir = os.path.join(cur_dir, "..", "templ")
 
		self._env = Environment(loader=FileSystemLoader(template_dir))
 
		self._env.trim_blocks = True
 
		self._env.lstrip_blocks = True
 

	
 
	def addStone(self, x, y, color):
 
	def add_stone(self, x, y, color):
 
		assert (x, y) not in self._index
 
		self._index[(x, y)] = (next(self._indexGen), DiagramPoint(x, y, color))
 
		self._index[(x, y)] = (next(self._index_gen), DiagramPoint(x, y, color))
 

	
 
	def addMove(self, x, y, color, label):
 
		if (not self.highNumbers) and isinstance(label, int) and label%100 != 0:
 
	def add_move(self, x, y, color, label):
 
		if (not self.high_numbers) and isinstance(label, int) and label%100 != 0:
 
			label %= 100
 

	
 
		if (x, y) not in self._index:
 
			self._index[(x, y)] = (next(self._indexGen), DiagramPoint(x, y, color, label))
 
			self._index[(x, y)] = (next(self._index_gen), DiagramPoint(x, y, color, label))
 
		else:
 
			(_, point) = self._index[(x, y)]
 
			if not point.label:
 
				point.label = self._letter
 
				self._letter = chr(ord(self._letter)+1)
 
			self.overlays.append((label, point.label))
 

	
 
	def addLabel(self, x, y, label):
 
		self._index[(x, y)] = (next(self._indexGen), DiagramPoint(x, y, "", label))
 
	def add_label(self, x, y, label):
 
		self._index[(x, y)] = (next(self._index_gen), DiagramPoint(x, y, "", label))
 

	
 
	def save(self, filename):
 
		notes=open(filename+".txt", 'w')
 
		notes = open(filename+".txt", 'w')
 
		notes.write("\n".join("{0} = {1}".format(a, b) for (a, b) in self.overlays))
 
		notes.close()
src/diana/drawer/svg.py
Show inline comments
 
from .base import Base
 

	
 

	
 
def adjustFont(base,text):
 
def adjust_font(base, text):
 
	text = str(text)
 
	if len(text) < 2:
 
		return round(0.7*base)
 
	elif len(text) < 3:
 
		return round(0.55*base)
 
	else:
 
		return round(0.4*base)
 

	
 

	
 
class Svg(Base):
 
	extension = "svg"
 

	
 
	padding = 15
 
	highNumbers = True
 
	high_numbers = True
 
	
 
	def __init__(self, start=0):
 
		super().__init__(start)
 
		self.boardSize = 480
 
		self.padding = 30
 

	
 
	def render(self, templateName, bgcolor=""):
 
	def render(self, template_name, bgcolor=""):
 
		points = [p for (i, p) in sorted(self._index.values(), key=lambda x: x[0])]
 

	
 
		stones = [p for p in points if p.color and p.label == ""]
 
		moves = [p for p in points if p.color and p.label]
 
		labels = [p for p in points if not p.color and p.label]
 

	
 
		params = {
 
			"boardSize": self.boardSize, "padding": self.padding, "stones": stones, "moves": moves,
 
			"labels": labels, "adjustFont": adjustFont, "bgcolor": bgcolor}
 
			"labels": labels, "adjustFont": adjust_font, "bgcolor": bgcolor}
 

	
 
		return self._env.get_template(templateName).render(params)
 
		return self._env.get_template(template_name).render(params)
 

	
 
	def save(self, filename, template="templ.svg", bgcolor=""):
 
		file = open(filename+".svg", 'w')
 
		file.write(self.render(template, bgcolor))
 
		file.close()
 

	
 
		super().save(filename)
src/diana/drawer/tikz.py
Show inline comments
 
class Tikz:
 
	content = ""
 
	footer = ""
 
	extension = "tex"
 

	
 
	highNumbers = True
 
	high_numbers = True
 

	
 
	def __init__(self):
 
		self.content = r'''\begin{tikzpicture}
 
	\draw[step=\boardSquare,gray,very thin] (0,0) grid (18\boardSquare,18\boardSquare);
 
	\draw (0,0) rectangle (18\boardSquare,18\boardSquare);
 

	
 
	'''
 

	
 
		# stars
 
		for i in range(3):
 
			for j in range(3):
 
				self.content += r'''  \filldraw[fill=black] ({0}\boardSquare, {1}\boardSquare) circle[radius=0.04];'''.format(6*i+3, 6*j+3)+'\n'
 
			self.content += '\n'
 

	
 
		self.footer = r'\end{tikzpicture}' '\n'
 

	
 
	def __str__(self):
 
		return self.content+self.footer
 

	
 
	def drawStone(self, x, y, color):
 
	def draw_stone(self, x, y, color):
 
		fill = "black" if color == "b" else "white"
 
		self.content += r'  \filldraw[draw=black, fill={0}] ({1}\boardSquare, {2}\boardSquare) circle[radius=0.5\boardSquare];'.format(fill, x, 18-y)+'\n'
 

	
 
	def drawMove(self, x, y, label, color):
 
	def draw_move(self, x, y, label, color):
 
		fill = "black" if color == "b" else "white"
 
		labelColor = "white" if color == "b" else "black"
 
		if (not self.highNumbers) and isinstance(label, int) and label%100 != 0:
 
		label_color = "white" if color == "b" else "black"
 
		if (not self.high_numbers) and isinstance(label, int) and label%100 != 0:
 
			label = label%100
 

	
 
		self.content += r'  \filldraw[draw=black, fill={0}] ({1}\boardSquare, {2}\boardSquare) circle[radius=0.5\boardSquare] node[color={3}]{{{4}}};'.format(fill, x, 18-y, labelColor, label)+'\n'
 
		self.content += r'  \filldraw[draw=black, fill={0}] ({1}\boardSquare, {2}\boardSquare) circle[radius=0.5\boardSquare] node[color={3}]{{{4}}};'.format(fill, x, 18-y, label_color, label)+'\n'
 

	
 
	def getContent(self):
 
	def get_content(self):
 
		return self.content+self.footer
src/diana/go.py
Show inline comments
 
BLACK = 1
 
WHITE = -1
 
EMPTY = 0
 

	
 

	
 
class Go:
 
	board = [[EMPTY]*19 for i in range(19)]
 
	
 
	def __init__(self):
 
		self.board = [[EMPTY]*19 for i in range(19)]
 
		self.moveCount = 0
 
		self.move_count = 0
 
		self.temp = [[]]
 
	
 
	def move(self, color, y, x):
 
		if self.board[x][y] != EMPTY:
 
			return False
 

	
 
		self.board[x][y] = color
 

	
 
		for i, j in ((-1, 0), (1, 0), (0, -1), (0, 1)):
 
			self.temp = [[False]*19 for i in range(19)]
 
			if not self._floodFill(-color, x+i, y+j):
 
			if not self._flood_fill(-color, x + i, y + j):
 
				self._remove()
 
		self.temp = [[False]*19 for i in range(19)]
 
		if not self._floodFill(color, x, y):
 
		if not self._flood_fill(color, x, y):
 
			self.board[x][y] = EMPTY
 
			return False
 
		self.moveCount += 1
 
		self.move_count += 1
 
		return True
 

	
 
	def _floodFill(self, color, x, y):
 
	def _flood_fill(self, color, x, y):
 
		if x < 0 or x > 18 or y < 0 or y > 18:
 
			return False
 
		if self.temp[x][y]:
 
			return False
 
		if self.board[x][y] == EMPTY:
 
			return True
 
		if self.board[x][y] != color:
 
			return False
 
		self.temp[x][y] = True
 

	
 
		return self._floodFill(color, x-1, y) or \
 
			self._floodFill(color, x+1, y) or \
 
			self._floodFill(color, x, y-1) or \
 
			self._floodFill(color, x, y+1)
 
		return self._flood_fill(color, x - 1, y) or \
 
			self._flood_fill(color, x + 1, y) or \
 
			self._flood_fill(color, x, y - 1) or \
 
			self._flood_fill(color, x, y + 1)
 
	
 
	def _remove(self):
 
		for i in range(19):
 
			for j in range(19):
 
				if self.temp[i][j]:
 
					self.board[i][j] = EMPTY
src/diana/sgfParser/__init__.py
Show inline comments
 
def skipWhitespace(s, start):
 
def skip_whitespace(s, start):
 
	i = start
 
	while i < len(s) and s[i].isspace():
 
		i+=1
 
		i += 1
 

	
 
	return i
 

	
 

	
 
def strRowCol(s, i):
 
def str_row_col(s, i):
 
	k = 0
 
	(r, c) = (0, 0)
 
	for (r, line) in enumerate(s.splitlines(True)):
 
		c = i-k
 
		if k+len(line) > i:
 
			break
 
		else:
 
			k += len(line)
 
	return (r+1, c+1)
 

	
 

	
 
class ParserError(Exception):
 
	def __init__(self, msg, s, i):
 
		self.msg = msg
 
		(self.row, self.col) = strRowCol(s, i)
 
		(self.row, self.col) = str_row_col(s, i)
 
		self.context = s[i:i+16]
 

	
 
	def __str__(self):
 
		return "{0} at row {1}, col {2}, got '{3}...' instead".format(self.msg, self.row, self.col, self.context)
 

	
 

	
 
class ParserWarning(ParserError):
 
	pass
src/diana/sgfParser/collection.py
Show inline comments
 
from .node import Node
 
from . import skipWhitespace, ParserError
 
from . import skip_whitespace, ParserError
 
from .gameRecord import GameRecord
 

	
 

	
 
class Collection:
 
	def __init__(self, s):
 
		self.gameTrees = []
 
		i = skipWhitespace(s, 0)
 
		self.game_trees = []
 
		i = skip_whitespace(s, 0)
 
		if i >= len(s):
 
			return
 
		elif not GameTree.fits(s, i):
 
			raise ParserError("expected a GameTree starting with '('", s, i)
 
		while GameTree.fits(s, i):
 
			(i, x) = GameTree.create(s, i)
 
			self.gameTrees.append(x)
 
			self.game_trees.append(x)
 
		if i < len(s):
 
			raise ParserError("expected EOF", s, i)
 

	
 
	def listGames(self):
 
		for tree in self.gameTrees:
 
			for game in tree.listGames():
 
	def list_games(self):
 
		for tree in self.game_trees:
 
			for game in tree.list_games():
 
				yield game
 

	
 

	
 
class GameTree:
 
	def __init__(self):
 
		self.nodes = []
 
		self.branches = []
 

	
 
	@staticmethod
 
	def fits(s, i):
 
		return i < len(s) and s[i] == "("
 

	
 
	@staticmethod
 
	def create(s, start):
 
		assert GameTree.fits(s, start)
 
		res = GameTree()
 

	
 
		i = skipWhitespace(s, start+1)
 
		i = skip_whitespace(s, start + 1)
 
		if not Node.fits(s, i):
 
			raise ParserError("expected a Node starting with ';'", s, i)
 

	
 
		y = None
 
		while Node.fits(s, i):
 
			(i, x) = Node.create(s, i)
 
			res.nodes.append(x)
 
			if y:
 
				y.addChild(x)
 
				y.add_child(x)
 
			x.parent = y
 
			y = x
 
			i = skipWhitespace(s, i)
 
			i = skip_whitespace(s, i)
 

	
 
		while GameTree.fits(s, i):
 
			(i, x) = GameTree.create(s, i)
 
			res.branches.append(x)
 
			subroot = x.getNode(0)
 
			subroot = x.get_node(0)
 
			subroot.parent = y
 
			if y:
 
				y.addChild(subroot)
 
			i = skipWhitespace(s, i)
 
				y.add_child(subroot)
 
			i = skip_whitespace(s, i)
 

	
 
		if i >= len(s) or s[i] != ")":
 
			raise ParserError("expected end of a GameTree marked by ')'", s, i)
 
		i = skipWhitespace(s, i+1)
 
		i = skip_whitespace(s, i + 1)
 
		return (i, res)
 

	
 
	## Expand multiple games into distinct GameTrees and yield each.
 
	def listGames(self):
 
	def list_games(self):
 
		if len(self.nodes) == 0:
 
			return None
 
		for node in self.nodes[0].listGINodes():
 
			yield GameRecord(self._buildSubtree(node))
 
		for node in self.nodes[0].list_gi_nodes():
 
			yield GameRecord(self._build_subtree(node))
 

	
 
	def getNode(self, i):
 
	def get_node(self, i):
 
		if 0 <= i < len(self.nodes):
 
			return self.nodes[i]
 
		return None
 

	
 
	## Create and return a new game tree containing the provided Node.
 
	#
 
	# Ancestor nodes are copied, descendants are moved from the seedNode.
 
	def _buildSubtree(self, seedNode):
 
		node = seedNode.copy()
 
		node.setChildren(seedNode.children)
 
		seedNode.children = []
 
	# Ancestor nodes are copied, descendants are moved from the seed_node.
 
	def _build_subtree(self, seed_node):
 
		node = seed_node.copy()
 
		node.set_children(seed_node.children)
 
		seed_node.children = []
 

	
 
		while node.parent:
 
			newNode = node.parent.copy()
 
			newNode.addChild(node)
 
			node = newNode
 
			new_node = node.parent.copy()
 
			new_node.add_child(node)
 
			node = new_node
 

	
 
		return node
src/diana/sgfParser/gameRecord.py
Show inline comments
 
from .node import Node
 

	
 

	
 
## Wrapper around a Node tree.
 
class GameRecord:
 
	def __init__(self, root=None):
 
		self.root = root or Node()
 
		self._gameInfoNode = next(root.listGINodes())
 
		self._game_info_node = next(root.list_gi_nodes())
 

	
 
	def export(self):
 
		return self.root.export()
 

	
 
	def set(self, name, value):
 
		self._gameInfoNode.setProp(name, value)
 
		self._game_info_node.set_prop(name, value)
 

	
 
	def get(self, name, default=None):
 
		return self._gameInfoNode.getProp(name, default)
 
		return self._game_info_node.get_prop(name, default)
src/diana/sgfParser/node.py
Show inline comments
 
from collections import deque
 
import logging as log
 

	
 
from . import skipWhitespace, ParserWarning
 
from . import skip_whitespace, ParserWarning
 
from .property import Property, GAME_INFO
 

	
 

	
 
class Node:
 
	def __init__(self):
 
		self.properties = dict()
 
		self.parent = None
 
		self.children = []
 

	
 
	@staticmethod
 
	def fits(s, i):
 
		return i < len(s) and s[i] == ";"
 

	
 
	@staticmethod
 
	def create(s, start):
 
		assert Node.fits(s, start)
 
		res = Node()
 

	
 
		i = skipWhitespace(s, start+1)
 
		i = skip_whitespace(s, start + 1)
 
		while Property.fits(s, i):
 
			(i, x) = Property.create(s, i)
 
			if x.name in res.properties:
 
				log.warning(ParserWarning('duplicate "{0}" property in a node. second value ignored'.format(x.name), s, i))
 
			else:
 
				res.properties[x.name] = x
 
			i = skipWhitespace(s, i)
 
			i = skip_whitespace(s, i)
 
		return (i, res)
 

	
 
	def listGINodes(self):
 
		if self.isGINode():
 
	def list_gi_nodes(self):
 
		if self.is_gi_node():
 
			yield self
 
		empty = not self.isGINode()
 
		empty = not self.is_gi_node()
 

	
 
		node = self
 
		while node.parent:
 
			node = node.parent
 
			if node.isGINode():
 
			if node.is_gi_node():
 
				empty = False
 
				yield node
 

	
 
		queue = deque(self.children)
 
		while len(queue) > 0:
 
			node = queue.popleft()
 
			if node.isGINode():
 
			if node.is_gi_node():
 
				empty = False
 
				yield node
 
			queue.extend(node.children)
 
		if empty:
 
			yield self  # always yield at least self, can work as GINode as well as any other
 

	
 
	def isGINode(self):
 
	def is_gi_node(self):
 
		return any(prop.type == GAME_INFO for prop in self.properties.values())
 

	
 
	def setProp(self, name, value):
 
	def set_prop(self, name, value):
 
		self.properties[name] = value
 
		# check value type
 

	
 
	def setChildren(self, children):
 
	def set_children(self, children):
 
		self.children = children
 
		for child in children:
 
			child.parent = self
 

	
 
	def addChild(self, node):
 
	def add_child(self, node):
 
		if node in self.children:
 
			return node
 
		node.parent = self
 
		self.children.append(node)
 
		return node
 

	
 
	def removeChild(self, node):
 
	def remove_child(self, node):
 
		if node not in self.children:
 
			return None
 
		del self.children[self.children.index(node)]
 
		node.parent = None
 
		return node
 

	
 
	def removeChildAt(self, i):
 
	def remove_child_at(self, i):
 
		if -len(self.children) < i < len(self.children):
 
			res = self.children[i]
 
			del self.children[i]
 
			res.parent = None
 
			return res
 
		return None
 

	
 
	## Create a copy of the Node, with the same parent and deep copied properties, no copied children.
 
	def copy(self):
 
		res = Node()
 
		res.properties = {k: v.copy() for (k, v) in self.properties.items()}
 
		res.parent = self.parent
 
		return res
 

	
 
	def getProp(self, name, default=None):
 
	def get_prop(self, name, default=None):
 
		if name in self.properties:
 
			return self.properties[name].value
 
		else:
 
			return default
 

	
 
	## Returns textual representation of the Node itself, but disregards its children.
 
	def __str__(self):
 
		return ";" + "".join(str(p) for p in self.properties.values())
 

	
 
	def export(self):
 
		# there is a beautiful recursive solution, which this stack is too narrow to contain
 
		stack = [(self, 1, 1)]
 
		output = []
 

	
 
		while len(stack) > 0:
 
			(node, left, right) = stack.pop()
 
			if left > 0:
 
				output.append("("*left)
 
			output.append(str(node)+"\n")
 

	
 
			childCount = len(node.children)
 
			if childCount == 0:  # a leaf
 
			child_count = len(node.children)
 
			if child_count == 0:  # a leaf
 
				output.append(")"*right+"\n")
 
			elif childCount == 1:  # a line
 
			elif child_count == 1:  # a line
 
				stack.append((node.children[0], 0, right))
 
			else:  # a branching node
 
				# first child pops first, last child closes parent's parentheses
 
				children = zip(node.children, [1]*childCount, [1]*(childCount-1)+[1+right])
 
				children = zip(node.children, [1]*child_count, [1]*(child_count-1)+[1+right])
 
				stack.extend(reversed(list(children)))
 

	
 
		return "".join(output)
src/diana/sgfParser/propValues.py
Show inline comments
 
import re
 

	
 
from . import ParserError, skipWhitespace
 
from . import ParserError, skip_whitespace
 

	
 

	
 
class Regexp:
 
	number = re.compile(r"(\+|-|)\d+")
 
	real = re.compile(r"(\+|-|)\d+(\.\d+)?")
 
	point = re.compile(r"[a-zA-Z]{2}|")
 
	text = re.compile(r"(?:.*?[^\\])??(?:\\\\)*(?=])", re.DOTALL)
 
	composedText = re.compile(r"(?:.*?[^\\])??(?:\\\\)*(?=]|:)", re.DOTALL)
 
	composed_text = re.compile(r"(?:.*?[^\\])??(?:\\\\)*(?=]|:)", re.DOTALL)
 

	
 
	class Text:
 
		softBreaks = re.compile(r"(^|[^\\])((\\\\)*)\\((\n\r)|(\r\n)|\r|\n)")
 
		soft_breaks = re.compile(r"(^|[^\\])((\\\\)*)\\((\n\r)|(\r\n)|\r|\n)")
 
		whitespace = re.compile(r"[\t\f\v]")
 
		simpleWhitespace = re.compile(r"[\t\f\v\n\r]")
 
		removeSlashes = re.compile(r"(^|[^\\])((\\\\)*)\\($|[^\\])")
 
		unescapeSlashes = re.compile(r"\\\\")
 
		simple_whitespace = re.compile(r"[\t\f\v\n\r]")
 
		remove_slashes = re.compile(r"(^|[^\\])((\\\\)*)\\($|[^\\])")
 
		unescape_slashes = re.compile(r"\\\\")
 

	
 

	
 
class Composed:
 
	def __init__(self, a=None, b=None):
 
		self.a = a
 
		self.b = b
 

	
 
	def __str__(self):
 
		return "{0}:{1}".format(self.a, self.b)
 

	
 

	
 
class Point:
 
@@ -35,80 +35,80 @@ class Point:
 
	def __iter__(self):
 
		yield self.c
 
		yield self.r
 

	
 
	def __str__(self):
 
		a = ord("a")
 
		return chr(a+self.c) + chr(a+self.r)
 

	
 

	
 
## Metatype matching one of the provided types.
 
#
 
# Returns the first match, so the order is important.
 
def choose(*vTypes):
 
def choose(*v_types):
 
	def f(s, start):
 
		for vType in vTypes:
 
		for vType in v_types:
 
			try:
 
				(i, x) = vType(s, start)
 
				return (i, x)
 
			except ParserError:
 
				pass
 
		raise ParserError("no variant of a 'choose' property value matched", s, start)
 
	return f
 

	
 

	
 
def singletonFits(s, i):
 
def singleton_fits(s, i):
 
	return i < len(s) and s[i] == "["
 

	
 

	
 
def singletonEnds(s, i):
 
def singleton_ends(s, i):
 
	return i < len(s) and s[i] == "]"
 

	
 

	
 
def singleton(vType):
 
def singleton(v_type):
 
	def f(s, start):
 
		if not singletonFits(s, start):
 
		if not singleton_fits(s, start):
 
			raise ParserError("expected a property value starting with '['", s, start)
 
		(i, x) = vType(s, start+1)
 
		if not singletonEnds(s, i):
 
		(i, x) = v_type(s, start + 1)
 
		if not singleton_ends(s, i):
 
			raise ParserError("expected a property value ending with ']'", s, i)
 
		i = skipWhitespace(s, i+1)
 
		i = skip_whitespace(s, i + 1)
 
		return (i, x)
 
	return f
 

	
 

	
 
def listOf(vType, allowEmpty=False):
 
def list_of(v_type, allow_empty=False):
 
	def f(s, start):
 
		i = start
 
		if not singletonFits(s, i):
 
		if not singleton_fits(s, i):
 
			raise ParserError("expected a property value starting with '['", s, i)
 
		if singletonEnds(s, i+1) and allowEmpty:
 
			i = skipWhitespace(s, i+2)
 
		if singleton_ends(s, i + 1) and allow_empty:
 
			i = skip_whitespace(s, i + 2)
 
			return (i, [])
 
		single = singleton(vType)
 
		single = singleton(v_type)
 
		(i, x) = single(s, i)
 
		res = [x]
 
		while singletonFits(s, i):
 
		while singleton_fits(s, i):
 
			(i, x) = single(s, i)
 
			res.append(x)
 
		return (i, res)
 
	return f
 

	
 

	
 
def compose(vTypeA, vTypeB):
 
def compose(v_type_a, v_type_b):
 
	def f(s, start):
 
		(i, a) = vTypeA(s, start)
 
		(i, a) = v_type_a(s, start)
 
		if i >= len(s) or s[i] != ":":
 
			raise ParserError("expected a composed property value separated by ':'", s, i)
 
		(i, b) = vTypeB(s, i+1)
 
		(i, b) = v_type_b(s, i + 1)
 
		return (i, Composed(a, b))
 
	return f
 

	
 

	
 
def number(s, start):
 
	m = Regexp.number.match(s, start)
 
	if m is None:
 
		raise ParserError("expected a number matching '{0}'".format(Regexp.number.pattern), s, start)
 
	res = int(m.group(0))
 
	return (m.end(), res)
 

	
 

	
 
@@ -128,33 +128,33 @@ def double(s, start):
 

	
 

	
 
def color(s,start):
 
	c = s[start]
 
	if c not in ("B", "W"):
 
		raise ParserError("expected a color value, either 'B' or 'W'", s, start)
 
	return (start+1, c)
 

	
 

	
 
def text(simple=True, composed=False):
 
	def f(s, start):
 
		regexps = Regexp.Text
 
		m = Regexp.composedText.match(s, start) if composed else Regexp.text.match(s, start)
 
		m = Regexp.composed_text.match(s, start) if composed else Regexp.text.match(s, start)
 
		res = m.group(0)
 
		res = regexps.softBreaks.sub(r"\1\2", res)  # remove soft line breaks
 
		res = regexps.soft_breaks.sub(r"\1\2", res)  # remove soft line breaks
 
		if simple:
 
			res = regexps.simpleWhitespace.sub(" ", res)  # convert whitespace to spaces, no escapes
 
			res = regexps.simple_whitespace.sub(" ", res)  # convert whitespace to spaces, no escapes
 
		else:
 
			res = regexps.whitespace.sub(" ", res)  # convert whitespace to spaces, no escapes
 
		res = regexps.removeSlashes.sub(r"\1\2\4", res)
 
		res = regexps.unescapeSlashes.sub(r"\\", res)  # unescape slashes
 
		res = regexps.remove_slashes.sub(r"\1\2\4", res)
 
		res = regexps.unescape_slashes.sub(r"\\", res)  # unescape slashes
 

	
 
		return (m.end(), res)
 
	return f
 

	
 

	
 
def empty(s, start):
 
	return (start, "")
 

	
 

	
 
def anything(s, start):
 
	esc = False
 
	i = start
src/diana/sgfParser/property.py
Show inline comments
 
import re
 
from datetime import date
 
import logging as log
 

	
 
from .propValues import choose, singleton, listOf, compose, number, real, double, color, text, empty, anything, point, move, stone
 
from . import skipWhitespace, ParserError
 
from .propValues import choose, singleton, list_of, compose, number, real, double, color, text, empty, anything, point, move, stone
 
from . import skip_whitespace, ParserError
 

	
 
GAME_INFO = 1
 
UNKNOWN = 99
 

	
 

	
 
class DateException(Exception):
 
	pass
 

	
 

	
 
class Property:
 
	identRegexp = re.compile(r"[A-Z]+")
 
	ident_regexp = re.compile(r"[A-Z]+")
 

	
 
	def __init__(self):
 
		self.name = ""
 
		self.value = ""
 

	
 
	@staticmethod
 
	def fits(s, i):
 
		return i < len(s) and s[i].isupper()
 

	
 
	@staticmethod
 
	def create(s, start):
 
		assert Property.fits(s, start)
 
		res = Property()
 
		(i, res.name) = Property.ident(s, start)
 
		i = skipWhitespace(s, i)
 
		i = skip_whitespace(s, i)
 
		try:
 
			(i, x) = Property.createValue(s, i, res.name)
 
			(i, x) = Property.create_value(s, i, res.name)
 
		except ParserError as e:  # a malformed value
 
			log.warning(e)
 
			(i, x) = choose(listOf(anything), singleton(anything))(s, i)
 
			(i, x) = choose(list_of(anything), singleton(anything))(s, i)
 
			res.name = "_"+res.name
 
		res.value = x
 
		if res.name == "DT":
 
			res = DateProperty(x)
 
		i = skipWhitespace(s, i)
 
		i = skip_whitespace(s, i)
 
		return (i, res)
 

	
 
	@staticmethod
 
	def ident(s, start):
 
		m = Property.identRegexp.match(s, start)
 
		m = Property.ident_regexp.match(s, start)
 
		if m is None:
 
			raise ParserError("expected a property identifier matching '[A-Z]+'", s, start)
 
		return (m.end(), m.group())
 

	
 
	@staticmethod
 
	def createValue(s, start, name):
 
	def create_value(s, start, name):
 
		if name in Property.patterns:
 
			return Property.patterns[name](s, start)
 
		else:
 
			log.info("unknown property %s at position %d", name, start)
 
			return choose(listOf(anything), singleton(anything))(s, start)
 
			return choose(list_of(anything), singleton(anything))(s, start)
 

	
 
	@property
 
	def type(self):
 
		gameInfo = {"AN", "BR", "BT", "CP", "DT", "EV", "GN", "GC", "ON", "OT", "PB", "PC", "PW", "RE", "RO", "RU", "SO", "TM", "US", "WR", "WT"}
 
		if self.name in gameInfo:
 
		game_info = {"AN", "BR", "BT", "CP", "DT", "EV", "GN", "GC", "ON", "OT", "PB", "PC", "PW", "RE", "RO", "RU", "SO", "TM", "US", "WR", "WT"}
 
		if self.name in game_info:
 
			return GAME_INFO
 
		else:
 
			return UNKNOWN
 

	
 
	def copy(self):
 
		res = Property()
 
		res.name = self.name
 
		res.value = self.value if not isinstance(self.value, list) else self.value[:]
 
		return res
 

	
 
	def __str__(self):
 
		name = self.name.lstrip("_")
 
		val = "[{0}]".format(self.value) \
 
			if not isinstance(self.value, list) \
 
			else "".join("[{0}]".format(x) for x in self.value)
 
		return "{0}{1}".format(name, val)
 

	
 
	patterns = {
 
		"B": singleton(move),
 
		"KO": singleton(empty),
 
		"MN": singleton(number),
 
		"W": singleton(move),
 
		"AB": listOf(stone),  #
 
		"AE": listOf(point),  #
 
		"AW": listOf(stone),  #
 
		"AB": list_of(stone),  #
 
		"AE": list_of(point),  #
 
		"AW": list_of(stone),  #
 
		"PL": singleton(color),
 
		"C": singleton(text(simple=False)),
 
		"DM": singleton(double),
 
		"GB": singleton(double),
 
		"GW": singleton(double),
 
		"HO": singleton(double),
 
		"N": singleton(text()),
 
		"UC": singleton(double),
 
		"V": singleton(real),
 
		"BM": singleton(double),
 
		"DO": singleton(empty),
 
		"IT": singleton(empty),
 
		"TE": singleton(double),
 
		"AR": listOf(compose(point, point)),  #
 
		"CR": listOf(point),  #
 
		"DD": listOf(point, allowEmpty=True),  #
 
		"LB": listOf(compose(point, text())),  #
 
		"LN": listOf(compose(point, point)),  #
 
		"MA": listOf(point),  #
 
		"SL": listOf(point),  #
 
		"SQ": listOf(point),  #
 
		"TR": listOf(point),  #
 
		"AR": list_of(compose(point, point)),  #
 
		"CR": list_of(point),  #
 
		"DD": list_of(point, allow_empty=True),  #
 
		"LB": list_of(compose(point, text())),  #
 
		"LN": list_of(compose(point, point)),  #
 
		"MA": list_of(point),  #
 
		"SL": list_of(point),  #
 
		"SQ": list_of(point),  #
 
		"TR": list_of(point),  #
 
		"AP": singleton(compose(text(composed=True), text())),  #
 
		"CA": singleton(text()),
 
		"FF": singleton(number),
 
		"GM": singleton(number),
 
		"ST": singleton(number),
 
		"SZ": choose(singleton(number), singleton(compose(number, number))),  #
 
		"AN": singleton(text()),
 
		"BR": singleton(text()),
 
		"BT": singleton(text()),
 
		"CP": singleton(text()),
 
		"DT": singleton(text()),
 
		"EV": singleton(text()),
 
@@ -132,66 +132,66 @@ class Property:
 
		"RU": singleton(text()),
 
		"SO": singleton(text()),
 
		"TM": singleton(real),
 
		"US": singleton(text()),
 
		"WR": singleton(text()),
 
		"WT": singleton(text()),
 
		"BL": singleton(real),
 
		"OB": singleton(number),
 
		"OW": singleton(number),
 
		"WL": singleton(real),
 
		"FG": choose(singleton(empty), singleton(compose(number, text()))),  #
 
		"PM": singleton(number),
 
		"VW": listOf(point, allowEmpty=True),  #
 
		"VW": list_of(point, allow_empty=True),  #
 

	
 
		# go specific
 
		"HA": singleton(number),
 
		"KM": singleton(real),
 
		"TB": listOf(point, allowEmpty=True),
 
		"TW": listOf(point, allowEmpty=True)
 
		"TB": list_of(point, allow_empty=True),
 
		"TW": list_of(point, allow_empty=True)
 
	}
 

	
 

	
 
class DateProperty(Property):
 
	def __init__(self, value):
 
		super().__init__()
 
		self.name = "DT"
 
		self.value = []
 
		self.rawValue = value
 
		self.raw_value = value
 
		self.parse(value)
 

	
 
	def parse(self, s):
 
		regexp = re.compile(r"\d{4}(-\d\d){0,2}(,(\d{4}(-\d\d){0,2}|\d\d(-\d\d)?))*")
 
		match = re.search(regexp, s)
 
		if not match:
 
			raise DateException('Could not parse a DT value: "{0}"'.format(s))
 
		substr = match.group(0)
 
		dateStrs = substr.split(",")
 
		date_strs = substr.split(",")
 
		dates = []
 
		prevFormat = None
 
		prev_format = None
 

	
 
		for s in dateStrs:
 
		for s in date_strs:
 
			try:
 
				(prevFormat, d) = DateProperty.parseSingle(s, prevFormat, dates[-1] if dates else None)
 
				(prev_format, d) = DateProperty.parse_single(s, prev_format, dates[-1] if dates else None)
 
			except ValueError:
 
				raise DateException('Could not parse a DT value: "{0}"'.format(s))
 
			dates.append(d)
 
		self.value = dates
 

	
 
	@staticmethod
 
	def parseSingle(dateStr, prevFormat, prev=None):
 
		tokens = dateStr.split("-")
 
	def parse_single(date_str, prev_format, prev=None):
 
		tokens = date_str.split("-")
 
		num_tokens = list(map(int, tokens))
 
		if len(tokens) == 3:
 
			return ("YMD", date(*num_tokens))
 
		elif len(tokens) == 2:
 
			if len(tokens[0]) == 4:
 
				return ("YM", date(*num_tokens, 1))
 
			else:
 
				return ("MD", date(prev.year, *num_tokens))
 
		else:
 
			if len(tokens[0]) == 4:
 
				return ("Y", date(*num_tokens, 1, 1))
 
			elif prevFormat in ("YM","M"):
 
			elif prev_format in ("YM", "M"):
 
				return ("M", date(prev.year, *num_tokens, 1))
 
			else:
 
				return ("D", date(prev.year, prev.month, *num_tokens))
src/diana/tests/testSgfParser.py
Show inline comments
 
from itertools import chain
 
from datetime import date
 
import unittest
 
from unittest import TestCase
 
import os
 

	
 
from ..sgfParser import strRowCol
 
from ..sgfParser import str_row_col
 
from ..sgfParser.collection import Collection
 
from ..sgfParser.property import Property, DateProperty, DateException
 
from ..sgfParser.propValues import text, compose
 

	
 

	
 
dataDir = os.path.join(os.path.dirname(__file__), "data")
 

	
 

	
 
class TestUtils(TestCase):
 
	def testTextPos(self):
 
		s = "abc\ndef\rgh\r\nij\n\rklmn"
 
		rc = [
 
			[1, 2, 3, 4],
 
			[1, 2, 3, 4],
 
			[1, 2, 3, 4],
 
			[1, 2, 3], [1],  # don't care about LFCR, we unicode now
 
			[1, 2, 3, 4]
 
		]
 
		res = chain((r+1, c) for (r, row) in enumerate(rc) for c in row)
 
		for (i, (r, c)) in zip(range(len(s)+1), res):
 
			self.assertEqual(strRowCol(s, i), (r, c))
 
			self.assertEqual(str_row_col(s, i), (r, c))
 

	
 

	
 
class TestProperty(TestCase):
 
	def testName(self):
 
		with self.assertRaises(AssertionError):
 
			Property.create("[99]", 0)
 
		with self.assertRaises(AssertionError):
 
			Property.create("99[99]", 0)
 

	
 
		(i, prop) = Property.create("MN[99]", 0)
 
		self.assertNotEqual((i, prop), (0, None))
 
		self.assertEqual((i, prop.name), (6, "MN"))
 
@@ -48,53 +48,53 @@ ghi]"""
 

	
 
		s = """[m\\no\\\tpqr\\]\\\\]"""
 
		self.assertEqual(text()(s, 1)[1], "mno pqr]\\")
 
		self.assertEqual(text(False)(s, 1)[1], "mno pqr]\\")
 

	
 
		s = """[abc:def]"""
 
		parsed = compose(text(composed=True), text(composed=True))(s, 1)
 
		self.assertEqual(str(parsed[1]), "abc:def")
 

	
 

	
 
class TestDateProperty(TestCase):
 
	def testSingle(self):
 
		self.assertEqual(DateProperty.parseSingle("2019", "Y")[1], date(2019, 1, 1))
 
		self.assertEqual(DateProperty.parseSingle("2019-06", "YM")[1], date(2019, 6, 1))
 
		self.assertEqual(DateProperty.parseSingle("2019-06-22", "YMD")[1], date(2019, 6, 22))
 
		self.assertEqual(DateProperty.parse_single("2019", "Y")[1], date(2019, 1, 1))
 
		self.assertEqual(DateProperty.parse_single("2019-06", "YM")[1], date(2019, 6, 1))
 
		self.assertEqual(DateProperty.parse_single("2019-06-22", "YMD")[1], date(2019, 6, 22))
 
		d = date(2019, 6, 21)
 
		self.assertEqual(DateProperty.parseSingle("22", "D", d)[1], date(2019, 6, 22))
 
		self.assertEqual(DateProperty.parseSingle("07-22", "MD", d)[1], date(2019, 7, 22))
 
		self.assertEqual(DateProperty.parseSingle("2020-07-22", "YMD", d)[1], date(2020, 7, 22))
 
		self.assertEqual(DateProperty.parse_single("22", "D", d)[1], date(2019, 6, 22))
 
		self.assertEqual(DateProperty.parse_single("07-22", "MD", d)[1], date(2019, 7, 22))
 
		self.assertEqual(DateProperty.parse_single("2020-07-22", "YMD", d)[1], date(2020, 7, 22))
 
		with self.assertRaises(ValueError):
 
			DateProperty.parseSingle("2019-31", "YMD")
 
			DateProperty.parse_single("2019-31", "YMD")
 

	
 
	def testParse(self):
 
		self.assertEqual(DateProperty("1996-05,06").value, [date(1996, 5, 1), date(1996, 6, 1)])
 
		self.assertEqual(DateProperty("1996-05-06,07,08").value, [date(1996, 5, 6), date(1996, 5, 7), date(1996, 5, 8)])
 
		self.assertEqual(DateProperty("1996,1997").value, [date(1996, 1, 1), date(1997, 1, 1)])
 
		self.assertEqual(DateProperty("1996-12-27,28,1997-01-03,04").value, [date(1996, 12, 27), date(1996, 12, 28), date(1997, 1, 3), date(1997, 1, 4)])
 
		self.assertEqual(DateProperty("1997-05-05,1997-05-06").value, [date(1997, 5, 5), date(1997, 5, 6)])
 
		self.assertEqual(DateProperty("Published on 1997-05-06").value, [date(1997, 5, 6)])
 
		with self.assertRaises(DateException):
 
			DateProperty("unknown")
 

	
 

	
 
class TestCollection(TestCase):
 
	def testSubtrees(self):
 
		c = Collection("""
 
(;B[aa]
 
	(;W[ab]PB[Some Black]PW[Some White];B[ac])
 
	(;W[bb]PB[Other Black]PW[Other White])
 
)""")
 
		games = list(c.listGames())
 
		games = list(c.list_games())
 

	
 
		self.assertEqual(len(games), 2)
 
		self.assertRegex(games[0].export(), r"^\(;B\[aa]\n;(PB\[Some Black]|PW\[Some White]|W\[ab]){3}\n;B\[ac]\n\)\n$")
 
		self.assertRegex(games[1].export(), r"^\(;B\[aa]\n;(PB\[Other Black]|PW\[Other White]|W\[bb]){3}\n\)\n$")
 

	
 
	def testEmptySgf(self):
 
		Collection("(;)")
 

	
 
	def testSimpleSgf(self):
 
		with open(os.path.join(dataDir, "simple.sgf")) as f:
 
			Collection(f.read())
 

	
0 comments (0 inline, 0 general)