Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
# R0401: Cyclic import
# C0103: Invalid constant name
# R0901: Too many ancestor
disable=I0011,W0142,W0511,R0903,R0904,R0401,C0103,R0901
# R0913: Too many arguments
disable=I0011,W0142,W0511,R0903,R0904,R0401,C0103,R0901,R0913

[REPORTS]

Expand Down
54 changes: 42 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,26 @@ ifndef TRAVIS
PYTHON_MINOR := 4
endif

# Testake settings
# Test settings
UNIT_TEST_COVERAGE := 82
INTEGRATION_TEST_COVERAGE := 82
INTEGRATION_TEST_COVERAGE := 49
COMBINED_TEST_COVERAGE := 82

# System paths
PLATFORM := $(shell python -c 'import sys; print(sys.platform)')
ifneq ($(findstring win32, $(PLATFORM)), )
WINDOWS := 1
SYS_PYTHON_DIR := C:\\Python$(PYTHON_MAJOR)$(PYTHON_MINOR)
SYS_PYTHON := $(SYS_PYTHON_DIR)\\python.exe
SYS_VIRTUALENV := $(SYS_PYTHON_DIR)\\Scripts\\virtualenv.exe
# https://bugs.launchpad.net/virtualenv/+bug/449537
export TCL_LIBRARY=$(SYS_PYTHON_DIR)\\tcl\\tcl8.5
else
ifneq ($(findstring darwin, $(PLATFORM)), )
MAC := 1
else
LINUX := 1
endif
SYS_PYTHON := python$(PYTHON_MAJOR)
ifdef PYTHON_MINOR
SYS_PYTHON := $(SYS_PYTHON).$(PYTHON_MINOR)
Expand Down Expand Up @@ -58,8 +65,10 @@ PYREVERSE := $(BIN)/pyreverse
NOSE := $(BIN)/nosetests
PYTEST := $(BIN)/py.test
COVERAGE := $(BIN)/coverage
SNIFFER := $(BIN)/sniffer

# Flags for PHONY targets
INSTALLED := $(ENV)/.installed
DEPENDS_CI := $(ENV)/.depends-ci
DEPENDS_DEV := $(ENV)/.depends-dev
ALL := $(ENV)/.all
Expand Down Expand Up @@ -102,10 +111,10 @@ launch-public: env
# Development Installation #####################################################

.PHONY: env
env: .virtualenv $(EGG_INFO)
$(EGG_INFO): Makefile setup.py requirements.txt
VIRTUAL_ENV=$(ENV) $(PYTHON) setup.py develop
touch $(EGG_INFO) # flag to indicate package is installed
env: .virtualenv $(INSTALLED)
$(INSTALLED): Makefile requirements.txt
VIRTUAL_ENV=$(ENV) $(PIP) install -r requirements.txt
touch $(INSTALLED) # flag to indicate package is installed

.PHONY: .virtualenv
.virtualenv: $(PIP)
Expand All @@ -125,7 +134,14 @@ $(DEPENDS_CI): Makefile
.PHONY: depends-dev
depends-dev: env Makefile $(DEPENDS_DEV)
$(DEPENDS_DEV): Makefile
$(PIP) install --upgrade pip pep8radius pygments docutils pdoc wheel
$(PIP) install --upgrade pip pep8radius pygments docutils pdoc wheel readme sniffer
ifdef WINDOWS
$(PIP) install --upgrade pywin32
else ifdef MAC
$(PIP) install --upgrade pync MacFSEvents
else ifdef LINUX
$(PIP) install --upgrade pyinotify
endif
touch $(DEPENDS_DEV) # flag to indicate dependencies are installed

