diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..0fcfff8 --- /dev/null +++ b/.flake8 @@ -0,0 +1,15 @@ +[flake8] +exclude = + .git + .github + .venv + __pycache__ + .eggs + *.egg + *.egg-info + build + dist + venv +ignore = D200, D204, D205, D400, D401 +max-line-length = 100 +max-complexity = 10 \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml index c9d864a..977de80 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,7 +7,7 @@ build: sphinx: configuration: docs/source/conf.py - + formats: - pdf diff --git a/docs/source/conf.py b/docs/source/conf.py index 1baf757..a18d0fb 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -30,11 +30,12 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'autoapi.extension' + 'autoapi.extension', + 'sphinx.ext.autosectionlabel', ] autoapi_dirs = ['../../notation'] - +autosectionlabel_prefix_document = True # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/source/index.rst b/docs/source/index.rst index e98ec6b..e7515f8 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,4 +1,4 @@ -.. Whist-Core documentation master file, created by +.. Notations documentation master file, created by sphinx-quickstart on Mon May 10 14:28:49 2021. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. @@ -12,6 +12,9 @@ Welcome to Notation's documentation! autoapi/notation/index.rst + +:ref:`standard:Card Game Notation Standard` + Indices and tables ================== diff --git a/docs/source/standard.rst b/docs/source/standard.rst new file mode 100644 index 0000000..29bb918 --- /dev/null +++ b/docs/source/standard.rst @@ -0,0 +1,55 @@ +Card Game Notation Standard +=========================== +This standard uses TOML_ as notation language. + +Header +------- +All meta data is defined in the Header. +This contains information about teams and players. +Furthermore, the location and datetime is stated. + +.. code-block:: toml + + [header] + # Timestamp of the starting time in RFC 3339 + timestamp = 2022-08-06T11:22:00 + # Location of the game or the server url + location = "Hamburg" + # Unsigned Integer + number_of_teams = 2 + [[players]] + # Unsigned Integer: ID of team starting with 0 + team = 0 + name = "John" + [[players]] + team = 1 + Name = "Lucy" + +Stack +----- +The data contains one mandatory field ``stack`` that is a list of strings representing a card. +Furthermore it has an optional field ``hands`` that is a table of the players' cards in hand. +One card can be represented with + +.. code-block:: toml + + suit = "[♦♣♥♠]" + rank = "[2-10JQKA]" + +.. code-block:: toml + + + [[stack]] + suit="heart" + rank="A" + [[stack]] + suit="heart" + rank="K" + + [[hands.player_1]] + [[hands.player_1.card]] + suit="heart" + rank="2" + + +.. _TOML: https://toml.io/en/ \ No newline at end of file diff --git a/notation/card.py b/notation/card.py new file mode 100644 index 0000000..f2a8077 --- /dev/null +++ b/notation/card.py @@ -0,0 +1,70 @@ +"""Python implementation of a general card and specific versions.""" +import tomlkit + +FRENCH_SUITS = ('♦', '♣', '♥', '♠') + +FRENCH_RANKS = ('A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K') + + +class Card: + """General representation of a card.""" + + def __init__(self, suit: str, rank: str) -> None: + """ + Construct a card. + + :param suit: The suit of the card. + :param rank: The rank of the card. + """ + self._suit: str = self._valid_suit(suit) + self._rank: str = self._valid_rank(rank) + + @staticmethod + def _valid_suit(suit: str) -> str: + if not isinstance(suit, str): + raise ValueError('Suit must be string.') + if not suit: + raise ValueError('Suit cannot be empty.') + return suit + + @staticmethod + def _valid_rank(rank: str) -> str: + if not isinstance(rank, str): + raise ValueError('Rank must be string.') + if not rank: + raise ValueError('Rank must not be empty.') + return rank + + @property + def suit(self) -> str: + """Returns the suit of a card as string.""" + return self._suit + + @property + def rank(self) -> str: + """Returns the rank of a card as string.""" + return self._rank + + def dumps(self) -> str: + """Return the card as TOML string.""" + return tomlkit.dumps(self.dict()) + + def dict(self): + """Return the card as dictionary.""" + return {'suit': self._suit, 'rank': self._rank} + + +class FrenchCard(Card): + """Represents a French card.""" + + @staticmethod + def _valid_suit(suit: str) -> str: + if suit not in FRENCH_SUITS: + raise ValueError('Not a correct french suit.') + return suit + + @staticmethod + def _valid_rank(rank: str) -> str: + if rank not in FRENCH_RANKS: + raise ValueError('Not a correct french rank.') + return rank diff --git a/notation/header.py b/notation/header.py new file mode 100644 index 0000000..c2c6c5b --- /dev/null +++ b/notation/header.py @@ -0,0 +1,39 @@ +"""Header containing the meta data of a game.""" +from datetime import datetime +from typing import Optional + +import tomlkit + +from notation.player import Player + + +# pylint: disable=too-few-public-methods +class Header: + """Class implementation of a meta data wrapper.""" + + def __init__(self, start_time: datetime, players: list[Player], + number_teams: int, location: Optional[str] = None, ): + """Constructor. + :param start_time: The start time of the game as datetime object + :param players: The list of players. + :param number_teams: The number of teams. + :param location: Optional. Where the game to place. + """ + self._start_time: datetime = start_time + self._location = location + if players is None or len(players) == 0: + raise ValueError('Player list cannot be empty') + if number_teams > len(players): + raise ValueError('Number teams cannot be greater than amount of players') + self._players = players + self._number_teams = number_teams + + def dumps(self) -> str: + """Return a TOML string representation of the header.""" + players = [tomlkit.dumps({'players': [player.dict()]}) for player in self._players] + header_dump = {'header': {'timestamp': self._start_time.isoformat(), + 'number_of_teams': self._number_teams}} + if self._location: + header_dump['header']['location'] = self._location + player_string = '\n'.join(players) + return f'{tomlkit.dumps(header_dump)}\n{player_string}' diff --git a/notation/player.py b/notation/player.py new file mode 100644 index 0000000..20548d4 --- /dev/null +++ b/notation/player.py @@ -0,0 +1,20 @@ +"""Wrapper for player data.""" + + +# pylint: disable=too-few-public-methods +class Player: + """Class containing player data.""" + + def __init__(self, name: str, team: int, seat: int): + """Constructs + :param name: name of player + :param team: integer of the team ID + :param seat: The unique identifier of place at table for that player. + """ + self._name = name + self._team = team + self._seat = seat + + def dict(self) -> dict[str, str]: + """Return player data as dictionary.""" + return {'name': self._name, 'team': self._team, 'seat': self._seat} diff --git a/notation/stack.py b/notation/stack.py new file mode 100644 index 0000000..ada6510 --- /dev/null +++ b/notation/stack.py @@ -0,0 +1,24 @@ +"""Implementation of a stack of cards.""" +from typing import List + +import tomlkit + +from notation.card import Card + + +# pylint: disable=too-few-public-methods +class Stack: + """A list of cards.""" + + def __init__(self, cards: List[Card]): + """ + Construct a stack of cards. + + :param cards: A list of cards. + """ + self._card_list: List[Card] = cards + + def dumps(self) -> str: + """Return the stack as TOML string.""" + card_dump = [card.dict() for card in self._card_list] + return tomlkit.dumps({'stack': card_dump}) diff --git a/poetry.lock b/poetry.lock index 3d5aeb9..10a56b6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,18 +2,37 @@ [[package]] name = "astroid" -version = "3.3.4" +version = "3.3.5" description = "An abstract syntax tree for Python with inference support." optional = false python-versions = ">=3.9.0" files = [ - {file = "astroid-3.3.4-py3-none-any.whl", hash = "sha256:5eba185467253501b62a9f113c263524b4f5d55e1b30456370eed4cdbd6438fd"}, - {file = "astroid-3.3.4.tar.gz", hash = "sha256:e73d0b62dd680a7c07cb2cd0ce3c22570b044dd01bd994bc3a2dd16c6cbba162"}, + {file = "astroid-3.3.5-py3-none-any.whl", hash = "sha256:a9d1c946ada25098d790e079ba2a1b112157278f3fb7e718ae6a9252f5835dc8"}, + {file = "astroid-3.3.5.tar.gz", hash = "sha256:5cfc40ae9f68311075d27ef68a4841bdc5cc7f6cf86671b49f00607d30188e2d"}, ] [package.dependencies] typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} +[[package]] +name = "attrs" +version = "24.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + [[package]] name = "colorama" version = "0.4.6" @@ -27,13 +46,13 @@ files = [ [[package]] name = "dill" -version = "0.3.8" +version = "0.3.9" description = "serialize all of Python" optional = false python-versions = ">=3.8" files = [ - {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, - {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, + {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, + {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, ] [package.extras] @@ -85,6 +104,39 @@ files = [ flake8 = ">=3" pydocstyle = ">=2.1" +[[package]] +name = "hypothesis" +version = "6.115.5" +description = "A library for property-based testing" +optional = false +python-versions = ">=3.9" +files = [ + {file = "hypothesis-6.115.5-py3-none-any.whl", hash = "sha256:b7733459ae9a93020fac3b91b41473c9b85e975139a152a70d88f3a5caa3fa3f"}, + {file = "hypothesis-6.115.5.tar.gz", hash = "sha256:4768c5fb426b305462ed31032d6e216a31daaefb1dc3134fdf2795b7961d7cb3"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +exceptiongroup = {version = ">=1.0.0", markers = "python_version < \"3.11\""} +sortedcontainers = ">=2.1.0,<3.0.0" + +[package.extras] +all = ["black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.74)", "django (>=4.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.16)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.19.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.2)"] +cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] +codemods = ["libcst (>=0.3.16)"] +crosshair = ["crosshair-tool (>=0.0.74)", "hypothesis-crosshair (>=0.0.16)"] +dateutil = ["python-dateutil (>=1.4)"] +django = ["django (>=4.2)"] +dpcontracts = ["dpcontracts (>=0.4)"] +ghostwriter = ["black (>=19.10b0)"] +lark = ["lark (>=0.10.1)"] +numpy = ["numpy (>=1.19.3)"] +pandas = ["pandas (>=1.1)"] +pytest = ["pytest (>=4.6)"] +pytz = ["pytz (>=2014.1)"] +redis = ["redis (>=3.0.0)"] +zoneinfo = ["tzdata (>=2024.2)"] + [[package]] name = "iniconfig" version = "2.0.0" @@ -134,19 +186,19 @@ files = [ [[package]] name = "platformdirs" -version = "4.2.2" +version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "pluggy" @@ -165,13 +217,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pycodestyle" -version = "2.12.0" +version = "2.12.1" description = "Python style guide checker" optional = false python-versions = ">=3.8" files = [ - {file = "pycodestyle-2.12.0-py2.py3-none-any.whl", hash = "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4"}, - {file = "pycodestyle-2.12.0.tar.gz", hash = "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c"}, + {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, + {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, ] [[package]] @@ -282,26 +334,37 @@ files = [ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +optional = false +python-versions = "*" +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + [[package]] name = "tomli" -version = "2.0.1" +version = "2.0.2" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"}, + {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"}, ] [[package]] name = "tomlkit" -version = "0.13.0" +version = "0.13.2" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" files = [ - {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, - {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] [[package]] @@ -318,4 +381,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "f6d9fe28934da9ee7176036d6ce3fc8a49e678076cc4b3a2d2f1ba54d33e6e89" +content-hash = "d9a4430acfbaac049f543a569480ee415c9059d058d120502612410041d0b3ef" diff --git a/pyproject.toml b/pyproject.toml index 2c6e4f4..b784153 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,13 +19,15 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.10" +tomlkit = "^0.13.2" -[tool.poetry.dev-dependencies] +[tool.poetry.group.dev.dependencies] pytest = "^8.3" pytest-asyncio = "^0.24.0" # .5 because that version is the first to have compat with pytest 8 flake8 = "^7.1" flake8-docstrings = "^1.7" pylint = "^3.3" +hypothesis = "^6.82.0" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/tests/composites.py b/tests/composites.py new file mode 100644 index 0000000..168e676 --- /dev/null +++ b/tests/composites.py @@ -0,0 +1,33 @@ +"""Build funtions for hypothesis tests.""" +import string + +from hypothesis.strategies import composite, text, characters, integers + +from notation.card import Card +from notation.player import Player + + +@composite +def build_card(draw): + """Build a card with random strings of size one for suit and rank.""" + suit: str = draw(text(min_size=1, max_size=1, + alphabet=characters(whitelist_categories=('L', 'N', 'S'), + blacklist_characters=('"', "'")))) + rank: str = draw(text(min_size=1, max_size=1, + alphabet=characters(whitelist_categories=('L', 'N', 'S'), + blacklist_characters=('"', "'")))) + return Card(suit=suit, rank=rank) + + +@composite +def build_player(draw, seat: int): + """Build a player with random strings of at least size one for the name.""" + name = draw(text(alphabet=string.ascii_letters, min_size=1)) + team = draw(integers()) + return Player(name, team, seat) + + +@composite +def build_player_list(draw): + amount_players = draw(integers(min_value=1, max_value=10)) + return [build_player(seat=seat) for seat in range(amount_players)] diff --git a/tests/test_card.py b/tests/test_card.py new file mode 100644 index 0000000..5f01fe2 --- /dev/null +++ b/tests/test_card.py @@ -0,0 +1,68 @@ +import pytest +import tomlkit +from hypothesis import given, assume +from hypothesis.strategies import text, characters + +from notation.card import Card, FrenchCard + + +@given(text(min_size=1), text(min_size=1)) +def test_init(suit, rank): + card = Card(suit, rank) + assert isinstance(card, Card) + + +@given(text()) +def test_init_empty_suit(rank): + with pytest.raises(ValueError): + _ = Card('', rank) + + +@given(text()) +def test_init_empty_rank(suit): + with pytest.raises(ValueError): + _ = Card(suit, '') + + +@given(text(min_size=1, alphabet=characters(whitelist_categories=('L', 'N', 'S'), + blacklist_characters=('"', "'"))), + text(min_size=1, alphabet=characters(whitelist_categories=('L', 'N', 'S'), + blacklist_characters=('"', "'")))) +def test_dumps(suit, rank): + card = Card(suit, rank) + toml_card = card.dumps() + expected_toml = f'''\ +suit = "{suit}" +rank = "{rank}" +''' + assume(tomlkit.loads(expected_toml)) + assert toml_card == expected_toml + + +@given(text(min_size=1, alphabet=characters(whitelist_categories=('L', 'N', 'S'), + blacklist_characters=('"', "'", '♦', '♣', '♥', '♠')))) +def test_init_french_invalid_suit(suit): + with pytest.raises(ValueError): + _ = FrenchCard(suit, 'A') + + +@given(text(min_size=1, alphabet=characters(whitelist_categories=('L', 'N', 'S'), + blacklist_characters=( + '"', "'", 'A', '2', '3', '4', '5', '6', '7', + '8', '9', 'J', 'Q', 'K')))) +def test_init_french_invalid_rank(rank): + with pytest.raises(ValueError): + _ = FrenchCard('♦', rank) + + +@pytest.mark.parametrize("suit", ['♦', '♣', '♥', '♠']) +@pytest.mark.parametrize("rank", ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']) +def test_french_dumps(suit, rank): + card = FrenchCard(suit, rank) + toml_card = card.dumps() + expected_toml = f'''\ +suit = "{suit}" +rank = "{rank}" +''' + assume(tomlkit.loads(expected_toml)) + assert toml_card == expected_toml diff --git a/tests/test_header.py b/tests/test_header.py new file mode 100644 index 0000000..8760162 --- /dev/null +++ b/tests/test_header.py @@ -0,0 +1,72 @@ +import string +from datetime import datetime + +import pytest +import tomlkit +from hypothesis import assume, example, given +from hypothesis.strategies import datetimes, lists, integers, composite, text, builds, uuids + +from notation.header import Header +from notation.player import Player +from tests.composites import build_player, build_player_list + + +@composite +def build_header_dumps(draw): + timestamp = draw(datetimes()) + amount_players = draw(integers(min_value=1, max_value=10)) + players = draw(lists(builds(Player, name=text(alphabet=string.ascii_letters, min_size=1), + team=integers(), seat=integers()), + min_size=1, + max_size=amount_players, + unique=True)) + teams = draw(integers(max_value=len(players))) + return (timestamp, players, teams) + + +@given(build_header_dumps()) +@example((datetime.now(), [Player(name='John', team=1, seat=0)], 1)) +@example((datetime.now(), [Player(name='John', team=1, seat=0), + Player(name='John', team=1, seat=1)], 1)) +def test_dumps(data): + start_time, players, number_teams = data + header = Header(start_time=start_time, players=players, number_teams=number_teams) + header_dump = header.dumps() + player_string = ((f'[[players]]\n' + f'name = "{player._name}"\n' + f'team = {player._team}\n' + f'seat = {player._seat}') for player in players) + expected_toml = (f'[header]\ntimestamp = "{start_time.isoformat()}"\nnumber_of_teams = ' + f'{number_teams}\n\n') + expected_toml += "\n\n".join(player_string) + '\n' + assume(tomlkit.loads(expected_toml)) + assert header_dump == expected_toml + + +@given(build_header_dumps(), text(alphabet=string.ascii_letters, min_size=1)) +@example((datetime.now(), [Player(name='John', team=1, seat=0)], 1), 'Europe') +def test_dumps_with_location(data, location): + start_time, players, number_teams = data + header = Header(start_time=start_time, players=players, number_teams=number_teams, + location=location) + header_dump = header.dumps() + player_string = ((f'[[players]]\n' + f'name = "{player._name}"\n' + f'team = {player._team}\n' + f'seat = {player._seat}') for player in players) + expected_toml = (f'[header]\ntimestamp = "{start_time.isoformat()}"\nnumber_of_teams = ' + f'{number_teams}\nlocation = "{location}"\n\n') + expected_toml += "\n\n".join(player_string) + '\n' + assume(tomlkit.loads(expected_toml)) + assert header_dump == expected_toml + + +def test_empty_player_list(): + with pytest.raises(ValueError): + Header(start_time=datetime.now(), players=[], number_teams=1) + + +def test_more_teams_than_players(): + with pytest.raises(ValueError): + Header(start_time=datetime.now(), players=[Player(name='John', team=1, seat=0)], + number_teams=2) diff --git a/tests/test_stack.py b/tests/test_stack.py new file mode 100644 index 0000000..4edd3d6 --- /dev/null +++ b/tests/test_stack.py @@ -0,0 +1,19 @@ +import tomlkit +from hypothesis import given, assume, example +from hypothesis.strategies import lists + +from notation.card import Card +from notation.stack import Stack +from tests.composites import build_card + + +@given(lists(build_card())) +@example([Card(suit="Heart", rank="King")]) +@example([Card(suit="Heart", rank="King"), Card(suit='♦', rank="K")]) +def test_dumps(cards): + stack = Stack(cards=cards) + toml_stack: str = stack.dumps() + stack = (f'[[stack]]\nsuit = "{card.suit}"\nrank = "{card.rank}"\n' for card in cards) + expected_toml = "\n".join(stack) + assume(tomlkit.loads(expected_toml)) + assert toml_stack == expected_toml