- Python Cards Training
This project models a playing card in Python.
This project demonstrates basic Python features, including:
- Setup - Virtual environments and pipenv *
- Classes
- Functions and magic methods
- Class attributes
- Importing modules
- Class methods
- Formatting and Linting *
- Docstrings
- Dataclasses
- Exceptions
- 3rd party packages
- Testing with pytest
- Using 3rd party packages
- Generators
- Debugging
- Typing and MyPy *
- Immutability *
(* Includes explanation slides)
These are the core files in the project
The Python core files are also full solutions to the assignment. You are very strongly discouraged to view these files if you are attempting the assignment.
The Python core files also have partial versions (e.g. card_0_classes.py) that demonstrate a step by step process of adding complexity that culminates in the core Python files of the project. These do not contain solutions to the assignment.
pip3 install black
pip3 install mypy
pip3 install pipenv
pip3 install flake8Install VS Code Python extension.
Add these to your VS Code settings.json.
"python.formatting.provider": "black",
"python.formatting.blackPath": "/usr/local/bin/black",
"python.linting.flake8Enabled": true,
"python.linting.flake8Path": "/usr/local/bin/flake8",
"python.linting.flake8Args": [
"--max-line-length=88"
],
"python.linting.mypyEnabled": true,
"python.linting.mypyPath": "/usr/local/bin/mypy",
"python.linting.pylintEnabled": false,
"python.testing.pyTestEnabled": true,
"python.testing.noseEnabled": false,
"python.testing.unittestEnabled": false,Introduce project. We want to make Python objects that represent playing cards so we can use them in a game.
This session should cover:
Create a new folder for the project. In this folder:
mkdir Python\ Cards
cd Python\ Cards
pipenv install
mkdir src-
Explain:
- Project structure
pipandpipenv
-
Use the training slides up to slide 6.
- Create:
- Folder for project
srcfolder
- Run:
- All of the VS Code Setup steps
- Create:
card_0- Create a basic class for a card
class Card:
def __init__(self, rank, suit):
self.rank = rank
self.suit = suit-
card_1 -
__repr__()
def __repr__(self):
return f"{self.rank} of {self.suit}"- Test with if name main
if __name__ == "__main__":
ace_of_spades = Card("Ace", "Spades")
print(ace_of_spades.rank)
print(ace_of_spades.suit)
print(ace_of_spades)-
card_2 -
Card ranks and suits (Share in chat)
RANKS = ("A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K")
SUITS = ("♣", "♦", "♥", "♠")
RANK_VALUES = {"A": 1, "J": 10, "Q": 10, "K": 10}
SUIT_SYMBOLS = {"Club": "♣", "Diamond": "♦", "Heart": "♥", "Spade": "♠"}- Can now implement
__add__and__int__
def __int__(self):
if self.rank in self.RANK_VALUES:
return self.RANK_VALUES[self.rank]
else:
return int(self.rank)def __add__(self, other):
return int(self) + int(other)def __radd__(self, other):
return self.__add__(other)-
card_2 -
Want to make random cards
-
Import random
from random import choice-
card_2 -
random()- Add random class method
@classmethod
def random(cls):
return cls(choice(cls.RANKS), choice(cls.SUITS))if __name__ == "__main__":
ace_of_spades = Card("A", "♠")
print(ace_of_spades)
print(Card.random())- Explain and demonstrate:
- PEP8
- Flake8
- Black
card_3- Add examples of docstrings. Show VS Code reading them.
"""A playing card. Can be added and converted to int based on rank.""""""Return a random playing card from available ranks and suits."""card_4- Demonstrate missing methods like
__eq__ - Change to data class
from dataclasses import dataclass
from random import choice
@dataclassrank: str
suit: str- Ordering
card_5
sort_index: int = field(init=False, repr=False) # This must come first for sorting.- Explain
__post_init__
def __post_init__(self):
self._add_sort_index_field()- Show private method with underscore
def _add_sort_index_field(self):
"""Add field `sort_index` for card order."""
rank_i = self.RANKS.index(self.rank)
suit_i = self.SUITS.index(self.suit)
self.sort_index = rank_i * len(self.SUITS) + suit_iif __name__ == "__main__":
ace_of_spades = Card("A", "♠")
print(ace_of_spades)
hand = [Card.random() for _ in range(10)]
print(hand)
print(sorted(hand))
shuffle(hand)
print(hand)card_6
def _add_sort_index_field(self):
"""Add field `sort_index` for card order and verifies valid rank and suit."""
try:
rank_i = self.RANKS.index(self.rank)
except ValueError:
print(f"Card rank ({self.rank}) not in {self.RANKS}\n")
raise
try:
suit_i = self.SUITS.index(self.suit)
except ValueError:
print(f"Card suit ({self.suit}) not in {self.SUITS}")
raise
self.sort_index = rank_i * len(self.SUITS) + suit_iprint(Card("17", "♥")) # Expect this to fail but with useful error- Explain:
pipenvpackages and dev packages
- Run:
pipenv install pillow
pipenv install --dev pytesttest_card_0- Create:
testsfoldertest_card.pyfile
from src.card import Carddef test_repr():
ace_of_spades = Card("A", "♠")
assert repr(ace_of_spades) == "A♠"-
Explain:
assert- pytest
-
Run in project directory:
pipenv run python -m pytest
-
test_card_1 -
Explain:
isinstance
-
test_card_2
def test_init_symbol():
card = Card("2", "♠")
assert isinstance(card, Card)- Explain:
- Fixtures
test_card_3
import pytest@pytest.fixture
def two_of_spades():
return Card("2", "♠")
def test_repr(two_of_spades):
assert repr(two_of_spades) == "2♠"- Explain:
pytest.raises()
def test_validation():
with pytest.raises(ValueError):
Card("52", "♣")
with pytest.raises(ValueError):
Card("2", "B")create_card_image_0
from PIL import Image, ImageDraw, ImageFont
from card import Card
def create_card_image(card):
image_width = 600
image_height = 900
light_grey = (240, 240, 240)
dark_grey = (30, 30, 20)
dark_red = (220, 30, 20)
suit_colour = dark_grey if card.suit in "♣♠" else dark_red
font = ImageFont.truetype("Apple Symbols.ttf", size=120)
card_image = Image.new("RGB", (image_width, image_height), color=light_grey)
draw = ImageDraw.Draw(card_image)
draw.text((60, 60), f"{card.rank}{card.suit}", fill=suit_colour, font=font)
return card_image
if __name__ == "__main__":
image = create_card_image(Card.random())
image.show()[Starting from a base of card_6_exceptions]
Between session one and two there are the following assignment tasks. These are fully implemented in the core files (e.g. src/card.py).
The _convert_suit() function will be added to __post_init__() and run before the _add_sort_index_field() function.
It will take the current suit, and if it is the "name" of a suit (e.g. 'Spade' or 'Spades') convert it to the symbol (e.g. '♠').
It will then update the value of self.suit to the symbol.
The all() function should return all possible valid cards for the class. This should be every combination of rank and suit. all() should be a classmethod, and callable as Card.all().
Return a list of all cards.
Return a generator for all cards.
[Starting from a base of test_card_3_raises]
At this stage of the Card class, some of these tests will fail. They are marked with #Fail.
Tests that depend on the assignment work are marked with #Assignment.
These tests should be added to your test_card.py file:
test_init_name()- Passes if a card instance can be created when the suit is given as a name (e.g. 'Spade').
test_init_name_plural()#Assignment- Passes if a card instance can be created when the suit is given as a plural name (e.g. 'Spades').
test_init_random()- Passes if a card instance can be created with the
Card.random()method.
- Passes if a card instance can be created with the
test_init_random_multiple()- Tests that
Card.random()outputs different cards each time.
- Tests that
- Fixture
hand()- Add a pytest fixture which is a list of at least 5 different cards.
- Fixtures for more cards
- Add at least two more fixtures for individual cards.
test_str()- Passes if card instances match their expected string representation.
- Test more than one card instance.
test_eq()- Passes if:
- A card is equal (
==) to itself. - A card is equal to another instance of the same card (same rank and suit).
- A card is not equal (
!=) to a different card.
- A card is equal (
- Passes if:
test_add()- Passes if all of these give a correct result:
Card+CardCard+intint+CardCard(with non numeric rank) +CardCard(with non numeric rank) +int
- Passes if all of these give a correct result:
test_sum()- Passes if the
sum()of the hand fixture is correct.
- Passes if the
test_ordering- Passes if:
- Less than operator works correctly.
- Greater than operator works correctly.
sorted(hand)gives a correctly sorted list of cards.
- Passes if:
test_hash()#Fail- Passes if cards can be hashed (
hash())
- Passes if cards can be hashed (
test_frozen#Fail- Passes if changing the rank or suit of a card results in a
dataclasses.FrozenInstanceErrorerror.
- Passes if changing the rank or suit of a card results in a
test_all#Assignment- Passes if:
Card.all()returns all of the possible cards.Card.all()is a generator.
- Passes if:
[Starting from a base of create_card_image_0]
This is an open ended tasks to improve the create_card_image() function to output better card images. You can chose to make your images as traditional or non-traditional as you want.
Some traditional things you may want to try adding:
- A border around your card.
- Probably with a margin.
- Your border could be simple, or multilayered and patterned.
- The symbol and rank flipped in the bottom right corner.
- To ensure the card's top left corner is visible in either orientation.
- Hint: If you make a change to an
Imageobject, you will need to make a newDrawobject to draw on the image correctly - I'm willing to provide a code snippet to help with image rotation
- Hint: If you make a change to an
- To ensure the card's top left corner is visible in either orientation.
- Symbols in the centre of the card.
- Matching the suit.
- With a count matching the rank.
- Center a large version of the symbol for aces.
- Have the symbols arranged half one way up, and half flipped.
Useful links:
This section is intended for mentors who are reviewing a trainee's work on the assignment.
After the assignment and sessions two, the trainee's code should resemble the core files.
The trainee's card class file should loosely match the example. It is possible they may not have implemented some of the things added in session two, such as immutability and type hints. The version of the code they will have started with at the end of sessions one is version 6. Implementing all() with a generator was given as an advanced option, but simply returning a list of all cards is fine, generators are explained in session two. Ensure their code is clean, with good naming, docstrings, and comments where appropriate. Discuss with them if you feel this is not the case.
The trainee's test file should closely match the example, with some difference choices made for the fixtures. Ensure they have implemented the multiple random test in a way they won't fail for some proportion of the runs.
Since the create_card_image.py part of the assignment is very open ended, the trainee's code is unlikely to match the example very closely. Use the core file to see a valid approach and some examples of image manipulation, and use your own judgement of good code to assess their approach. It is fine for the outputted images to be very none traditional, for the examples generated by create_card_image.py, see the Card Images folder.
This session should cover:
Sessions One followup:
- Recap session one using the examples files
- Check for questions
Assignment followup:
- Talk about any issues encountered
- Compare card images generated
- Generators (Briefly)
- Show debugging in VS Code
-
card_7 -
Add
mypy.ini- Explain:
- Types
- MyPy
- Explain:
card_8- Explain:
- Immutability
- e.g.
listvstuple
- e.g.
- Immutability