# Documentation ################################################################
Expand All @@ -150,7 +166,7 @@ apidocs/$(PACKAGE)/index.html: $(SOURCES)
.PHONY: uml
uml: depends-dev docs/*.png
docs/*.png: $(SOURCES)
$(PYREVERSE) $(PACKAGE) -p $(PACKAGE) -f ALL -o png --ignore test
$(PYREVERSE) $(PACKAGE) -p $(PACKAGE) -a 1 -f ALL -o png --ignore test
- mv -f classes_$(PACKAGE).png docs/classes.png
- mv -f packages_$(PACKAGE).png docs/packages.png

Expand All @@ -175,7 +191,7 @@ pep8: depends-ci
pep257: depends-ci
# D102: docstring missing (checked by PyLint)
# D202: No blank lines allowed *after* function docstring
$(PEP257) $(PACKAGE) --ignore=D102,D202
$(PEP257) $(PACKAGE) --ignore=D100,D101,D102,D202

.PHONY: pylint
pylint: depends-ci
Expand All @@ -194,20 +210,34 @@ PYTEST_COV_OPTS := --cov=$(PACKAGE) --cov-report=term-missing --cov-report=html
PYTEST_CAPTURELOG_OPTS := --log-format="%(name)-25s %(lineno)4d %(levelname)8s: %(message)s"
PYTEST_OPTS := $(PYTEST_CORE_OPTS) $(PYTEST_COV_OPTS) $(PYTEST_CAPTURELOG_OPTS)

.PHONY: test-unit
test-unit: test
.PHONY: test
test: depends-ci .clean-test
$(PYTEST) $(PYTEST_OPTS) tests
$(PYTEST) $(PYTEST_OPTS) $(PACKAGE)
$(COVERAGE) report --fail-under=$(UNIT_TEST_COVERAGE) > /dev/null

.PHONY: tests
tests: depends-ci .clean-test
.PHONY: test-int
test-int: depends-ci .clean-test
TEST_INTEGRATION=1 $(PYTEST) $(PYTEST_OPTS) tests
$(COVERAGE) report --fail-under=$(INTEGRATION_TEST_COVERAGE) > /dev/null

.PHONY: test-all
test-all: tests
.PHONY: tests
tests: depends-ci .clean-test
TEST_INTEGRATION=1 $(PYTEST) $(PYTEST_OPTS) $(PACKAGE) tests
$(COVERAGE) report --fail-under=$(COMBINED_TEST_COVERAGE) > /dev/null

.PHONY: read-coverage
read-coverage:
$(OPEN) htmlcov/index.html

.PHONY: watch
watch: depends-dev
mkdir -p htmlcov && touch htmlcov/index.html && $(MAKE) read-coverage
$(SNIFFER)

# Cleanup ######################################################################

.PHONY: clean
Expand Down
2 changes: 1 addition & 1 deletion data/games/_started_.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
turn: 1
players:
- code: '1234'
color: red
Expand All @@ -10,3 +9,4 @@ players:
turns:
- done: false
moves: []
turn: 1
3 changes: 1 addition & 2 deletions gridcommand/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
exit("Python {}.{}+ is required.".format(*PYTHON_VERSION))

try:
from .views import app
from . import data
from .routes import app
except (ImportError, AttributeError): # pragma: no cover (manual test)
import logging
logging.exception("dependencies:")
16 changes: 0 additions & 16 deletions gridcommand/data.py

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Data models for the application."""
"""Domain models for the application."""

from .move import Move, Moves
from .turn import Turn, Turns
from .player import Player, Players
from .game import Game, Games
from .game import Game
44 changes: 6 additions & 38 deletions gridcommand/models/game.py → gridcommand/domain/game.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,13 @@
import string
import random

from flask import url_for # TODO: remove this import
import yorm

from .. import common
from .player import Players
from .turn import Turn

log = common.logger(__name__)


@yorm.attr(players=Players)
@yorm.attr(turn=yorm.converters.Integer)
@yorm.sync("data/games/{self.key}.yml")
class Game:

"""An individual game instance."""
Expand All @@ -31,6 +25,12 @@ def __init__(self, key=None):
def __repr__(self):
return "<game: {}>".format(self.key)

def __eq__(self, other):
return self.key == other.key

def __ne__(self, other):
return not self == other

@staticmethod
def _generate_key():
return ''.join(random.choice(Game.KEY_CHARS)
Expand Down Expand Up @@ -63,35 +63,3 @@ def advance(self):
if player.turns.current:
player.turns.current.done = True
player.turns.append(Turn())

def serialize(self):
kwargs = dict(_external=True, key=self.key)
game_url = url_for('.games_detail', **kwargs)
players_url = url_for('.players_list', **kwargs)
start_url = url_for('.games_start', **kwargs)
return {'uri': game_url,
'players': players_url,
'start': start_url,
'turn': self.turn}


class Games(dict):

"""A collection of all games in the application."""

def serialize(self):
return [url_for('.games_detail',
_external=True, key=key) for key in self]

def create(self):
game = Game()
self[game.key] = game
return game

def find(self, key, exc=ValueError):
try:
player = self[key]
except KeyError:
raise exc("The game '{}' does not exist.".format(key)) from None
else:
return player
19 changes: 2 additions & 17 deletions gridcommand/models/move.py → gridcommand/domain/move.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
"""Classes representing player's moves."""

from flask import url_for # TODO: remove this import
import yorm


@yorm.attr(begin=yorm.converters.Integer)
@yorm.attr(end=yorm.converters.Integer)
@yorm.attr(count=yorm.converters.Integer)
class Move(yorm.converters.AttributeDictionary):
class Move:

"""A planned transfer of tokens from one cell to another."""

Expand All @@ -32,23 +26,14 @@ def __lt__(self, other):
return False
return self.end < other.end

def serialize(self):
return {'count': self.count}


@yorm.attr(all=Move)
class Moves(yorm.converters.SortedList):
class Moves(list):

"""A collection of moves for a player."""

def __repr__(self):
return "<{} move{}>".format(len(self), "" if len(self) == 1 else "s")

def serialize(self, game, player):
return [url_for('.moves_detail', _external=True,
key=game.key, color=player.color, code=player.code,
begin=move.begin, end=move.end) for move in self]

def get(self, begin, end):
move = Move(begin, end)
for move2 in self:
Expand Down
32 changes: 5 additions & 27 deletions gridcommand/models/player.py → gridcommand/domain/player.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,13 @@
"""Classes representing players in a game."""

from flask import url_for # TODO: remove this import
import yorm

from ..common import logger
from .turn import Turns


log = logger(__name__)


@yorm.attr(color=yorm.converters.String)
@yorm.attr(code=yorm.converters.String)
@yorm.attr(turns=Turns)
class Player(yorm.converters.AttributeDictionary):
class Player:

"""An entity that plans moves during a turn."""

Expand All @@ -30,25 +24,13 @@ def __eq__(self, other):
return self.color == other.color

def authenticate(self, code, exc=ValueError):
if not code:
raise exc("Player code required.")
if code != self.code:
raise exc("The code '{}' is invalid.".format(code))

def serialize(self, game, auth=False):
data = {'turn': len(self.turns)}
kwargs = dict(_external=True, key=game.key, color=self.color)
if auth:
kwargs.update(code=self.code)
player_url = url_for('.players_detail', **kwargs)
turns_url = url_for('.turns_list', **kwargs)
data['turns'] = turns_url
else:
player_url = url_for('.players_detail', **kwargs)
data['uri'] = player_url
return data
raise exc("Player code '{}' is invalid.".format(code))


@yorm.attr(all=Player)
class Players(yorm.converters.List):
class Players(list):

"""A collection players in a game."""

Expand All @@ -70,10 +52,6 @@ def __repr__(self):
def maximum(self):
return len(self.COLORS)

def serialize(self, game):
return [url_for('.players_detail', _external=True,
key=game.key, color=player.color) for player in self]

def create(self, code='', exc=RuntimeError):
log.info("creating player with code %r...", code)
colors = [player.color for player in self]
Expand Down
38 changes: 38 additions & 0 deletions gridcommand/domain/turn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Classes representing turns in a game."""

from .move import Moves


class Turn:

"""An individual turn for a player."""

def __init__(self):
super().__init__()
self.moves = Moves()
self.done = False

def __repr__(self):
return "<turn>"


class Turns(list):

"""A list of turns in a game for each player."""

def __repr__(self):
return "<{} turn{}>".format(len(self), "" if len(self) == 1 else "s")

@property
def current(self):
"""Get the most recent turn."""
try:
return self[-1]
except IndexError:
return None

def find(self, number, exc=ValueError):
try:
return self[number - 1]
except IndexError:
raise exc("The turn '{}' does not exist.".format(number))
Loading