Skip to content

Commit

Permalink
Add unit tests and integrate CI (#5)
Browse files Browse the repository at this point in the history
* Bunch of small fixes to types, tiny bugfix to get_moves

* Implement test suite for Game class

* Renaming of get_tag_value to shorter get_tag

* Add Travis CI support

* Replace unsupported f-string

* Remove Python 3.5 support

* Minor README update
  • Loading branch information
DaniruKun committed Nov 27, 2019
1 parent d9a0cdc commit 7e1e1c6
Show file tree
Hide file tree
Showing 11 changed files with 200 additions and 41 deletions.
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,9 @@ venv.bak/
.idea/modules.xml
.idea/pypgn.iml
.idea/vcs.xml
pypgn/test.pgn
.idea/.gitignore
.idea/dictionaries/
docs/make.bat
docs/source/_static/
docs/source/_templates/
target
9 changes: 9 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
language: python
python:
- "3.6"
- "3.7"
- "3.8"
install:
- pip install -r requirements.txt
script:
- make test
54 changes: 54 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
.PHONY: clean-pyc clean-build clean-test docs clean test lint help

help:
@echo "clean - remove all build, test, coverage and Python artifacts"
@echo "clean-build - remove build artifacts"
@echo "clean-pyc - remove Python file artifacts"
@echo "clean-test - remove test and coverage artifacts"
@echo "lint - check style with flake8"
@echo "test - run tests quickly with the default Python"
@echo "test-all - run tests on every Python version with tox"
@echo "release - package and upload a release"
@echo "dist - package"
@echo "install - install the package to the active Python's site-packages"

clean: clean-build clean-pyc clean-test

clean-build:
rm -fr build/
rm -fr dist/
rm -fr .eggs/
find . -name '*.egg-info' -exec rm -fr {} +
find . -name '*.egg' -exec rm -f {} +

clean-pyc:
find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rm -fr {} +

clean-test:
rm -f .coverage
rm -fr htmlcov/
rm -rf .pytest_cache

lint:
flake8 pypgn

test:
python -m pytest -v --maxfail=2

test-all:
test

release: clean
python setup.py sdist upload
python setup.py bdist_wheel upload

dist: clean
python setup.py sdist
python setup.py bdist_wheel
ls -l dist

install: clean
python setup.py install
35 changes: 29 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
[![PyPI version](https://badge.fury.io/py/pypgn.svg)](https://badge.fury.io/py/pypgn)
[![Python 3.5](https://img.shields.io/badge/python-3.5-blue.svg)](https://www.python.org/downloads/release/python-360/)
[![Python 3.6](https://img.shields.io/badge/python-3.6-blue.svg)](https://www.python.org/downloads/release/python-360/)
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/DaniruKun/pypgn.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/DaniruKun/pypgn/context:python)
[![Documentation Status](https://readthedocs.org/projects/pypgn/badge/?version=latest)](https://pypgn.readthedocs.io/en/latest/?badge=latest)
[![Build Status](https://travis-ci.org/DaniruKun/pypgn.svg?branch=master)](https://travis-ci.org/DaniruKun/pypgn)
# PyPGN
A pure Python 3 library to simplify parsing and manipulation of [PGN](http://portablegamenotation.com/FIDE.html) (Portable Game Notation) format files, which are often used for serializing games such as chess.

## Prerequisites

Python version `3.x` > `3.4` and `Pip`
Python version `3.x` > `3.6` and `Pip`

## Install

Expand All @@ -32,17 +33,39 @@ from pypgn.game import Game

chess_game = Game('test.pgn')

print(chess_game.get_tag_value('Event'))
print(chess_game.get_tag('Event'))
print(chess_game.get_result())
# Print opening ply for white
print(chess_game.get_ply(1, 'w'))
```

Output:
```shell script
>> Rated Blitz game
>> 0-1
>> e4
$ Rated Blitz game
$ 0-1
$ e4
```

## Contributing
Setup a virtual environment with `virtualenv`
```shell script
$ virtualenv venv
$ source venv/bin/activate
```

Install requirements
```shell script
$ make install
```

Run unit tests locally with `pytest`
```shell script
$ make test
```

Run `flake8` lint with
```shell script
$ make lint
```

## Authors
Expand Down
2 changes: 1 addition & 1 deletion pypgn/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from pypgn.game import Game
from pypgn.game import Game
51 changes: 27 additions & 24 deletions pypgn/game.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Mapping, List
from typing import List
from pypgn.game_utils import (
_get_tags, _get_pgn_list, _get_moves, Move)

Expand All @@ -15,7 +15,9 @@ class Game:
tags: Mapping
a map of the parsed PGN file game tags
moves: List[Move]
a list of Moves, with [0] - move number, [1] - white ply, [2] - black ply
a list of Moves, with [0] - move number,
[1] - white ply,
[2] - black ply
Methods
-------
Expand All @@ -25,7 +27,7 @@ class Game:
Returns a map of tags of the PGN file
get_moves()
Returns a list of moves of the movetext
get_tag_value(name: str)
get_tag(name: str)
Returns a value for a given key in the parsed PGN tags
get_move(index: int)
Returns a move list for a given move number
Expand All @@ -40,14 +42,15 @@ class Game:
get_move_range(start: int, end: int)
Returns a list of moves from given start to end index
"""

def __init__(self, file_path: str):
"""
:param file_path: path to pgn file
:type file_path: str
"""
self.pgn: list = _get_pgn_list(file_path)
self.tags: Mapping = _get_tags(self.pgn)
self.tags: dict = _get_tags(self.pgn)
self.moves: List[Move] = _get_moves(self.pgn)

def get_pgn_list(self) -> list:
Expand All @@ -58,23 +61,7 @@ def get_pgn_list(self) -> list:
"""
return self.pgn

def get_tags(self) -> Mapping:
"""Gets and returns a map of metadata tags of the PGN
:return: Map of PGN tags
:rtype: Mapping
"""
return self.tags

def get_moves(self) -> List[Move]:
"""Gets and returns a list of moves
:return: A list of Moves
:rtype: List[Move]
"""
return self.moves

def get_tag_value(self, name: str) -> str:
def get_tag(self, name: str) -> str:
"""Gets and returns a tag for a given key name
:param name: Key name
Expand All @@ -84,6 +71,14 @@ def get_tag_value(self, name: str) -> str:
"""
return self.tags[name]

def get_tags(self) -> dict:
"""Gets and returns a map of metadata tags of the PGN
:return: Map of PGN tags
:rtype: dict
"""
return self.tags

def get_move(self, index: int) -> Move:
"""Gets and returns a move of a certain number
Expand All @@ -94,6 +89,14 @@ def get_move(self, index: int) -> Move:
"""
return self.moves[index - 1]

def get_moves(self) -> List[Move]:
"""Gets and returns a list of moves
:return: A list of Moves
:rtype: List[Move]
"""
return self.moves

def get_ply(self, index: int, player: str) -> str:
"""Gets and returns a ply for a given move
Expand All @@ -120,14 +123,14 @@ def get_result(self) -> str:
:return: Result of the game
:rtype: str
"""
return self.get_tag_value('Result')
return self.get_tag('Result')

def get_date(self) -> str:
"""Gets and returns the date of the game
:return: Date of the game in format YYYY.MM.DD
"""
return self.get_tag_value('Date')
return self.get_tag('Date')

def get_move_range(self, start: int, end: int) -> List[Move]:
"""Gets and returns a range of moves
Expand All @@ -136,4 +139,4 @@ def get_move_range(self, start: int, end: int) -> List[Move]:
:param end: End index of moves to get
:return: List of moves in given range
"""
return self.moves[start:end]
return self.moves[start - 1:end]
4 changes: 2 additions & 2 deletions pypgn/game_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
from typing import Mapping, List, NewType, Union
from typing import List, NewType, Union

Move = NewType('Move', Union[str, List])

Expand All @@ -10,7 +10,7 @@ def _get_pgn_list(path: str) -> list:
return lines


def _get_tags(pgn: list) -> Mapping:
def _get_tags(pgn: list) -> dict:
tag_list = [tag for tag in pgn if re.search(r'^\[', tag)]
tag_dict: dict = {}
for tag in tag_list:
Expand Down
16 changes: 16 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
attrs==19.3.0
entrypoints==0.3
flake8==3.7.9
importlib-metadata==0.23
mccabe==0.6.1
more-itertools==7.2.0
packaging==19.2
pluggy==0.13.1
py==1.8.0
pycodestyle==2.5.0
pyflakes==2.1.1
pyparsing==2.4.5
pytest==5.3.0
six==1.13.0
wcwidth==0.1.7
zipp==0.6.0
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
name='pypgn',
long_description=long_description,
long_description_content_type='text/markdown',
version='0.3.2',
version='0.3.9',
packages=['pypgn'],
url='https://github.com/DaniruKun/pypgn',
license='MPL-2.0',
Expand Down
10 changes: 4 additions & 6 deletions pypgn/test.pgn → test/resources/test.pgn
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[Event "Rated Blitz game"]
[Site "https://lichess.org/wX8aYOzE"]
[Site "https://lichess.org/#"]
[Date "2019.11.06"]
[Round "-"]
[White "wolkexas"]
[Black "danpetrov"]
[White "player1"]
[Black "player2"]
[Result "0-1"]
[UTCDate "2019.11.06"]
[UTCTime "18:51:44"]
Expand All @@ -17,6 +17,4 @@
[Opening "Caro-Kann Defense"]
[Termination "Normal"]

1. e4 c6 2. Nf3 d5 3. exd5 cxd5 4. d4 Nc6 {Some opening.} 5. Nc3 e6 6. a3 g6 7. Bb5 Bd7 8. Bg5 f6 9. Bh4 g5 10. Bg3 Bd6 11. Bxd6 Nh6 12. h3 e5 13. dxe5 Nxe5 14. Bxd7+ Qxd7 15. Bxe5 Qe6 16. O-O O-O-O 17. Re1 Rhe8 18. Bg3 Qb6 19. Nxd5 Qc5 20. c4 Nf5 21. Rc1 Nxg3 22. Rxe8 Rxe8 23. Nxf6 Rd8 24. Qa4 Ne2+ 25. Kh2 Nxc1 26. b4 Qxf2 27. Ne5 Ne2 28. Nd5 Qg1# 0-1


1. e4 c6 2. Nf3 d5 3. exd5 cxd5 4. d4 Nc6 {Some opening.} 5. Nc3 e6 6. a3 g6 7. Bb5 Bd7 8. Bg5 f6 9. Bh4 g5 10. Bg3 Bd6 11. Bxd6 Nh6 12. h3 e5 13. dxe5 Nxe5 14. Bxd7+ Qxd7 15. Bxe5 Qe6 16. O-O O-O-O 17. Re1 Rhe8 18. Bg3 Qb6 19. Nxd5 Qc5 20. c4 Nf5 21. Rc1 Nxg3 22. Rxe8 Rxe8 23. Nxf6 Rd8 24. Qa4 Ne2+ 25. Kh2 Nxc1 26. b4 Qxf2 27. Ne5 Ne2 28. Nd5 Qg1# 0-1
51 changes: 51 additions & 0 deletions test/test_game.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import pytest
import os


@pytest.fixture(scope="module")
def game_obj():
from pypgn.game import Game
from pathlib import Path
pgn_file_path = Path('test/resources') if 'test' not in os.getcwd() else Path('resources')

return Game(str(pgn_file_path / 'test.pgn'))


class TestGame:
def test_get_pgn_list(self, game_obj):
assert len(game_obj.get_pgn_list()) == 20

def test_get_tags(self, game_obj):
assert type(game_obj.get_tags()) == dict

def test_get_tag_value(self, game_obj):
assert game_obj.get_tag('Event') == "Rated Blitz game"
assert game_obj.get_tag('Site') == "https://lichess.org/#"
assert game_obj.get_tag('UTCDate') == "2019.11.06"

def test_get_move(self, game_obj):
assert game_obj.get_move(4) == ["4.", "d4", "Nc6"]

def test_get_moves(self, game_obj):
moves = game_obj.get_moves()
assert type(moves) == list
assert moves[0] == ["1.", "e4", "c6"]

def test_get_ply(self, game_obj):
for i in range(1, game_obj.get_move_count()):
move = game_obj.get_move(i)
assert move[1] == game_obj.get_ply(i, 'w') \
and move[2] == game_obj.get_ply(i, 'b'), \
"Non-matching ply pair at move %s !" % str(i)

def test_get_move_count(self, game_obj):
assert game_obj.get_move_count() == len(game_obj.get_moves())

def test_get_result(self, game_obj):
assert game_obj.get_result() == game_obj.get_tag('Result')

def test_get_date(self, game_obj):
assert game_obj.get_date() == game_obj.get_tag('UTCDate')

def test_get_move_range(self, game_obj):
assert game_obj.get_move_range(1, 28) == game_obj.get_moves()

0 comments on commit 7e1e1c6

Please sign in to comment.