diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml
new file mode 100644
index 0000000..4e975a7
--- /dev/null
+++ b/.github/workflows/pylint.yml
@@ -0,0 +1,28 @@
+name: Pylint
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.12"]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v3
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install pylint
+ pip install -r requirements.txt
+ - name: Analysing the code with pylint
+ run: |
+ python -m pylint --verbose $(find . -name "*.py" ! -path "*/.venv/*" ! -path "*/venv/*") --rcfile=.pylintrc
\ No newline at end of file
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
new file mode 100644
index 0000000..c9c9d86
--- /dev/null
+++ b/.github/workflows/pytest.yml
@@ -0,0 +1,29 @@
+name: Pytest Workflow
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Python 3.12
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r requirements.txt
+ pip install pytest
+
+ - name: Run pytest
+ run: pytest --verbose --ignore=solutions
\ No newline at end of file
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..97d344e
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,3 @@
+[MASTER]
+ignore=.venv
+ignore-paths=^(.*/|)solutions/.*$
\ No newline at end of file
diff --git a/README.md b/README.md
index 6fd8298..7770891 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,22 @@
-# Exercism Python Track
+# [Exercism Python Track](https://exercism.io/my/tracks/python)
-Learn and master concepts to achieve fluency in Python.
+
+

+

+
+
+## Exercism exercises in Python
+
+### About Exercism
+Exercism is an online platform designed to help you improve your coding skills through practice and mentorship.
+
+Exercism provides you with thousands of exercises spread across numerous language tracks. Once you start a language track you are presented with a core set of exercises to complete. Each one is a fun and interesting challenge designed to teach you a little more about the features of a language.
+
+You complete a challenge by downloading the exercise to your computer and solving it in your normal working environment. Once you've finished you submit it online and one of our mentors will give you feedback on how you could improve it using features of the language that you may not be familiar with. After a couple of rounds of refactoring, your exercise will be complete and you will unlock both the next core exercise and also a series of related side-exercises for you to practice with.
+
+Exercism is entirely open source and relies on the contributions of thousands of wonderful people.
+
+Exercism is designed to be fun and friendly, and we place a strong emphasis on empathetic communication.
+
+Sign up and have fun. Exercism is 100% free :)
-143 coding exercises for Python on Exercism. From Error Handling to ISBN Verifier.
diff --git a/armstrong-numbers/armstrong_numbers_test.py b/armstrong-numbers/armstrong_numbers_test.py
index 4024766..092f75d 100644
--- a/armstrong-numbers/armstrong_numbers_test.py
+++ b/armstrong-numbers/armstrong_numbers_test.py
@@ -1,3 +1,46 @@
+# pylint: disable=C0301
+"""
+Armstrong Numbers Test Suite Documentation
+
+## Overview
+
+This test suite validates the `is_armstrong_number` function,
+ensuring its correct behavior for various types of numbers:
+
+- Single-digit and multi-digit numbers
+- Known Armstrong numbers and non-Armstrong numbers
+
+Tests are auto-generated based on canonical data from
+[Exercism problem specifications](https://github.com/exercism/problem-specifications/tree/main/exercises/armstrong-numbers/canonical-data.json).
+
+## Structure
+
+- **Framework:** Uses Python's built-in `unittest`.
+- **Target Function:** `is_armstrong_number` (imported from `armstrong_numbers` module).
+
+## Test Cases
+
+| Test Description | Input | Expected Output |
+|---------------------------------------------------------|----------|-----------------|
+| Zero is an Armstrong number | 0 | `True` |
+| Single-digit numbers are Armstrong numbers | 5 | `True` |
+| No two-digit numbers (e.g. 10) are Armstrong numbers | 10 | `False` |
+| 153 is an Armstrong number | 153 | `True` |
+| 100 is not an Armstrong number | 100 | `False` |
+| 9474 is an Armstrong number | 9474 | `True` |
+| 9475 is not an Armstrong number | 9475 | `False` |
+| 9926315 is an Armstrong number | 9926315 | `True` |
+| 9926314 is not an Armstrong number | 9926314 | `False` |
+
+## Usage
+
+To run the tests, ensure `is_armstrong_number` is implemented and run:
+
+```bash
+python -m unittest armstrong_numbers_test.py
+
+"""
+
# These tests are auto-generated with test data from:
# https://github.com/exercism/problem-specifications/tree/main/exercises/armstrong-numbers/canonical-data.json
# File last updated on 2023-07-20
@@ -10,29 +53,88 @@
class ArmstrongNumbersTest(unittest.TestCase):
+ """Armstrong Numbers Test."""
+
def test_zero_is_an_armstrong_number(self):
+ """
+ Test that zero is correctly identified as an Armstrong number.
+
+ This test verifies that the function correctly determines that 0
+ is an Armstrong number, as 0^1 == 0.
+
+ :returns: None
+ :rtype: NoneType
+ """
self.assertIs(is_armstrong_number(0), True)
def test_single_digit_numbers_are_armstrong_numbers(self):
+ """
+ Test that all single digit numbers are Armstrong numbers.
+
+ :returns: None. Asserts that a single digit number (e.g., 5) is an Armstrong number.
+ :rtype: NoneType
+ """
self.assertIs(is_armstrong_number(5), True)
def test_there_are_no_two_digit_armstrong_numbers(self):
+ """
+ Test that no two-digit numbers are Armstrong numbers.
+
+ :returns: None. Asserts that a two-digit number (e.g., 10) is not an Armstrong number.
+ :rtype: NoneType
+ """
self.assertIs(is_armstrong_number(10), False)
def test_three_digit_number_that_is_an_armstrong_number(self):
+ """
+ Test that 153 is correctly identified as an Armstrong number.
+
+ :returns: None. Asserts that 153 is an Armstrong number.
+ :rtype: NoneType
+ """
self.assertIs(is_armstrong_number(153), True)
def test_three_digit_number_that_is_not_an_armstrong_number(self):
+ """
+ Test that 100 is not identified as an Armstrong number.
+
+ :returns: None. Asserts that 100 is not an Armstrong number.
+ :rtype: NoneType
+ """
self.assertIs(is_armstrong_number(100), False)
def test_four_digit_number_that_is_an_armstrong_number(self):
+ """
+ Test that 9474 is correctly identified as an Armstrong number.
+
+ :returns: None. Asserts that 9474 is an Armstrong number.
+ :rtype: NoneType
+ """
self.assertIs(is_armstrong_number(9474), True)
def test_four_digit_number_that_is_not_an_armstrong_number(self):
+ """
+ Test that 9475 is not identified as an Armstrong number.
+
+ :returns: None. Asserts that 9475 is not an Armstrong number.
+ :rtype: NoneType
+ """
self.assertIs(is_armstrong_number(9475), False)
def test_seven_digit_number_that_is_an_armstrong_number(self):
+ """
+ Test that 9926315 is correctly identified as an Armstrong number.
+
+ :returns: None. Asserts that 9926315 is an Armstrong number.
+ :rtype: NoneType
+ """
self.assertIs(is_armstrong_number(9926315), True)
def test_seven_digit_number_that_is_not_an_armstrong_number(self):
+ """
+ Test that 9926314 is not identified as an Armstrong number.
+
+ :returns: None. Asserts that 9926314 is not an Armstrong number.
+ :rtype: NoneType
+ """
self.assertIs(is_armstrong_number(9926314), False)
diff --git a/black-jack/black_jack.py b/black-jack/black_jack.py
index c252b88..fb9dec8 100644
--- a/black-jack/black_jack.py
+++ b/black-jack/black_jack.py
@@ -19,7 +19,8 @@ def value_of_card(card) -> int:
"""
if card in 'JKQ':
return 10
- elif card == 'A':
+
+ if card == 'A':
return 1
return int(card)
@@ -29,8 +30,10 @@ def higher_card(card_one, card_two) -> str | tuple[str, str]:
"""
Determine which card has a higher value in the hand.
- :param card_one, card_two: str - cards dealt in hand. See below for values.
- :return: str or tuple - resulting Tuple contains both cards if they are of equal value.
+ :param card_one: str - cards dealt in hand. See below for values.
+ :param card_two: str - cards dealt in hand. See below for values.
+ :return: str or tuple - resulting Tuple contains both cards if
+ they are of equal value.
1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10
2. 'A' (ace card) = 1
@@ -38,7 +41,8 @@ def higher_card(card_one, card_two) -> str | tuple[str, str]:
"""
if value_of_card(card_one) == value_of_card(card_two):
return card_one, card_two
- elif value_of_card(card_one) > value_of_card(card_two):
+
+ if value_of_card(card_one) > value_of_card(card_two):
return card_one
return card_two
@@ -56,11 +60,14 @@ def value_of_ace(card_one, card_two) -> int:
3. '2' - '10' = numerical value.
"""
total: int = value_of_card(card_one) + value_of_card(card_two)
- # Hint: if we already have an ace in hand, then the value for the upcoming ace would be 1.
+ # Hint: if we already have an ace in hand, then the value for
+ # the upcoming ace would be 1.
if card_one == 'A' or card_two == 'A':
return 1
- # The value of the hand with the ace needs to be as high as possible without going over 21.
- elif 21 - total >= 11:
+ # The value of the hand with the ace needs to be as high as
+ # possible without going over 21.
+
+ if 21 - total >= 11:
return 11
return 1
@@ -70,7 +77,10 @@ def is_blackjack(card_one, card_two) -> bool:
"""
Determine if the hand is a 'natural' or 'blackjack'.
- :param card_one, card_two: str - card dealt. See below for values.
+ :param card_one: card dealt. See below for values.
+ :type card_one: str
+ :param card_two: card dealt. See below for values.
+ :type card_two: str
:return: bool - is the hand is a blackjack (two cards worth 21).
1. 'J', 'Q', or 'K' (otherwise known as "face cards") = 10
@@ -81,7 +91,8 @@ def is_blackjack(card_one, card_two) -> bool:
# as their first two cards, then the player has a score of 21.
if card_one == 'A' and card_two in ('J', 'Q', 'K', '10'):
return True
- elif card_two == 'A' and card_one in ('J', 'Q', 'K', '10'):
+
+ if card_two == 'A' and card_one in ('J', 'Q', 'K', '10'):
return True
return False
@@ -91,8 +102,10 @@ def can_split_pairs(card_one, card_two) -> bool:
"""
Determine if a player can split their hand into two hands.
- :param card_one, card_two: str - cards dealt.
- :return: bool - can the hand be split into two pairs? (i.e. cards are of the same value).
+ :param card_one: str - cards dealt.
+ :param card_two: str - cards dealt.
+ :return: bool - can the hand be split into two pairs?
+ (i.e. cards are of the same value).
"""
if value_of_card(card_one) == value_of_card(card_two):
return True
@@ -104,7 +117,9 @@ def can_double_down(card_one, card_two) -> bool:
"""
Determine if a blackjack player can place a double down bet.
- :param card_one, card_two: str - first and second cards in hand.
- :return: bool - can the hand can be doubled down? (i.e. totals 9, 10 or 11 points).
+ :param card_one: str - first and second cards in hand.
+ :param card_two: str - first and second cards in hand.
+ :return: bool - can the hand can be doubled down?
+ (i.e. totals 9, 10 or 11 points).
"""
return 9 <= value_of_card(card_one) + value_of_card(card_two) <= 11
diff --git a/black-jack/black_jack_test.py b/black-jack/black_jack_test.py
index 0962781..f01f2df 100644
--- a/black-jack/black_jack_test.py
+++ b/black-jack/black_jack_test.py
@@ -1,114 +1,188 @@
+# pylint: disable=C0301
+"""
+Unit tests suite for blackjack game logic functions.
+
+This test case class covers the following functionalities:
+
+ - :func:`value_of_card`: Evaluates the value of individual blackjack cards.
+ - :func:`higher_card`: Determines the higher value card or equality.
+ - :func:`value_of_ace`: Calculates optimal ace value given two cards.
+ - :func:`is_blackjack`: Checks if a hand is a 'blackjack' (natural 21).
+ - :func:`can_split_pairs`: Determines if a hand can be split into two pairs.
+ - :func:`can_double_down`: Assesses eligibility to double down.
+
+Tests use parameterized cases with informative error messages for each assertion.
+"""
+
import unittest
+from parameterized import parameterized
import pytest
from black_jack import (
- value_of_card,
- higher_card,
- value_of_ace,
- is_blackjack,
- can_split_pairs,
- can_double_down
- )
+ value_of_card,
+ higher_card,
+ value_of_ace,
+ is_blackjack,
+ can_split_pairs,
+ can_double_down
+)
+# pylint: disable=C0301
class BlackJackTest(unittest.TestCase):
+ """
+ Unit test suite for blackjack game logic functions.
- @pytest.mark.task(taskno=1)
- def test_value_of_card(self):
- test_data = [('2', 2), ('5', 5), ('8', 8),
- ('A', 1), ('10', 10), ('J', 10),
- ('Q', 10), ('K', 10)]
-
- for variant, (card, expected) in enumerate(test_data, 1):
- with self.subTest(f'variation #{variant}', card=card, expected=expected):
- actual_result = value_of_card(card)
- error_msg = (f'Called value_of_card({card}). '
- f'The function returned {actual_result} as the value of the {card} card, '
- f'but the test expected {expected} as the {card} card value.')
+ Each test validates a specific utility function from the blackjack module:
+ - :func:`test_value_of_card`: Checks the correct card value assignment.
+ - :func:`test_higher_card`: Ensures the function returns the card with higher value or both if equal.
+ - :func:`test_value_of_ace`: Verifies optimal ace value calculation based on the current hand.
+ - :func:`test_is_blackjack`: Determines if a two-card hand is a blackjack.
+ - :func:`test_can_split_pairs`: Tests if two cards can be split into two separate hands.
+ - :func:`test_can_double_down`: Evaluates if the hand is eligible for doubling down.
- self.assertEqual(actual_result, expected, msg=error_msg)
+ Tests are parameterized for thorough coverage and use descriptive assertion error messages.
+ """
+ @pytest.mark.task(taskno=1)
+ @parameterized.expand([
+ ('2', 2), ('5', 5), ('8', 8),
+ ('A', 1), ('10', 10), ('J', 10),
+ ('Q', 10), ('K', 10),
+ ])
+ def test_value_of_card(self, card, expected):
+ """
+ Test that the value_of_card function returns the correct
+ value for a given card.
+
+ :param card: Card to evaluate.
+ :type card: str
+ :param expected: Expected value for the provided card.
+ :type expected: int
+ """
+ actual_result = value_of_card(card)
+ error_msg = (f'Called value_of_card({card}). '
+ f'The function returned {actual_result} '
+ f'as the value of '
+ f'the {card} card, '
+ f'but the test expected {expected} as the '
+ f'{card} card value.')
+ self.assertEqual(actual_result, expected, msg=error_msg)
@pytest.mark.task(taskno=2)
- def test_higher_card(self):
- test_data = [('A', 'A', ('A', 'A')),
- ('10', 'J', ('10', 'J')),
- ('3', 'A', '3'),
- ('3', '6', '6'),
- ('Q', '10', ('Q', '10')),
- ('4', '4', ('4', '4')),
- ('9', '10', '10'),
- ('6', '9', '9'),
- ('4', '8', '8')]
-
- for variant, (card_one, card_two, expected) in enumerate(test_data, 1):
- with self.subTest(f'variation #{variant}', card_one=card_one, card_two=card_two, expected=expected):
- actual_result = higher_card(card_one, card_two)
- error_msg = (f'Called higher_card({card_one}, {card_two}). '
- f'The function returned {actual_result}, '
- f'but the test expected {expected} as the result for the cards {card_one, card_two}.')
-
- self.assertEqual(actual_result, expected, msg=error_msg)
+ @parameterized.expand([
+ ('A', 'A', ('A', 'A')),
+ ('10', 'J', ('10', 'J')),
+ ('3', 'A', '3'),
+ ('3', '6', '6'),
+ ('Q', '10', ('Q', '10')),
+ ('4', '4', ('4', '4')),
+ ('9', '10', '10'),
+ ('6', '9', '9'),
+ ('4', '8', '8'),
+ ])
+ def test_higher_card(self, card_one, card_two, expected):
+ """
+ Test that the higher_card function correctly determines
+ which card has the higher value.
+
+ :param card_one: First card to compare.
+ :param card_two: Second card to compare.
+ :param expected: The expected result for the higher card.
+ """
+ actual_result = higher_card(card_one, card_two)
+ error_msg = (f'Called higher_card({card_one}, {card_two}). '
+ f'The function returned {actual_result}, '
+ f'but the test expected {expected} as the result for '
+ f'the cards {card_one, card_two}.')
+ self.assertEqual(actual_result, expected, msg=error_msg)
@pytest.mark.task(taskno=3)
- def test_value_of_ace(self):
- test_data = [('2', '3', 11), ('3', '6', 11), ('5', '2', 11),
- ('8', '2', 11), ('5', '5', 11), ('Q', 'A', 1),
- ('10', '2', 1), ('7', '8', 1), ('J', '9', 1),
- ('K', 'K', 1), ('2', 'A', 1), ('A', '2', 1)]
-
- for variant, (card_one, card_two, ace_value) in enumerate(test_data, 1):
- with self.subTest(f'variation #{variant}', card_one=card_one, card_two=card_two, ace_value=ace_value):
- actual_result = value_of_ace(card_one, card_two)
- error_msg = (f'Called value_of_ace({card_one}, {card_two}). '
- f'The function returned {actual_result}, '
- f'but the test expected {ace_value} as the value of an ace card '
- f'when the hand includes {card_one, card_two}.')
-
- self.assertEqual(value_of_ace(card_one, card_two), ace_value, msg=error_msg)
+ @parameterized.expand([
+ ('2', '3', 11), ('3', '6', 11), ('5', '2', 11),
+ ('8', '2', 11), ('5', '5', 11), ('Q', 'A', 1),
+ ('10', '2', 1), ('7', '8', 1), ('J', '9', 1),
+ ('K', 'K', 1), ('2', 'A', 1), ('A', '2', 1),
+ ])
+ def test_value_of_ace(self, card_one, card_two, ace_value):
+ """
+ Test that the value_of_ace function returns the correct ace value
+ given the other two cards.
+
+ :param card_one: The first card in hand.
+ :param card_two: The second card in hand.
+ :param ace_value: The expected ace value.
+ """
+ actual_result = value_of_ace(card_one, card_two)
+ error_msg = (f'Called value_of_ace({card_one}, {card_two}). '
+ f'The function returned {actual_result}, '
+ f'but the test expected {ace_value} '
+ f'as the value of an ace card '
+ f'when the hand includes {card_one, card_two}.')
+ self.assertEqual(actual_result, ace_value, msg=error_msg)
@pytest.mark.task(taskno=4)
- def test_is_blackjack(self):
- test_data = [(('A', 'K'), True), (('10', 'A'), True),
- (('10', '9'), False), (('A', 'A'), False),
- (('4', '7'), False), (('9', '2'), False),
- (('Q', 'K'), False)]
-
- for variant, (hand, expected) in enumerate(test_data, 1):
- with self.subTest(f'variation #{variant}', hand=hand, expected=expected):
- actual_result = is_blackjack(*hand)
- error_msg = (f'Called is_blackjack({hand[0]}, {hand[1]}). '
- f'The function returned {actual_result}, '
- f'but hand {hand} {"is" if expected else "is not"} a blackjack.')
-
- self.assertEqual(actual_result, expected, msg=error_msg)
+ @parameterized.expand([
+ (('A', 'K'), True), (('10', 'A'), True),
+ (('10', '9'), False), (('A', 'A'), False),
+ (('4', '7'), False), (('9', '2'), False),
+ (('Q', 'K'), False),
+ ])
+ def test_is_blackjack(self, hand, expected):
+ """
+ Test if a given hand qualifies as blackjack.
+
+ :param hand: List representing the player's hand.
+ :param expected: Expected boolean indicating if the hand is a blackjack.
+ """
+ actual_result = is_blackjack(*hand)
+ error_msg = (f'Called is_blackjack({hand[0]}, {hand[1]}). '
+ f'The function returned {actual_result}, '
+ f'but hand {hand} {"is" if expected else "is not"} '
+ f'a blackjack.')
+ self.assertEqual(actual_result, expected, msg=error_msg)
@pytest.mark.task(taskno=5)
- def test_can_split_pairs(self):
- test_data = [(('Q', 'K'), True), (('6', '6'), True),
- (('A', 'A'), True),(('10', 'A'), False),
- (('10', '9'), False)]
-
- for variant, (hand, expected) in enumerate(test_data, 1):
- with self.subTest(f'variation #{variant}', input=hand, expected=expected):
- actual_result = can_split_pairs(*hand)
- error_msg = (f'Called can_split_pairs({hand[0]}, {hand[1]}). '
- f'The function returned {actual_result}, '
- f'but hand {hand} {"can" if expected else "cannot"} be split into pairs.')
-
- self.assertEqual(actual_result, expected, msg=error_msg)
+ @parameterized.expand([
+ (('Q', 'K'), True), (('6', '6'), True),
+ (('A', 'A'), True), (('10', 'A'), False),
+ (('10', '9'), False),
+ ])
+ def test_can_split_pairs(self, hand, expected):
+ """
+ Test whether the `can_split_pairs` function correctly
+ determines if a given blackjack hand can be split into pairs.
+
+ :param hand: List representing the player's hand.
+ :param expected: Expected boolean result indicating if split is allowed.
+ """
+ actual_result = can_split_pairs(*hand)
+ error_msg = (f'Called can_split_pairs({hand[0]}, {hand[1]}). '
+ f'The function returned {actual_result}, '
+ f'but hand {hand} '
+ f'{"can" if expected else "cannot"} '
+ f'be split into pairs.')
+ self.assertEqual(actual_result, expected, msg=error_msg)
@pytest.mark.task(taskno=6)
- def test_can_double_down(self):
- test_data = [(('A', '9'), True), (('K', 'A'), True),
- (('4', '5'), True),(('A', 'A'), False),
- (('10', '2'), False), (('10', '9'), False)]
-
- for variant, (hand, expected) in enumerate(test_data, 1):
- with self.subTest(f'variation #{variant}', hand=hand, expected=expected):
- actual_result = can_double_down(*hand)
- error_msg = (f'Called can_double_down({hand[0]}, {hand[1]}). '
- f'The function returned {actual_result}, '
- f'but hand {hand} {"can" if expected else "cannot"} be doubled down.')
-
- self.assertEqual(actual_result, expected, msg=error_msg)
+ @parameterized.expand([
+ (('A', '9'), True), (('K', 'A'), True),
+ (('4', '5'), True), (('A', 'A'), False),
+ (('10', '2'), False), (('10', '9'), False),
+ ])
+ def test_can_double_down(self, hand, expected):
+ """
+ Test whether a hand qualifies to double down in blackjack.
+
+ :param hand: The current cards in the player's hand.
+ :type hand: list
+ :param expected: The expected boolean result of whether the
+ hand can double down.
+ :type expected: bool
+ """
+ actual_result = can_double_down(*hand)
+ error_msg = (f'Called can_double_down({hand[0]}, {hand[1]}). '
+ f'The function returned {actual_result}, '
+ f'but hand {hand} {"can" if expected else "cannot"} '
+ f'be doubled down.')
+ self.assertEqual(actual_result, expected, msg=error_msg)
diff --git a/card-games/lists.py b/card-games/lists.py
index 1fc2e68..55f76fc 100644
--- a/card-games/lists.py
+++ b/card-games/lists.py
@@ -56,7 +56,7 @@ def approx_average_is_average(hand: list[int]) -> bool:
:return: bool - does one of the approximate averages equal the `true average`?
"""
avg: float = card_average(hand)
- return (hand[0] + hand[-1]) / 2 == avg or hand[len(hand) // 2] == avg
+ return avg in ((hand[0] + hand[-1]) / 2, hand[len(hand) // 2])
def average_even_is_average_odd(hand: list[int]) -> bool:
diff --git a/card-games/lists_test.py b/card-games/lists_test.py
index e550112..1c5386e 100644
--- a/card-games/lists_test.py
+++ b/card-games/lists_test.py
@@ -1,3 +1,14 @@
+# pylint: disable=C0301
+"""
+Unit tests for the card game utility functions in the lists module.
+
+Tests cover core behaviors such as generating rounds, concatenating rounds,
+verifying round presence, calculating averages, comparing approximate and
+actual averages, comparing even/odd index averages, and conditionally
+doubling the last card value.
+
+Uses unittest framework with pytest marking for grading tasks.
+"""
import unittest
import pytest
@@ -13,17 +24,26 @@
class CardGamesTest(unittest.TestCase):
+ """Unit tests for card games utility functions."""
@pytest.mark.task(taskno=1)
def test_get_rounds(self):
+ """
+ Test the get_rounds function with various inputs to ensure it returns
+ the current round and the next two rounds.
+ :param self: The test case instance.
+ """
input_data = [0, 1, 10, 27, 99, 666]
result_data = [[0, 1, 2], [1, 2, 3],
[10, 11, 12], [27, 28, 29],
[99, 100, 101], [666, 667, 668]]
- for variant, (number, expected) in enumerate(zip(input_data, result_data), start=1):
- with self.subTest(f'variation #{variant}', number=number, expected=expected):
+ for variant, (number, expected) in enumerate(
+ zip(input_data, result_data), start=1):
+ with self.subTest(f'variation #{variant}',
+ number=number,
+ expected=expected):
actual_result = get_rounds(number)
error_message = (f'Called get_rounds({number}). '
f'The function returned {actual_result}, '
@@ -34,7 +54,12 @@ def test_get_rounds(self):
@pytest.mark.task(taskno=2)
def test_concatenate_rounds(self):
+ """
+ Test the concatenate_rounds function to ensure it correctly combines
+ two lists of rounds.
+ :param self: The test case instance.
+ """
input_data = [([], []), ([0, 1], []), ([], [1, 2]),
([1], [2]), ([27, 28, 29], [35, 36]),
([1, 2, 3], [4, 5, 6])]
@@ -43,8 +68,13 @@ def test_concatenate_rounds(self):
[27, 28, 29, 35, 36],
[1, 2, 3, 4, 5, 6]]
- for variant, ((rounds_1, rounds_2), expected) in enumerate(zip(input_data, result_data), start=1):
- with self.subTest(f'variation #{variant}', rounds_1=rounds_1, rounds_2=rounds_2, expected=expected):
+ for variant, ((rounds_1, rounds_2), expected) in enumerate(
+ zip(input_data, result_data),
+ start=1):
+ with self.subTest(f'variation #{variant}',
+ rounds_1=rounds_1,
+ rounds_2=rounds_2,
+ expected=expected):
actual_result = concatenate_rounds(rounds_1, rounds_2)
error_message = (f'Called concatenate_rounds({rounds_1}, {rounds_2}). '
f'The function returned {actual_result}, but the tests '
@@ -55,15 +85,25 @@ def test_concatenate_rounds(self):
@pytest.mark.task(taskno=3)
def test_list_contains_round(self):
+ """
+ Test the list_contains_round function to check if a specific round
+ is present in the list of played rounds.
+ :param self: The test case instance.
+ """
input_data = [([], 1), ([1, 2, 3], 0),
([27, 28, 29, 35, 36], 30),
([1], 1), ([1, 2, 3], 1),
([27, 28, 29, 35, 36], 29)]
result_data = [False, False, False, True, True, True]
- for variant, ((rounds, round_number), expected) in enumerate(zip(input_data, result_data), start=1):
- with self.subTest(f'variation #{variant}', rounds=rounds, round_number=round_number, expected=expected):
+ for variant, ((rounds, round_number), expected) in enumerate(
+ zip(input_data, result_data),
+ start=1):
+ with self.subTest(f'variation #{variant}',
+ rounds=rounds,
+ round_number=round_number,
+ expected=expected):
actual_result = list_contains_round(rounds, round_number)
error_message = (f'Called list_contains_round({rounds}, {round_number}). '
f'The function returned {actual_result}, but round {round_number} '
@@ -73,7 +113,12 @@ def test_list_contains_round(self):
@pytest.mark.task(taskno=4)
def test_card_average(self):
+ """
+ Test the card_average function to ensure it calculates the correct
+ average of card values in a hand.
+ :param self: The test case instance.
+ """
input_data = [[1], [5, 6, 7], [1, 2, 3, 4], [1, 10, 100]]
result_data = [1.0, 6.0, 2.5, 37.0]
@@ -88,7 +133,12 @@ def test_card_average(self):
@pytest.mark.task(taskno=5)
def test_approx_average_is_average(self):
+ """
+ Test the approx_average_is_average function to check if approximate
+ averages match the actual average.
+ :param self: The test case instance.
+ """
input_data = [[0, 1, 5], [3, 6, 9, 12, 150], [1, 2, 3, 5, 9],
[2, 3, 4, 7, 8], [1, 2, 3], [2, 3, 4],
[2, 3, 4, 8, 8], [1, 2, 4, 5, 8]]
@@ -107,7 +157,12 @@ def test_approx_average_is_average(self):
@pytest.mark.task(taskno=6)
def test_average_even_is_average_odd(self):
+ """
+ Test the average_even_is_average_odd function to verify if averages
+ of even and odd indexed cards are equal.
+ :param self: The test case instance.
+ """
input_data = [[5, 6, 8], [1, 2, 3, 4], [1, 2, 3], [5, 6, 7], [1, 3, 5, 7, 9]]
result_data = [False, False, True, True, True]
@@ -123,15 +178,24 @@ def test_average_even_is_average_odd(self):
@pytest.mark.task(taskno=7)
def test_maybe_double_last(self):
+ """
+ Test the maybe_double_last function to ensure it doubles the last card
+ if it is a Jack (11).
+ :param self: The test case instance.
+ """
input_data = [(1, 2, 11), (5, 9, 11), (5, 9, 10), (1, 2, 3), (1, 11, 8)]
result_data = [[1, 2, 22], [5, 9, 22], [5, 9, 10], [1, 2, 3], [1, 11, 8]]
- for variant, (hand, expected) in enumerate(zip(input_data, result_data), start=1):
- with self.subTest(f'variation #{variant}', hand=list(hand), expected=expected):
+ for variant, (hand, expected) in enumerate(
+ zip(input_data, result_data), start=1):
+ with self.subTest(f'variation #{variant}',
+ hand=list(hand),
+ expected=expected):
actual_result = maybe_double_last(list(hand))
error_message = (f'Called maybe_double_last({list(hand)}). '
f'The function returned {actual_result}, but '
- f'the tests expected {expected} as the maybe-doubled version of {list(hand)}.')
+ f'the tests expected {expected} as the '
+ f'maybe-doubled version of {list(hand)}.')
self.assertEqual(actual_result, expected, msg=error_message)
diff --git a/chaitanas-colossal-coaster/list_methods_test.py b/chaitanas-colossal-coaster/list_methods_test.py
index 7a754b7..2d68b0b 100644
--- a/chaitanas-colossal-coaster/list_methods_test.py
+++ b/chaitanas-colossal-coaster/list_methods_test.py
@@ -1,3 +1,20 @@
+# pylint: disable=C0301
+"""
+Unit tests for queue management functions at Chaitana's roller coaster.
+
+This test suite,
+- Validates correct functional behavior for basic queue operations (add, remove, insert, search, and sort) for both express and normal queues.
+- Ensures that each function both returns the correct result and properly mutates or does not mutate the input lists as expected.
+- Employs parameterized testing patterns, subTest blocks, and deep copies to safeguard against unintended mutation of shared state, ensuring accurate and isolated assertions.
+- Uses unittest and pytest frameworks for flexible test discovery and marking (with @pytest.mark.task to delineate test responsibilities).
+
+Each function imported from `list_methods` is tested with a range of scenarios, including:
+- Standard and edge inputs,
+- In-place versus return-value mutation expectations,
+- Validation that the queue's structure and contents match anticipated post-conditions.
+
+Users can run this file as a standard unittest/pytest module. Extend or edit test cases as new queue behaviors are implemented.
+"""
import unittest
from copy import deepcopy
import pytest
@@ -14,14 +31,31 @@
)
+# pylint: disable=C0301
class ListMethodsTest(unittest.TestCase):
+ """
+ Unit test suite for list manipulation functions.
+
+ Tests various utility methods defined in the list_methods module.
+ """
+
@pytest.mark.task(taskno=1)
def test_add_me_to_the_queue(self):
+ """
+ Test the add_me_to_the_queue function to ensure it adds a person
+ to the correct queue based on ticket type and returns the updated queue.
+
+ :param self: The test case instance.
+ """
test_data = [
- ((['Tony', 'Bruce'], ['RobotGuy', 'WW'], 0, 'HawkEye'), ['RobotGuy', 'WW', 'HawkEye']),
- ((['Tony', 'Bruce'], ['RobotGuy', 'WW'], 1, 'RichieRich'), ['Tony', 'Bruce', 'RichieRich']),
- ((['Agatha', 'Pepper', 'Valkyrie'], ['Drax', 'Nebula'], 1, 'Okoye'), ['Agatha', 'Pepper', 'Valkyrie', 'Okoye']),
- ((['Agatha', 'Pepper', 'Valkyrie'], ['Drax', 'Nebula'], 0, 'Gamora'), ['Drax', 'Nebula', 'Gamora']),
+ ((['Tony', 'Bruce'], ['RobotGuy', 'WW'], 0, 'HawkEye'),
+ ['RobotGuy', 'WW', 'HawkEye']),
+ ((['Tony', 'Bruce'], ['RobotGuy', 'WW'], 1, 'RichieRich'),
+ ['Tony', 'Bruce', 'RichieRich']),
+ ((['Agatha', 'Pepper', 'Valkyrie'], ['Drax', 'Nebula'], 1, 'Okoye'),
+ ['Agatha', 'Pepper', 'Valkyrie', 'Okoye']),
+ ((['Agatha', 'Pepper', 'Valkyrie'], ['Drax', 'Nebula'], 0, 'Gamora'),
+ ['Drax', 'Nebula', 'Gamora']),
]
for variant, (params, expected) in enumerate(test_data, start=1):
@@ -29,11 +63,14 @@ def test_add_me_to_the_queue(self):
# That mutation wrecks havoc with the verification and error messaging.
express_queue, normal_queue, ticket_type, person_name = deepcopy(params)
- with self.subTest(f'variation #{variant}', params=params, expected=expected):
+ with self.subTest(f'variation #{variant}',
+ params=params,
+ expected=expected):
actual_result = add_me_to_the_queue(*params)
error_message = (
- f'\nCalled add_me_to_the_queue{express_queue, normal_queue, ticket_type, person_name}.\n'
+ f'\nCalled add_me_to_the_queue'
+ f'{express_queue, normal_queue, ticket_type, person_name}.\n'
f'The function returned {actual_result},\n'
f' but the tests expected {expected} after {person_name} was added.')
@@ -41,11 +78,21 @@ def test_add_me_to_the_queue(self):
@pytest.mark.task(taskno=1)
def test_add_me_to_the_queue_validate_queue(self):
+ """
+ Test the add_me_to_the_queue function to validate that it mutates
+ and returns the correct queue based on ticket type.
+
+ :param self: The test case instance.
+ """
test_data = [
- ((['Tony', 'Bruce'], ['RobotGuy', 'WW'], 0, 'HawkEye'), ['RobotGuy', 'WW', 'HawkEye']),
- ((['Tony', 'Bruce'], ['RobotGuy', 'WW'], 1, 'RichieRich'), ['Tony', 'Bruce', 'RichieRich']),
- ((['Agatha', 'Pepper', 'Valkyrie'], ['Drax', 'Nebula'], 1, 'Okoye'), ['Agatha', 'Pepper', 'Valkyrie', 'Okoye']),
- ((['Agatha', 'Pepper', 'Valkyrie'], ['Drax', 'Nebula'], 0, 'Gamora'), ['Drax', 'Nebula', 'Gamora']),
+ ((['Tony', 'Bruce'], ['RobotGuy', 'WW'], 0, 'HawkEye'),
+ ['RobotGuy', 'WW', 'HawkEye']),
+ ((['Tony', 'Bruce'], ['RobotGuy', 'WW'], 1, 'RichieRich'),
+ ['Tony', 'Bruce', 'RichieRich']),
+ ((['Agatha', 'Pepper', 'Valkyrie'], ['Drax', 'Nebula'], 1, 'Okoye'),
+ ['Agatha', 'Pepper', 'Valkyrie', 'Okoye']),
+ ((['Agatha', 'Pepper', 'Valkyrie'], ['Drax', 'Nebula'], 0, 'Gamora'),
+ ['Drax', 'Nebula', 'Gamora']),
]
for variant, (params, expected) in enumerate(test_data, start=1):
@@ -55,14 +102,18 @@ def test_add_me_to_the_queue_validate_queue(self):
express, normal, ticket, name = params
with self.subTest(f'variation #{variant}',
- express=express, normal=normal,
- ticket=ticket, name=name, expected=expected):
+ express=express,
+ normal=normal,
+ ticket=ticket,
+ name=name,
+ expected=expected):
actual_result = add_me_to_the_queue(express, normal, ticket, name)
if type == 1:
error_message = (
- f'\nCalled add_me_to_the_queue{express_queue, normal_queue, ticket_type, person_name}.\n'
+ f'\nCalled add_me_to_the_queue'
+ f'{express_queue, normal_queue, ticket_type, person_name}.\n'
f'The queue == {express}, but the tests expected\n'
f'queue == {expected} after {person_name} was added.'
)
@@ -80,6 +131,12 @@ def test_add_me_to_the_queue_validate_queue(self):
@pytest.mark.task(taskno=2)
def test_find_my_friend(self):
+ """
+ Test the find_my_friend function to ensure it returns the correct
+ index of the friend in the queue.
+
+ :param self: The test case instance.
+ """
test_data = [
(['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 'Natasha'),
(['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 'Steve'),
@@ -102,6 +159,12 @@ def test_find_my_friend(self):
@pytest.mark.task(taskno=3)
def test_add_me_with_my_friends(self):
+ """
+ Test the add_me_with_my_friends function to ensure it inserts the person
+ at the specified index and returns the updated queue.
+
+ :param self: The test case instance.
+ """
test_data = [
(['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 0, 'Bucky'),
(['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 1, 'Bucky'),
@@ -133,6 +196,12 @@ def test_add_me_with_my_friends(self):
@pytest.mark.task(taskno=3)
def test_add_me_with_my_friends_validate_queue(self):
+ """
+ Test the add_me_with_my_friends function to validate that it mutates
+ the original queue and returns it.
+
+ :param self: The test case instance.
+ """
test_data = [
(['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 0, 'Bucky'),
(['Natasha', 'Steve', 'Tchalla', 'Wanda', 'Rocket'], 1, 'Bucky'),
@@ -164,6 +233,12 @@ def test_add_me_with_my_friends_validate_queue(self):
@pytest.mark.task(taskno=4)
def test_remove_the_mean_person(self):
+ """
+ Test the remove_the_mean_person function to ensure it removes the specified
+ person and returns the updated queue.
+
+ :param self: The test case instance.
+ """
test_data = [
(['Natasha', 'Steve', 'Ultron', 'Wanda', 'Rocket'], 'Ultron'),
(['Natasha', 'Steve', 'Wanda', 'Rocket', 'Ultron'], 'Rocket'),
@@ -195,6 +270,12 @@ def test_remove_the_mean_person(self):
@pytest.mark.task(taskno=4)
def test_remove_the_mean_person_validate_queue(self):
+ """
+ Test the remove_the_mean_person function to validate that it mutates
+ the original queue and returns it.
+
+ :param self: The test case instance.
+ """
test_data = [
(['Natasha', 'Steve', 'Ultron', 'Wanda', 'Rocket'], 'Ultron'),
(['Natasha', 'Steve', 'Wanda', 'Rocket', 'Ultron'], 'Rocket'),
@@ -229,6 +310,12 @@ def test_remove_the_mean_person_validate_queue(self):
@pytest.mark.task(taskno=5)
def test_how_many_namefellows(self):
+ """
+ Test the how_many_namefellows function to ensure it correctly counts
+ occurrences of a name in the queue.
+
+ :param self: The test case instance.
+ """
test_data = [(['Natasha', 'Steve', 'Ultron', 'Natasha', 'Rocket'], 'Bucky'),
(['Natasha', 'Steve', 'Ultron', 'Rocket'], 'Natasha'),
(['Natasha', 'Steve', 'Ultron', 'Natasha', 'Rocket'], 'Natasha')]
@@ -248,10 +335,19 @@ def test_how_many_namefellows(self):
@pytest.mark.task(taskno=6)
def test_remove_the_last_person(self):
+ """
+ Test the remove_the_last_person function to ensure it removes and returns
+ the last person from the queue, mutating the queue.
+
+ :param self: The test case instance.
+ """
test_data = [
- (['Natasha', 'Steve', 'Ultron', 'Natasha', 'Rocket'], ['Natasha', 'Steve', 'Ultron', 'Natasha'], 'Rocket'),
- (['Wanda', 'Natasha', 'Steve', 'Rocket', 'Ultron'], ['Wanda', 'Natasha', 'Steve', 'Rocket'], 'Ultron'),
- (['Steve', 'Wanda', 'Rocket', 'Ultron', 'Natasha'], ['Steve', 'Wanda', 'Rocket', 'Ultron'], 'Natasha')
+ (['Natasha', 'Steve', 'Ultron', 'Natasha', 'Rocket'],
+ ['Natasha', 'Steve', 'Ultron', 'Natasha'], 'Rocket'),
+ (['Wanda', 'Natasha', 'Steve', 'Rocket', 'Ultron'],
+ ['Wanda', 'Natasha', 'Steve', 'Rocket'], 'Ultron'),
+ (['Steve', 'Wanda', 'Rocket', 'Ultron', 'Natasha'],
+ ['Steve', 'Wanda', 'Rocket', 'Ultron'], 'Natasha')
]
for variant, (queue, modified, expected) in enumerate(test_data, start=1):
with self.subTest(f'variation #{variant}', queue=queue, modified=modified, expected=expected):
@@ -264,15 +360,23 @@ def test_remove_the_last_person(self):
expected_queue = modified
error_message = (f'\nCalled remove_the_last_person({unmodified_queue}).\n'
- f'The function was expected to remove and return the name "{expected_result}" '
+ f'The function was expected to remove and return the name '
+ f'"{expected_result}" '
f'and change the queue to {expected_queue},\n'
- f'but the name "{actual_result}" was returned and the queue == {queue}.')
+ f'but the name "{actual_result}" was returned and the queue == '
+ f'{queue}.')
self.assertEqual((actual_result, queue), (expected_result, expected_queue), msg=error_message)
@pytest.mark.task(taskno=7)
def test_sorted_names(self):
+ """
+ Test the sorted_names function to ensure it returns a sorted copy
+ of the queue without mutating the original.
+
+ :param self: The test case instance.
+ """
test_data =(
(['Steve', 'Ultron', 'Natasha', 'Rocket'], ['Natasha', 'Rocket', 'Steve', 'Ultron']),
(['Agatha', 'Pepper', 'Valkyrie', 'Drax', 'Nebula'], ['Agatha', 'Drax', 'Nebula', 'Pepper', 'Valkyrie']),
@@ -292,6 +396,12 @@ def test_sorted_names(self):
@pytest.mark.task(taskno=7)
def test_sorted_names_validate_queue(self):
+ """
+ Test the sorted_names function to validate that it does not mutate
+ the original queue.
+
+ :param self: The test case instance.
+ """
test_data = (
(['Steve', 'Ultron', 'Natasha', 'Rocket'], ['Natasha', 'Rocket', 'Steve', 'Ultron']),
(['Agatha', 'Pepper', 'Valkyrie', 'Drax', 'Nebula'], ['Agatha', 'Drax', 'Nebula', 'Pepper', 'Valkyrie']),
diff --git a/collatz-conjecture/collatz_conjecture.py b/collatz-conjecture/collatz_conjecture.py
index 7755bfc..a97aab0 100644
--- a/collatz-conjecture/collatz_conjecture.py
+++ b/collatz-conjecture/collatz_conjecture.py
@@ -13,6 +13,7 @@
"""
+# pylint: disable=R0801
def steps(number: int) -> int:
"""
Return the number of steps it takes to reach 1 according to
diff --git a/collatz-conjecture/collatz_conjecture_test.py b/collatz-conjecture/collatz_conjecture_test.py
index 306e3db..1b2b3f5 100644
--- a/collatz-conjecture/collatz_conjecture_test.py
+++ b/collatz-conjecture/collatz_conjecture_test.py
@@ -1,3 +1,11 @@
+# pylint: disable=C0301
+"""
+Unit tests for the steps function implementing the Collatz Conjecture.
+
+Tests include validation for zero, negative, even, odd values, and large numbers.
+Ensures ValueError is raised for invalid input according to Collatz rules.
+"""
+
# These tests are auto-generated with test data from:
# https://github.com/exercism/problem-specifications/tree/main/exercises/collatz-conjecture/canonical-data.json
# File last updated on 2023-07-20
@@ -9,7 +17,10 @@
)
+# pylint: disable=C0116
class CollatzConjectureTest(unittest.TestCase):
+ """Unit tests for the functions related to the Collatz conjecture."""
+
def test_zero_steps_for_one(self):
self.assertEqual(steps(1), 0)
@@ -26,10 +37,12 @@ def test_zero_is_an_error(self):
with self.assertRaises(ValueError) as err:
steps(0)
self.assertEqual(type(err.exception), ValueError)
- self.assertEqual(err.exception.args[0], "Only positive integers are allowed")
+ self.assertEqual(err.exception.args[0],
+ "Only positive integers are allowed")
def test_negative_value_is_an_error(self):
with self.assertRaises(ValueError) as err:
steps(-15)
self.assertEqual(type(err.exception), ValueError)
- self.assertEqual(err.exception.args[0], "Only positive integers are allowed")
+ self.assertEqual(err.exception.args[0],
+ "Only positive integers are allowed")
diff --git a/currency-exchange/exchange.py b/currency-exchange/exchange.py
index e1871e5..ee23d07 100644
--- a/currency-exchange/exchange.py
+++ b/currency-exchange/exchange.py
@@ -1,9 +1,11 @@
"""
Functions for calculating steps in exchanging currency.
-Python numbers documentation: https://docs.python.org/3/library/stdtypes.html#numeric-types-int-float-complex
+Python numbers documentation:
+https://docs.python.org/3/library/stdtypes.html#numeric-types-int-float-complex
-Overview of exchanging currency when travelling: https://www.compareremit.com/money-transfer-tips/guide-to-exchanging-currency-for-overseas-travel/
+Overview of exchanging currency when travelling:
+https://www.compareremit.com/money-transfer-tips/guide-to-exchanging-currency-for-overseas-travel/
"""
@@ -31,7 +33,7 @@ def get_change(budget: float,
:param exchanging_value: float - amount of your money you want to exchange now.
:return: float - amount left of your starting currency after exchanging.
"""
- return budget - exchanging_value
+ return budget - exchanging_value # pylint: disable=R0801
def get_value_of_bills(denomination: float,
@@ -75,6 +77,7 @@ def get_leftover_of_bills(amount: float,
return amount % denomination
+# pylint: disable=R0801
def exchangeable_value(budget: float,
exchange_rate: float,
spread: int,
diff --git a/currency-exchange/exchange_test.py b/currency-exchange/exchange_test.py
index fd3754c..7f1f495 100644
--- a/currency-exchange/exchange_test.py
+++ b/currency-exchange/exchange_test.py
@@ -1,3 +1,12 @@
+# pylint: disable=C0301
+"""
+Unit tests for currency exchange functions.
+
+Tests cover calculations for currency exchange, change, bill values, bill counts, leftovers, and determining maximum currency exchangeable after applying spread and denomination constraints.
+
+Uses pytest and unittest for parametrized and task-specific testing.
+"""
+
import unittest
import pytest
@@ -10,7 +19,10 @@
exchangeable_value)
+# pylint: disable=C0116
class CurrencyExchangeTest(unittest.TestCase):
+ """Unit tests for currency exchange utility functions."""
+
@pytest.mark.task(taskno=1)
def test_exchange_money(self):
test_data = [(100000, 0.8), (700000, 10.0)]
@@ -133,7 +145,8 @@ def test_exchangeable_value(self):
expected=expected):
actual_result = exchangeable_value(budget, exchange_rate, spread, denomination)
- error_message = (f'Called exchangeable_value{budget, exchange_rate, spread, denomination}. '
+ error_message = (f'Called exchangeable_value'
+ f'{budget, exchange_rate, spread, denomination}. '
f'The function returned {actual_result}, but '
f'The tests expected {expected} as the maximum '
f'value of the new currency .')
diff --git a/ghost-gobble-arcade-game/arcade_game_test.py b/ghost-gobble-arcade-game/arcade_game_test.py
index 2ffdd3d..336eefd 100644
--- a/ghost-gobble-arcade-game/arcade_game_test.py
+++ b/ghost-gobble-arcade-game/arcade_game_test.py
@@ -1,10 +1,66 @@
+# pylint: disable=C0301, C0116
+"""
+
+This `arcade_game_test.py` file is a comprehensive set of unit tests
+for the Pac-Man-style "ghost gobble arcade game" logic found in
+`arcade_game.py`. The test class uses Python's `unittest` framework
+(with `pytest` markers for task organization) and checks the
+correctness of each function (`eat_ghost`, `score`, `lose`, `win`)
+provided by the game logic module.
+
+Structure & Coverage
+
+1. **eat_ghost**
+- Tests if Pac-Man can eat a ghost only when:
+ - A power pellet is active **and**
+ - He's touching a ghost.
+- Includes positive (should eat) and negative (should not eat) scenarios.
+
+2. **score**
+- Tests Pac-Man scores when:
+ - Touching a dot.
+ - Touching a power pellet.
+ - Not scoring when touching neither.
+
+3. **lose**
+- Tests losing logic:
+ - Should lose if touching a ghost without a power pellet.
+ - Should *not* lose if touching a ghost *with* a power pellet active.
+ - Should *not* lose if not touching a ghost.
+
+4. **win**
+- Tests winning the game:
+ - Wins if *all dots eaten* and hasn't lost (touching ghost *without* pellet).
+ - Doesn't win if all dots eaten *but* touching a ghost without a pellet.
+ - Wins if all dots eaten *and* touching a ghost *with* a pellet (i.e., didn't lose).
+ - Doesn't win if not all dots eaten.
+
+General Comments
+
+- The error messages are descriptive to aid debugging if an assertion fails.
+- Each test calls the appropriate function with various inputs to check all critical edge and task-specific cases.
+- Follows best practices for test case isolation and clarity.
+
+Final Note
+
+This suite provides full behavioral coverage of the gameplay logic as
+specified in `arcade_game.py`. To run these tests, simply execute the
+file with a compatible test runner (pytest or python -m unittest).
+Make sure the tested functions are correctly imported from the
+`arcade_game` module (as in your header).
+
+If you need any enhancements, parameterizations, or help troubleshooting
+failing test cases, let me know!
+"""
import unittest
import pytest
from arcade_game import eat_ghost, score, lose, win
class GhostGobbleGameTest(unittest.TestCase):
-
+ """
+ Unit tests for the Ghost Gobble arcade game functions using unittest framework.
+ """
@pytest.mark.task(taskno=1)
def test_ghost_gets_eaten(self):
actual_result = eat_ghost(True, True)
diff --git a/grains/grains_test.py b/grains/grains_test.py
index 3c7f281..526319a 100644
--- a/grains/grains_test.py
+++ b/grains/grains_test.py
@@ -1,3 +1,12 @@
+# pylint: disable=C0301
+"""
+Unit tests for the grains module.
+
+Tests the square and total functions to ensure correct calculations
+of grains on each square, total grains, and proper error handling for
+invalid input.
+"""
+
# These tests are auto-generated with test data from:
# https://github.com/exercism/problem-specifications/tree/main/exercises/grains/canonical-data.json
# File last updated on 2023-09-27
@@ -10,7 +19,17 @@
)
+# pylint: disable=C0116
class GrainsTest(unittest.TestCase):
+ """
+ Unit tests for the GrainsTest class, covering the square and
+ total functions from the grains module.
+
+ Verifies correct grain counts for specific chessboard squares,
+ total grain calculation, and proper exception handling for
+ invalid input values.
+ """
+
def test_grains_on_square_1(self):
self.assertEqual(square(1), 1)
@@ -48,19 +67,22 @@ def test_square_0_is_invalid(self):
with self.assertRaises(ValueError) as err:
square(0)
self.assertEqual(type(err.exception), ValueError)
- self.assertEqual(err.exception.args[0], "square must be between 1 and 64")
+ self.assertEqual(err.exception.args[0],
+ "square must be between 1 and 64")
def test_negative_square_is_invalid(self):
with self.assertRaises(ValueError) as err:
square(-1)
self.assertEqual(type(err.exception), ValueError)
- self.assertEqual(err.exception.args[0], "square must be between 1 and 64")
+ self.assertEqual(err.exception.args[0],
+ "square must be between 1 and 64")
def test_square_greater_than_64_is_invalid(self):
with self.assertRaises(ValueError) as err:
square(65)
self.assertEqual(type(err.exception), ValueError)
- self.assertEqual(err.exception.args[0], "square must be between 1 and 64")
+ self.assertEqual(err.exception.args[0],
+ "square must be between 1 and 64")
def test_returns_the_total_number_of_grains_on_the_board(self):
self.assertEqual(total(), 18446744073709551615)
diff --git a/guidos-gorgeous-lasagna/lasagna.py b/guidos-gorgeous-lasagna/lasagna.py
index 595a253..577645a 100644
--- a/guidos-gorgeous-lasagna/lasagna.py
+++ b/guidos-gorgeous-lasagna/lasagna.py
@@ -13,6 +13,7 @@
PREPARATION_TIME: int = 2
+# pylint: disable=R0801
def bake_time_remaining(elapsed_bake_time: int) -> int:
"""
Calculate the bake time remaining.
@@ -54,7 +55,8 @@ def elapsed_time_in_minutes(number_of_layers: int,
:param number_of_layers: The number of layers added to the lasagna.
:type number_of_layers: int
- :param elapsed_bake_time: The number of minutes the lasagna has spent baking in the oven already.
+ :param elapsed_bake_time: The number of minutes the lasagna has
+ spent baking in the oven already.
:type elapsed_bake_time: int
:return: Elapsed time in minutes.
:rtype: int
diff --git a/guidos-gorgeous-lasagna/lasagna_test.py b/guidos-gorgeous-lasagna/lasagna_test.py
index 4066aa8..6a21967 100644
--- a/guidos-gorgeous-lasagna/lasagna_test.py
+++ b/guidos-gorgeous-lasagna/lasagna_test.py
@@ -1,9 +1,22 @@
+# pylint: disable=C0301
+"""
+Unit tests for Guido's gorgeous lasagna module functions and constants.
+
+Covers presence and values of EXPECTED_BAKE_TIME, and correctness of
+bake_time_remaining, preparation_time_in_minutes, and elapsed_time_in_minutes.
+Also checks that all required docstrings are present.
+
+Raises informative import errors to help identify naming issues during testing.
+"""
+
import unittest
import pytest
-# For this first exercise, it is really important to be clear about how we are importing names for tests.
-# To that end, we are putting a try/catch around imports and throwing specific messages to help students
-# decode that they need to create and title their constants and functions in a specific way.
+# For this first exercise, it is really important to be clear about
+# how we are importing names for tests.To that end, we are putting a
+# try/catch around imports and throwing specific messages to help
+# students decode that they need to create and title their constants
+# and functions in a specific way.
try:
from lasagna import (EXPECTED_BAKE_TIME,
bake_time_remaining,
@@ -17,20 +30,26 @@
if 'EXPECTED_BAKE_TIME' in item_name:
# pylint: disable=raise-missing-from
- raise ImportError(f'\n\nMISSING CONSTANT --> \nWe can not find or import the constant {item_name} in your'
- " 'lasagna.py' file.\nDid you misname or forget to define it?") from None
- else:
- item_name = item_name[:-1] + "()'"
- # pylint: disable=raise-missing-from
- raise ImportError("\n\nMISSING FUNCTION --> In your 'lasagna.py' file, we can not find or import the"
- f' function named {item_name}. \nDid you misname or forget to define it?') from None
+ raise ImportError(f'\n\nMISSING CONSTANT --> \nWe can not find '
+ f'or import the constant {item_name} in your'
+ " 'lasagna.py' file.\nDid you misname or forget "
+ "to define it?") from None
+ item_name = item_name[:-1] + "()'"
+ # pylint: disable=raise-missing-from
+ raise ImportError("\n\nMISSING FUNCTION --> In your 'lasagna.py' "
+ "file, we can not find or import the"
+ f' function named {item_name}. \nDid you misname '
+ f'or forget to define it?') from None
+
+# pylint: disable=C0301, C0116
# Here begins the formal test cases for the exercise.
class LasagnaTest(unittest.TestCase):
+ """Unit tests for lasagna-related functionality."""
@pytest.mark.task(taskno=1)
- def test_EXPECTED_BAKE_TIME(self):
+ def test_expected_bake_time(self):
failure_msg = 'Expected a constant of EXPECTED_BAKE_TIME with a value of 40.'
self.assertEqual(EXPECTED_BAKE_TIME, 40, msg=failure_msg)
@@ -39,11 +58,16 @@ def test_bake_time_remaining(self):
input_data = [1, 2, 5, 10, 15, 23, 33, 39]
result_data = [39, 38, 35, 30, 25, 17, 7, 1]
- for variant, (time, expected) in enumerate(zip(input_data, result_data), start=1):
- with self.subTest(f'variation #{variant}', time=time, expected=expected):
+ for variant, (time, expected) in enumerate(
+ zip(input_data, result_data),
+ start=1):
+ with self.subTest(f'variation #{variant}',
+ time=time,
+ expected=expected):
actual_result = bake_time_remaining(time)
- failure_msg = (f'Called bake_time_remaining({time}). '
- f'The function returned {actual_result}, but the tests '
+ failure_msg = (f'Called bake_time_remaining({time}). '
+ f'The function returned {actual_result}, '
+ f'but the tests '
f'expected {expected} as the remaining bake time.')
self.assertEqual(actual_result, expected, msg=failure_msg)
@@ -53,11 +77,16 @@ def test_preparation_time_in_minutes(self):
input_data = [1, 2, 5, 8, 11, 15]
result_data = [2, 4, 10, 16, 22, 30]
- for variant, (layers, expected) in enumerate(zip(input_data, result_data), start=1):
- with self.subTest(f'variation #{variant}', layers=layers, expected=expected):
+ for variant, (layers, expected) in enumerate(
+ zip(input_data, result_data),
+ start=1):
+ with self.subTest(f'variation #{variant}',
+ layers=layers,
+ expected=expected):
actual_result = preparation_time_in_minutes(layers)
failure_msg = (f'Called preparation_time_in_minutes({layers}). '
- f'The function returned {actual_result}, but the tests '
+ f'The function returned {actual_result}, '
+ f'but the tests '
f'expected {expected} as the preparation time.')
self.assertEqual(actual_result, expected, msg=failure_msg)
@@ -68,8 +97,13 @@ def test_elapsed_time_in_minutes(self):
time_data = (3, 7, 8, 4, 15, 20)
result_data = [5, 11, 18, 20, 37, 50]
- for variant, (layers, time, expected) in enumerate(zip(layer_data, time_data, result_data), start=1):
- with self.subTest(f'variation #{variant}', layers=layers, time=time, expected=expected):
+ for variant, (layers, time, expected) in enumerate(
+ zip(layer_data, time_data, result_data),
+ start=1):
+ with self.subTest(f'variation #{variant}',
+ layers=layers,
+ time=time,
+ expected=expected):
actual_result = elapsed_time_in_minutes(layers, time)
failure_msg = (f'Called elapsed_time_in_minutes({layers}, {time}). '
f'The function returned {actual_result}, but the tests '
diff --git a/hello-world/hello_world.py b/hello-world/hello_world.py
index d695ea1..4d0815d 100644
--- a/hello-world/hello_world.py
+++ b/hello-world/hello_world.py
@@ -1,2 +1,3 @@
+# pylint: disable=C0116, C0114
def hello():
return 'Hello, World!'
diff --git a/hello-world/hello_world_test.py b/hello-world/hello_world_test.py
index c2d1154..811bf6a 100644
--- a/hello-world/hello_world_test.py
+++ b/hello-world/hello_world_test.py
@@ -1,3 +1,12 @@
+# pylint: disable=C0301
+"""
+Unit tests for the 'hello' function in hello_world.py.
+
+Ensures the function returns 'Hello, World!' as expected.
+Provides informative error messages if the function is
+missing or incorrectly implemented.
+"""
+
# These tests are auto-generated with test data from:
# https://github.com/exercism/problem-specifications/tree/main/exercises/hello-world/canonical-data.json
# File last updated on 2023-07-19
@@ -24,8 +33,14 @@
) from None
+# pylint: disable=C0116
class HelloWorldTest(unittest.TestCase):
+ """
+ Test case class for verifying HelloWorld functionality using unittest.
+ """
+
def test_say_hi(self):
- msg = ("\n\nThis test expects a return of the string 'Hello, World!' \nDid you use print('Hello, World!') by "
- "mistake?")
+ msg = ("\n\nThis test expects a return of the string "
+ "'Hello, World!' \nDid you use print('Hello, World!') "
+ "by mistake?")
self.assertEqual(hello(), "Hello, World!", msg=msg)
diff --git a/leap/leap_test.py b/leap/leap_test.py
index 6a1d732..42e425a 100644
--- a/leap/leap_test.py
+++ b/leap/leap_test.py
@@ -1,3 +1,10 @@
+# pylint: disable=C0301
+"""
+Unit tests for the leap_year function, verifying correct leap
+year logic for various cases based on canonical Exercism
+problem specifications.
+"""
+
# These tests are auto-generated with test data from:
# https://github.com/exercism/problem-specifications/tree/main/exercises/leap/canonical-data.json
# File last updated on 2023-07-19
@@ -9,7 +16,10 @@
)
+# pylint: disable=C0116
class LeapTest(unittest.TestCase):
+ """Unit tests for verifying leap year calculation functionality."""
+
def test_year_not_divisible_by_4_in_common_year(self):
self.assertIs(leap_year(2015), False)
diff --git a/little-sisters-vocab/strings_test.py b/little-sisters-vocab/strings_test.py
index b13d4e9..1b2999c 100644
--- a/little-sisters-vocab/strings_test.py
+++ b/little-sisters-vocab/strings_test.py
@@ -1,3 +1,12 @@
+# pylint: disable=C0301
+"""
+Unit tests for the string manipulation functions in the 'strings' module.
+
+Tests adding the 'un' prefix, transforming word groups with a prefix, removing
+the '-ness' suffix, and converting adjectives to verbs within sentences.
+Uses pytest markers for task organization and detailed error messages for clarity.
+"""
+
import unittest
import pytest
from strings import (add_prefix_un,
@@ -6,28 +15,41 @@
adjective_to_verb)
+# pylint: disable=C0116
class LittleSistersVocabTest(unittest.TestCase):
+ """
+ Unit tests for vocabulary string manipulation functions in
+ the 'strings' module.
+ """
@pytest.mark.task(taskno=1)
def test_add_prefix_un(self):
- input_data = ['happy', 'manageable', 'fold', 'eaten', 'avoidable', 'usual']
+ input_data = ['happy', 'manageable', 'fold',
+ 'eaten', 'avoidable', 'usual']
result_data = [f'un{item}' for item in input_data]
- for variant, (word, expected) in enumerate(zip(input_data, result_data), start=1):
- with self.subTest(f'variation #{variant}', word=word, expected=expected):
-
+ for variant, (word, expected) in enumerate(
+ zip(input_data, result_data), start=1):
+ with self.subTest(f'variation #{variant}',
+ word=word,
+ expected=expected):
actual_result = add_prefix_un(word)
error_message = (f'Called add_prefix_un("{word}"). '
- f'The function returned "{actual_result}", but the '
- f'tests expected "{expected}" after adding "un" as a prefix.')
+ f'The function returned "{actual_result}", '
+ f'but the '
+ f'tests expected "{expected}" after adding '
+ f'"un" as a prefix.')
self.assertEqual(actual_result, expected, msg=error_message)
@pytest.mark.task(taskno=2)
def test_make_word_groups_en(self):
- input_data = ['en', 'circle', 'fold', 'close', 'joy', 'lighten', 'tangle', 'able', 'code', 'culture']
- expected = ('en :: encircle :: enfold :: enclose :: enjoy :: enlighten ::'
- ' entangle :: enable :: encode :: enculture')
+ input_data = ['en', 'circle', 'fold', 'close',
+ 'joy', 'lighten', 'tangle', 'able',
+ 'code', 'culture']
+ expected = ('en :: encircle :: enfold :: enclose :: '
+ 'enjoy :: enlighten ::'
+ ' entangle :: enable :: encode :: enculture')
actual_result = make_word_groups(input_data)
error_message = (f'Called make_word_groups({input_data}). '
@@ -39,10 +61,12 @@ def test_make_word_groups_en(self):
@pytest.mark.task(taskno=2)
def test_make_word_groups_pre(self):
- input_data = ['pre', 'serve', 'dispose', 'position', 'requisite', 'digest',
- 'natal', 'addressed', 'adolescent', 'assumption', 'mature', 'compute']
- expected = ('pre :: preserve :: predispose :: preposition :: prerequisite :: '
- 'predigest :: prenatal :: preaddressed :: preadolescent :: preassumption :: '
+ input_data = ['pre', 'serve', 'dispose', 'position',
+ 'requisite', 'digest', 'natal', 'addressed',
+ 'adolescent', 'assumption', 'mature', 'compute']
+ expected = ('pre :: preserve :: predispose :: preposition :: '
+ 'prerequisite :: predigest :: prenatal :: '
+ 'preaddressed :: preadolescent :: preassumption :: '
'premature :: precompute')
actual_result = make_word_groups(input_data)
@@ -55,7 +79,8 @@ def test_make_word_groups_pre(self):
@pytest.mark.task(taskno=2)
def test_make_word_groups_auto(self):
- input_data = ['auto', 'didactic', 'graph', 'mate', 'chrome', 'centric', 'complete',
+ input_data = ['auto', 'didactic', 'graph', 'mate',
+ 'chrome', 'centric', 'complete',
'echolalia', 'encoder', 'biography']
expected = ('auto :: autodidactic :: autograph :: automate :: autochrome :: '
'autocentric :: autocomplete :: autoecholalia :: autoencoder :: '
@@ -71,8 +96,9 @@ def test_make_word_groups_auto(self):
@pytest.mark.task(taskno=2)
def test_make_words_groups_inter(self):
- input_data = ['inter', 'twine', 'connected', 'dependent', 'galactic', 'action',
- 'stellar', 'cellular', 'continental', 'axial', 'operative', 'disciplinary']
+ input_data = ['inter', 'twine', 'connected', 'dependent',
+ 'galactic', 'action', 'stellar', 'cellular',
+ 'continental', 'axial', 'operative', 'disciplinary']
expected = ('inter :: intertwine :: interconnected :: interdependent :: '
'intergalactic :: interaction :: interstellar :: intercellular :: '
'intercontinental :: interaxial :: interoperative :: interdisciplinary')
@@ -87,16 +113,22 @@ def test_make_words_groups_inter(self):
@pytest.mark.task(taskno=3)
def test_remove_suffix_ness(self):
- input_data = ['heaviness', 'sadness', 'softness', 'crabbiness', 'lightness', 'artiness', 'edginess']
- result_data = ['heavy', 'sad', 'soft', 'crabby', 'light', 'arty', 'edgy']
-
- for variant, (word, expected) in enumerate(zip(input_data, result_data), start=1):
- with self.subTest(f'variation #{variant}', word=word, expected=expected):
+ input_data = ['heaviness', 'sadness', 'softness',
+ 'crabbiness', 'lightness', 'artiness',
+ 'edginess']
+ result_data = ['heavy', 'sad', 'soft', 'crabby',
+ 'light', 'arty', 'edgy']
+
+ for variant, (word, expected) in enumerate(
+ zip(input_data, result_data), start=1):
+ with self.subTest(f'variation #{variant}',
+ word=word,
+ expected=expected):
actual_result = remove_suffix_ness(word)
error_message = (f'Called remove_suffix_ness("{word}"). '
f'The function returned "{actual_result}", '
- f'but the tests expected "{expected}" after the '
- 'suffix was removed.')
+ f'but the tests expected "{expected}" '
+ f'after the suffix was removed.')
self.assertEqual(actual_result, expected, msg=error_message)
@@ -113,13 +145,20 @@ def test_adjective_to_verb(self):
'The black oil got on the white dog.']
index_data = [-2, -1, 3, 3, -2, -3, 5, 2, 1]
result_data = ['brighten', 'darken', 'harden', 'soften',
- 'lighten', 'dampen', 'shorten', 'weaken', 'blacken']
-
- for variant, (sentence, index, expected) in enumerate(zip(input_data, index_data, result_data), start=1):
- with self.subTest(f'variation #{variant}', sentence=sentence, index=index, expected=expected):
+ 'lighten', 'dampen', 'shorten', 'weaken',
+ 'blacken']
+
+ for variant, (sentence, index, expected) in enumerate(
+ zip(input_data, index_data, result_data), start=1):
+ with self.subTest(f'variation #{variant}',
+ sentence=sentence,
+ index=index,
+ expected=expected):
actual_result = adjective_to_verb(sentence, index)
- error_message = (f'Called adjective_to_verb("{sentence}", {index}). '
- f'The function returned "{actual_result}", but the tests '
+ error_message = (f'Called adjective_to_verb("'
+ f'{sentence}", {index}). '
+ f'The function returned "{actual_result}", '
+ f'but the tests '
f'expected "{expected}" as the verb for '
f'the word at index {index}.')
diff --git a/making-the-grade/loops.py b/making-the-grade/loops.py
index afe3994..06668b7 100644
--- a/making-the-grade/loops.py
+++ b/making-the-grade/loops.py
@@ -18,7 +18,7 @@ def count_failed_students(student_scores: list) -> int:
:param student_scores: list - containing int student scores.
:return: int - count of student scores at or below 40.
"""
- return len([score for score in student_scores if score <= 40.0])
+ return len([score for score in student_scores if score <= 40.0]) # pylint: disable=R0801
def above_threshold(student_scores: list,
@@ -33,7 +33,7 @@ def above_threshold(student_scores: list,
:param threshold: int - threshold to cross to be the "best" score.
:return: list - of integer scores that are at or above the "best" threshold.
"""
- return [score for score in student_scores if score >= threshold]
+ return [score for score in student_scores if score >= threshold] # pylint: disable=R0801
def letter_grades(highest: int) -> list:
@@ -54,6 +54,7 @@ def letter_grades(highest: int) -> list:
return [41 + i * interval for i in range(4)]
+# pylint: disable=R0801
def student_ranking(student_scores: list,
student_names: list) -> list[str]:
"""
@@ -67,6 +68,7 @@ def student_ranking(student_scores: list,
range(1, len(student_scores) + 1), student_scores, student_names)]
+# pylint: disable=R0801
def perfect_score(student_info: list) -> list:
"""
Create a list that contains the name and grade of the first
diff --git a/making-the-grade/loops_test.py b/making-the-grade/loops_test.py
index 598e2b0..47d6a34 100644
--- a/making-the-grade/loops_test.py
+++ b/making-the-grade/loops_test.py
@@ -1,3 +1,12 @@
+# pylint: disable=C0301
+"""
+Test suite for validating functions from the 'loops' module related
+to student exam score calculations, including rounding scores,
+counting failed students, filtering scores above a threshold,
+generating letter grade cutoffs, ranking students, and identifying
+perfect scores.
+"""
+
import unittest
import pytest
@@ -10,37 +19,52 @@
perfect_score)
+# pylint: disable=C0116
class MakingTheGradeTest(unittest.TestCase):
+ """
+ Test case class for verifying functions in the making-the-grade module.
+ """
@pytest.mark.task(taskno=1)
def test_round_scores(self):
- # Because we the input list can be mutated, the test data has been created
- # as tuples, which we then convert to a list when the test runs.
- # this makes accurate error messages easier to create.
+ # Because we the input list can be mutated, the test data has
+ # been created as tuples, which we then convert to a list when
+ # the test runs. This makes accurate error messages easier to create.
test_data = [tuple(),
(.5,),
(1.5,),
- (90.33, 40.5, 55.44, 70.05, 30.55, 25.45, 80.45, 95.3, 38.7, 40.3),
- (50, 36.03, 76.92, 40.7, 43, 78.29, 63.58, 91, 28.6, 88.0)]
+ (90.33, 40.5, 55.44, 70.05, 30.55, 25.45,
+ 80.45, 95.3, 38.7, 40.3),
+ (50, 36.03, 76.92, 40.7, 43, 78.29, 63.58,
+ 91, 28.6, 88.0)]
result_data = [[],
[0],
[2],
[90, 40, 55, 70, 31, 25, 80, 95, 39, 40],
[50, 36, 77, 41, 43, 78, 64, 91, 29, 88]]
- for variant, (student_scores, expected) in enumerate(zip(test_data, result_data), start=1):
- with self.subTest(f'variation #{variant}', student_scores=student_scores, expected=expected):
+ for variant, (student_scores, expected) in enumerate(
+ zip(test_data, result_data), start=1):
+ with self.subTest(f'variation #{variant}',
+ student_scores=student_scores,
+ expected=expected):
- # Because the test_input is a tuple, it has to be converted to a list for the function call.
+ # Because the test_input is a tuple, it has to be converted
+ # to a list for the function call.
actual_result = round_scores(list(student_scores))
error_message = (f'Called round_scores({list(student_scores)}). '
- f'The function returned {sorted(actual_result)} after sorting, but '
- f'the tests expected {sorted(expected)} after sorting. '
+ f'The function returned {sorted(actual_result)} '
+ f'after sorting, but '
+ f'the tests expected {sorted(expected)} after '
+ f'sorting. '
f'One or more scores were rounded incorrectly.')
# everything is sorted for easier comparison.
- self.assertEqual(sorted(actual_result), sorted(expected), msg=error_message)
+ self.assertEqual(
+ sorted(actual_result),
+ sorted(expected),
+ msg=error_message)
@pytest.mark.task(taskno=2)
def test_count_failed_students(self):
@@ -48,7 +72,8 @@ def test_count_failed_students(self):
[40, 40, 35, 70, 30, 41, 90]]
result_data = [0,4]
- for variant, (student_scores, expected) in enumerate(zip(test_data, result_data), start=1):
+ for variant, (student_scores, expected) in enumerate(
+ zip(test_data, result_data), start=1):
with self.subTest(f'variation #{variant}',
student_scores=student_scores,
expected=expected):
@@ -95,8 +120,11 @@ def test_letter_grades(self):
[41, 54, 67, 80],
[41, 51, 61, 71]]
- for variant, (highest, expected) in enumerate(zip(test_data, result_data), start=1):
- with self.subTest(f'variation #{variant}', highest=highest, expected=expected):
+ for variant, (highest, expected) in enumerate(
+ zip(test_data, result_data), start=1):
+ with self.subTest(f'variation #{variant}',
+ highest=highest,
+ expected=expected):
actual_result = letter_grades(highest)
error_message = (f'Called letter_grades({highest}). '
f'The function returned {actual_result}, but '
@@ -130,11 +158,14 @@ def test_student_ranking(self):
@pytest.mark.task(taskno=6)
def test_perfect_score(self):
test_data = [
- [['Joci', 100], ['Vlad', 100], ['Raiana', 100], ['Alessandro', 100]],
+ [['Joci', 100], ['Vlad', 100], ['Raiana', 100],
+ ['Alessandro', 100]],
[['Jill', 30], ['Paul', 73]],
[],
- [['Rui', 60], ['Joci', 58], ['Sara', 91], ['Kora', 93], ['Alex', 42],
- ['Jan', 81], ['Lilliana', 40], ['John', 60], ['Bern', 28], ['Vlad', 55]],
+ [['Rui', 60], ['Joci', 58], ['Sara', 91], ['Kora', 93],
+ ['Alex', 42],
+ ['Jan', 81], ['Lilliana', 40], ['John', 60], ['Bern', 28],
+ ['Vlad', 55]],
[['Yoshi', 52], ['Jan', 86], ['Raiana', 100], ['Betty', 60],
['Joci', 100], ['Kora', 81], ['Bern', 41], ['Rose', 94]]
@@ -143,9 +174,12 @@ def test_perfect_score(self):
result_data = [['Joci', 100],[], [], [], ['Raiana', 100]]
- for variant, (student_info, expected) in enumerate(zip(test_data, result_data), start=1):
+ for variant, (student_info, expected) in enumerate(
+ zip(test_data, result_data), start=1):
- with self.subTest(f'variation #{variant}', student_info=student_info, expected=expected):
+ with self.subTest(f'variation #{variant}',
+ student_info=student_info,
+ expected=expected):
actual_result = perfect_score(student_info)
error_message = (f'Called perfect_score({student_info}). '
f'The function returned {actual_result}, but '
diff --git a/meltdown-mitigation/conditionals.py b/meltdown-mitigation/conditionals.py
index 87b831b..7029096 100644
--- a/meltdown-mitigation/conditionals.py
+++ b/meltdown-mitigation/conditionals.py
@@ -1,8 +1,10 @@
"""Functions to prevent a nuclear meltdown."""
+# pylint: disable=C0301
def is_criticality_balanced(temperature, neutrons_emitted) -> bool:
- """Verify criticality is balanced.
+ """
+ Verify criticality is balanced.
:param temperature: int or float - temperature value in kelvin.
:param neutrons_emitted: int or float - number of neutrons emitted per second.
@@ -17,7 +19,8 @@ def is_criticality_balanced(temperature, neutrons_emitted) -> bool:
def reactor_efficiency(voltage, current, theoretical_max_power) -> str:
- """Assess reactor efficiency zone.
+ """
+ Assess reactor efficiency zone.
:param voltage: int or float - voltage value.
:param current: int or float - current value.
@@ -40,16 +43,19 @@ def reactor_efficiency(voltage, current, theoretical_max_power) -> str:
if efficiency < 30:
return 'black'
- elif 30 <= efficiency < 60:
+
+ if 30 <= efficiency < 60:
return 'red'
- elif 60 <= efficiency < 80:
+
+ if 60 <= efficiency < 80:
return 'orange'
return 'green'
def fail_safe(temperature, neutrons_produced_per_second, threshold) -> str:
- """Assess and return status code for the reactor.
+ """
+ Assess and return status code for the reactor.
:param temperature: int or float - value of the temperature in kelvin.
:param neutrons_produced_per_second: int or float - neutron flux.
@@ -65,7 +71,8 @@ def fail_safe(temperature, neutrons_produced_per_second, threshold) -> str:
if thr_percent - 10 <= current_state <= thr_percent + 10:
return 'NORMAL'
- elif current_state < thr_percent - 10:
+
+ if current_state < thr_percent - 10:
return 'LOW'
return 'DANGER'
diff --git a/meltdown-mitigation/conditionals_test.py b/meltdown-mitigation/conditionals_test.py
index 5e48ca3..f0a4b8e 100644
--- a/meltdown-mitigation/conditionals_test.py
+++ b/meltdown-mitigation/conditionals_test.py
@@ -1,3 +1,11 @@
+# pylint: disable=C0301
+"""
+Unit tests for the meltdown mitigation functions, verifying
+correct behavior for criticality balance, reactor efficiency,
+and fail safe status using a variety of boundary and
+representative input values.
+"""
+
import unittest
import pytest
from conditionals import (is_criticality_balanced,
@@ -5,16 +13,16 @@
fail_safe)
+# pylint: disable=C0116
class MeltdownMitigationTest(unittest.TestCase):
- """Test cases for Meltdown mitigation exercise.
- """
+ """Test cases for Meltdown mitigation exercise."""
@pytest.mark.task(taskno=1)
def test_is_criticality_balanced(self):
- """Testing border cases around typical points.
+ """
+ Testing border cases around typical points.
T, n == (800, 500), (625, 800), (500, 1000), etc.
-
"""
test_data = ((750, 650, True), (799, 501, True), (500, 600, True),
@@ -26,13 +34,18 @@ def test_is_criticality_balanced(self):
for variant, data in enumerate(test_data, start=1):
temp, neutrons_emitted, expected = data
- with self.subTest(f'variation #{variant}', temp=temp, neutrons_emitted=neutrons_emitted, expected=expected):
+ with self.subTest(f'variation #{variant}',
+ temp=temp,
+ neutrons_emitted=neutrons_emitted,
+ expected=expected):
# pylint: disable=assignment-from-no-return
actual_result = is_criticality_balanced(temp, neutrons_emitted)
- failure_message = (f'Called is_criticality_balanced({temp}, {neutrons_emitted}). '
+ failure_message = (f'Called is_criticality_balanced('
+ f'{temp}, {neutrons_emitted}). '
f' The function returned {actual_result}, '
- f'but the test expected {expected} as the return value.')
+ f'but the test expected {expected} as the '
+ f'return value.')
self.assertEqual(actual_result, expected, failure_message)
@@ -49,12 +62,16 @@ def test_reactor_efficiency(self):
for variant, data in enumerate(test_data, start=1):
current, expected = data
- with self.subTest(f'variation #{variant}', voltage=voltage, current=current,
- theoretical_max_power=theoretical_max_power, expected=expected):
+ with self.subTest(f'variation #{variant}',
+ voltage=voltage,
+ current=current,
+ theoretical_max_power=theoretical_max_power,
+ expected=expected):
# pylint: disable=assignment-from-no-return
actual_result = reactor_efficiency(voltage, current, theoretical_max_power)
- failure_message =(f'Called reactor_efficiency({voltage}, {current}, {theoretical_max_power}). '
+ failure_message =(f'Called reactor_efficiency('
+ f'{voltage}, {current}, {theoretical_max_power}). '
f'The function returned {actual_result}, '
f'but the test expected {expected} as the return value.')
@@ -70,12 +87,15 @@ def test_fail_safe(self):
(400, 'LOW'), (1101, 'DANGER'), (1200, 'DANGER'))
for variant, (neutrons_per_second, expected) in enumerate(test_data, start=1):
- with self.subTest(f'variation #{variant}', temp=temp, neutrons_per_second=neutrons_per_second,
+ with self.subTest(f'variation #{variant}',
+ temp=temp,
+ neutrons_per_second=neutrons_per_second,
threshold=threshold, expected=expected):
# pylint: disable=assignment-from-no-return
actual_result = fail_safe(temp, neutrons_per_second, threshold)
- failure_message = (f'Called fail_safe({temp}, {neutrons_per_second}, {threshold}). '
+ failure_message = (f'Called fail_safe('
+ f'{temp}, {neutrons_per_second}, {threshold}). '
f'The function returned {actual_result}, '
f'but the test expected {expected} as the return value.')
diff --git a/requirements.txt b/requirements.txt
index 015de27..ad58882 100644
Binary files a/requirements.txt and b/requirements.txt differ
diff --git a/solutions/python/collatz-conjecture/1/collatz_conjecture.py b/solutions/python/collatz-conjecture/1/collatz_conjecture.py
new file mode 100644
index 0000000..1b7ce6e
--- /dev/null
+++ b/solutions/python/collatz-conjecture/1/collatz_conjecture.py
@@ -0,0 +1,39 @@
+"""
+Collatz Conjecture.
+
+The rules were deceptively simple. Pick any positive integer.
+
+If it's even, divide it by 2.
+If it's odd, multiply it by 3 and add 1.
+Then, repeat these steps with the result, continuing indefinitely.
+
+Curious, you picked number 12 to test and began the journey:
+
+12 ➜ 6 ➜ 3 ➜ 10 ➜ 5 ➜ 16 ➜ 8 ➜ 4 ➜ 2 ➜ 1
+"""
+
+
+def steps(number: int) -> int | ValueError:
+ """
+ Return the number of steps it takes to reach 1 according to
+ the rules of the Collatz Conjecture.
+
+ :param number: any positive integer
+ :type number: int
+ :return: number of steps it takes to reach 1
+ :rtype: int
+ """
+ if number < 1:
+ raise ValueError("Only positive integers are allowed")
+
+ n_steps: int = 0
+ while number > 1:
+ # If it's even, divide it by 2
+ if number % 2 == 0:
+ number = number / 2
+ # If it's odd, multiply it by 3 and add 1
+ else:
+ number = (number * 3) + 1
+ n_steps += 1
+
+ return n_steps
diff --git a/solutions/python/currency-exchange/1/exchange.py b/solutions/python/currency-exchange/1/exchange.py
index 2632aa2..a0eeeac 100644
--- a/solutions/python/currency-exchange/1/exchange.py
+++ b/solutions/python/currency-exchange/1/exchange.py
@@ -1,9 +1,11 @@
"""
Functions for calculating steps in exchanging currency.
-Python numbers documentation: https://docs.python.org/3/library/stdtypes.html#numeric-types-int-float-complex
+Python numbers documentation:
+https://docs.python.org/3/library/stdtypes.html#numeric-types-int-float-complex
-Overview of exchanging currency when travelling: https://www.compareremit.com/money-transfer-tips/guide-to-exchanging-currency-for-overseas-travel/
+Overview of exchanging currency when travelling:
+https://www.compareremit.com/money-transfer-tips/guide-to-exchanging-currency-for-overseas-travel/
"""
@@ -37,7 +39,8 @@ def get_change(budget: float,
def get_value_of_bills(denomination: float,
number_of_bills: float) -> float:
"""
- Return only the total value of the bills (excluding fractional amounts) the booth would give back.
+ Return only the total value of the bills (excluding fractional amounts)
+ the booth would give back.
The total you receive must be divisible by the value of one "bill" or unit,
which can leave behind a fraction or remainder.
@@ -79,7 +82,8 @@ def exchangeable_value(budget: float,
spread: int,
denomination: int) -> int:
"""
- Return the maximum value of the new currency after calculating the *exchange rate* plus the *spread*.
+ Return the maximum value of the new currency after calculating
+ the *exchange rate* plus the *spread*.
:param budget: float - the amount of your money you are planning to exchange.
:param exchange_rate: float - the unit value of the foreign currency.
diff --git a/solutions/python/guidos-gorgeous-lasagna/1/lasagna.py b/solutions/python/guidos-gorgeous-lasagna/1/lasagna.py
index 595a253..a69c70b 100644
--- a/solutions/python/guidos-gorgeous-lasagna/1/lasagna.py
+++ b/solutions/python/guidos-gorgeous-lasagna/1/lasagna.py
@@ -54,7 +54,8 @@ def elapsed_time_in_minutes(number_of_layers: int,
:param number_of_layers: The number of layers added to the lasagna.
:type number_of_layers: int
- :param elapsed_bake_time: The number of minutes the lasagna has spent baking in the oven already.
+ :param elapsed_bake_time: The number of minutes the lasagna has spent
+ baking in the oven already.
:type elapsed_bake_time: int
:return: Elapsed time in minutes.
:rtype: int
diff --git a/square-root/square_root_test.py b/square-root/square_root_test.py
index 8f94940..fcd2bb7 100644
--- a/square-root/square_root_test.py
+++ b/square-root/square_root_test.py
@@ -1,3 +1,10 @@
+# pylint: disable=C0301
+"""
+Unit tests for the square_root function, verifying
+correct calculation of integer square roots for various
+inputs using test data from Exercism.
+"""
+
# These tests are auto-generated with test data from:
# https://github.com/exercism/problem-specifications/tree/main/exercises/square-root/canonical-data.json
# File last updated on 2023-07-19
@@ -9,7 +16,12 @@
)
+# pylint: disable=C0116
class SquareRootTest(unittest.TestCase):
+ """
+ Test suite for verifying the functionality of the square_root function.
+ """
+
def test_root_of_1(self):
self.assertEqual(square_root(1), 1)
diff --git a/triangle/.exercism/config.json b/triangle/.exercism/config.json
new file mode 100644
index 0000000..041bf28
--- /dev/null
+++ b/triangle/.exercism/config.json
@@ -0,0 +1,36 @@
+{
+ "authors": [],
+ "contributors": [
+ "behrtam",
+ "BethanyG",
+ "cmccandless",
+ "Dog",
+ "ikhadykin",
+ "kytrinyx",
+ "lowks",
+ "mpatibandla",
+ "N-Parsons",
+ "Nishant23",
+ "pheanex",
+ "rozuur",
+ "sjakobi",
+ "Stigjb",
+ "tqa236",
+ "xitanggg",
+ "YuriyCherniy"
+ ],
+ "files": {
+ "solution": [
+ "triangle.py"
+ ],
+ "test": [
+ "triangle_test.py"
+ ],
+ "example": [
+ ".meta/example.py"
+ ]
+ },
+ "blurb": "Determine if a triangle is equilateral, isosceles, or scalene.",
+ "source": "The Ruby Koans triangle project, parts 1 & 2",
+ "source_url": "https://web.archive.org/web/20220831105330/http://rubykoans.com"
+}
diff --git a/triangle/.exercism/metadata.json b/triangle/.exercism/metadata.json
new file mode 100644
index 0000000..0b73088
--- /dev/null
+++ b/triangle/.exercism/metadata.json
@@ -0,0 +1 @@
+{"track":"python","exercise":"triangle","id":"6f8c14ada1ce403c975161c5a18528c2","url":"https://exercism.org/tracks/python/exercises/triangle","handle":"myFirstCode","is_requester":true,"auto_approve":false}
\ No newline at end of file
diff --git a/triangle/HELP.md b/triangle/HELP.md
new file mode 100644
index 0000000..2d9233f
--- /dev/null
+++ b/triangle/HELP.md
@@ -0,0 +1,130 @@
+# Help
+
+## Running the tests
+
+We use [pytest][pytest: Getting Started Guide] as our website test runner.
+You will need to install `pytest` on your development machine if you want to run tests for the Python track locally.
+You should also install the following `pytest` plugins:
+
+- [pytest-cache][pytest-cache]
+- [pytest-subtests][pytest-subtests]
+
+Extended information can be found in our website [Python testing guide][Python track tests page].
+
+
+### Running Tests
+
+To run the included tests, navigate to the folder where the exercise is stored using `cd` in your terminal (_replace `{exercise-folder-location}` below with your path_).
+Test files usually end in `_test.py`, and are the same tests that run on the website when a solution is uploaded.
+
+Linux/MacOS
+```bash
+$ cd {path/to/exercise-folder-location}
+```
+
+Windows
+```powershell
+PS C:\Users\foobar> cd {path\to\exercise-folder-location}
+```
+
+
+
+Next, run the `pytest` command in your terminal, replacing `{exercise_test.py}` with the name of the test file:
+
+Linux/MacOS
+```bash
+$ python3 -m pytest -o markers=task {exercise_test.py}
+==================== 7 passed in 0.08s ====================
+```
+
+Windows
+```powershell
+PS C:\Users\foobar> py -m pytest -o markers=task {exercise_test.py}
+==================== 7 passed in 0.08s ====================
+```
+
+
+### Common options
+- `-o` : override default `pytest.ini` (_you can use this to avoid marker warnings_)
+- `-v` : enable verbose output.
+- `-x` : stop running tests on first failure.
+- `--ff` : run failures from previous test before running other test cases.
+
+For additional options, use `python3 -m pytest -h` or `py -m pytest -h`.
+
+
+### Fixing warnings
+
+If you do not use `pytest -o markers=task` when invoking `pytest`, you might receive a `PytestUnknownMarkWarning` for tests that use our new syntax:
+
+```bash
+PytestUnknownMarkWarning: Unknown pytest.mark.task - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/mark.html
+```
+
+To avoid typing `pytest -o markers=task` for every test you run, you can use a `pytest.ini` configuration file.
+We have made one that can be downloaded from the top level of the Python track directory: [pytest.ini][pytest.ini].
+
+You can also create your own `pytest.ini` file with the following content:
+
+```ini
+[pytest]
+markers =
+ task: A concept exercise task.
+```
+
+Placing the `pytest.ini` file in the _root_ or _working_ directory for your Python track exercises will register the marks and stop the warnings.
+More information on pytest marks can be found in the `pytest` documentation on [marking test functions][pytest: marking test functions with attributes] and the `pytest` documentation on [working with custom markers][pytest: working with custom markers].
+
+Information on customizing pytest configurations can be found in the `pytest` documentation on [configuration file formats][pytest: configuration file formats].
+
+
+### Extending your IDE or Code Editor
+
+Many IDEs and code editors have built-in support for using `pytest` and other code quality tools.
+Some community-sourced options can be found on our [Python track tools page][Python track tools page].
+
+[Pytest: Getting Started Guide]: https://docs.pytest.org/en/latest/getting-started.html
+[Python track tools page]: https://exercism.org/docs/tracks/python/tools
+[Python track tests page]: https://exercism.org/docs/tracks/python/tests
+[pytest-cache]:http://pythonhosted.org/pytest-cache/
+[pytest-subtests]:https://github.com/pytest-dev/pytest-subtests
+[pytest.ini]: https://github.com/exercism/python/blob/main/pytest.ini
+[pytest: configuration file formats]: https://docs.pytest.org/en/6.2.x/customize.html#configuration-file-formats
+[pytest: marking test functions with attributes]: https://docs.pytest.org/en/6.2.x/mark.html#raising-errors-on-unknown-marks
+[pytest: working with custom markers]: https://docs.pytest.org/en/6.2.x/example/markers.html#working-with-custom-markers
+
+## Submitting your solution
+
+You can submit your solution using the `exercism submit triangle.py` command.
+This command will upload your solution to the Exercism website and print the solution page's URL.
+
+It's possible to submit an incomplete solution which allows you to:
+
+- See how others have completed the exercise
+- Request help from a mentor
+
+## Need to get help?
+
+If you'd like help solving the exercise, check the following pages:
+
+- The [Python track's documentation](https://exercism.org/docs/tracks/python)
+- The [Python track's programming category on the forum](https://forum.exercism.org/c/programming/python)
+- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
+- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
+
+Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
+
+Below are some resources for getting help if you run into trouble:
+
+- [The PSF](https://www.python.org) hosts Python downloads, documentation, and community resources.
+- [The Exercism Community on Discord](https://exercism.org/r/discord)
+- [Python Community on Discord](https://pythondiscord.com/) is a very helpful and active community.
+- [/r/learnpython/](https://www.reddit.com/r/learnpython/) is a subreddit designed for Python learners.
+- [#python on Libera.chat](https://www.python.org/community/irc/) this is where the core developers for the language hang out and get work done.
+- [Python Community Forums](https://discuss.python.org/)
+- [Free Code Camp Community Forums](https://forum.freecodecamp.org/)
+- [CodeNewbie Community Help Tag](https://community.codenewbie.org/t/help)
+- [Pythontutor](http://pythontutor.com/) for stepping through small code snippets visually.
+
+Additionally, [StackOverflow](http://stackoverflow.com/questions/tagged/python) is a good spot to search for your problem/question to see if it has been answered already.
+ If not - you can always [ask](https://stackoverflow.com/help/how-to-ask) or [answer](https://stackoverflow.com/help/how-to-answer) someone else's question.
\ No newline at end of file
diff --git a/triangle/README.md b/triangle/README.md
new file mode 100644
index 0000000..961c673
--- /dev/null
+++ b/triangle/README.md
@@ -0,0 +1,60 @@
+# Triangle
+
+Welcome to Triangle on Exercism's Python Track.
+If you need help running the tests or submitting your code, check out `HELP.md`.
+
+## Instructions
+
+Determine if a triangle is equilateral, isosceles, or scalene.
+
+An _equilateral_ triangle has all three sides the same length.
+
+An _isosceles_ triangle has at least two sides the same length.
+(It is sometimes specified as having exactly two sides the same length, but for the purposes of this exercise we'll say at least two.)
+
+A _scalene_ triangle has all sides of different lengths.
+
+## Note
+
+For a shape to be a triangle at all, all sides have to be of length > 0, and the sum of the lengths of any two sides must be greater than or equal to the length of the third side.
+
+In equations:
+
+Let `a`, `b`, and `c` be sides of the triangle.
+Then all three of the following expressions must be true:
+
+```text
+a + b ≥ c
+b + c ≥ a
+a + c ≥ b
+```
+
+See [Triangle Inequality][triangle-inequality]
+
+[triangle-inequality]: https://en.wikipedia.org/wiki/Triangle_inequality
+
+## Source
+
+### Contributed to by
+
+- @behrtam
+- @BethanyG
+- @cmccandless
+- @Dog
+- @ikhadykin
+- @kytrinyx
+- @lowks
+- @mpatibandla
+- @N-Parsons
+- @Nishant23
+- @pheanex
+- @rozuur
+- @sjakobi
+- @Stigjb
+- @tqa236
+- @xitanggg
+- @YuriyCherniy
+
+### Based on
+
+The Ruby Koans triangle project, parts 1 & 2 - https://web.archive.org/web/20220831105330/http://rubykoans.com
\ No newline at end of file
diff --git a/triangle/triangle.py b/triangle/triangle.py
new file mode 100644
index 0000000..6537e75
--- /dev/null
+++ b/triangle/triangle.py
@@ -0,0 +1,52 @@
+"""Determine if a triangle is equilateral, isosceles, or scalene."""
+
+
+def equilateral(sides: list) -> bool:
+ """
+ An equilateral triangle has all three sides the same length.
+ """
+ if all_sides_positive(sides) and no_inequality_violation(sides):
+ return sides[0] == sides[1] == sides[2]
+ return False
+
+
+def isosceles(sides: list) -> bool:
+ """
+ An isosceles triangle has at least two sides the same length.
+ (It is sometimes specified as having exactly two sides the same length,
+ but for the purposes of this exercise we'll say at least two.)
+ """
+ if all_sides_positive(sides) and no_inequality_violation(sides):
+ return sides[0] == sides[1] or sides[1] == sides[2] or sides[0] == sides[2]
+ return False
+
+
+def scalene(sides: list) -> bool:
+ """
+ A scalene triangle has all sides of different lengths.
+ """
+ if all_sides_positive(sides) and no_inequality_violation(sides):
+ return sides[0] != sides[1] and sides[1] != sides[2] and sides[0] != sides[2]
+ return False
+
+
+def no_inequality_violation(sides: list) -> bool:
+ """
+ Let a, b, and c be sides of the triangle.
+ Then all three of the following expressions must be true:
+
+ a + b ≥ c
+ b + c ≥ a
+ a + c ≥ b
+ """
+ return (sides[0] + sides[1] >= sides[2] and
+ sides[0] + sides[2] >= sides[1] and
+ sides[2] + sides[1] >= sides[0])
+
+
+def all_sides_positive(sides: list) -> bool:
+ """
+ No zeroes or negative numbers allowed.
+ All triangles should have 3 sides.
+ """
+ return all(side > 0 for side in sides) and len(sides) == 3
diff --git a/triangle/triangle_test.py b/triangle/triangle_test.py
new file mode 100644
index 0000000..9312b90
--- /dev/null
+++ b/triangle/triangle_test.py
@@ -0,0 +1,85 @@
+# pylint: disable=C0116, C0114, C0115
+
+# These tests are auto-generated with test data from:
+# https://github.com/exercism/problem-specifications/tree/main/exercises/triangle/canonical-data.json
+# File last updated on 2023-07-19
+
+import unittest
+
+from triangle import (
+ equilateral,
+ isosceles,
+ scalene,
+)
+
+
+# pylint: disable=C0116, C0114, C0115
+class EquilateralTriangleTest(unittest.TestCase):
+ def test_all_sides_are_equal(self):
+ self.assertIs(equilateral([2, 2, 2]), True)
+
+ def test_any_side_is_unequal(self):
+ self.assertIs(equilateral([2, 3, 2]), False)
+
+ def test_no_sides_are_equal(self):
+ self.assertIs(equilateral([5, 4, 6]), False)
+
+ def test_all_zero_sides_is_not_a_triangle(self):
+ self.assertIs(equilateral([0, 0, 0]), False)
+
+ def test_sides_may_be_floats(self):
+ self.assertIs(equilateral([0.5, 0.5, 0.5]), True)
+
+
+# pylint: disable=C0116
+class IsoscelesTriangleTest(unittest.TestCase):
+ def test_last_two_sides_are_equal(self):
+ self.assertIs(isosceles([3, 4, 4]), True)
+
+ def test_first_two_sides_are_equal(self):
+ self.assertIs(isosceles([4, 4, 3]), True)
+
+ def test_first_and_last_sides_are_equal(self):
+ self.assertIs(isosceles([4, 3, 4]), True)
+
+ def test_equilateral_triangles_are_also_isosceles(self):
+ self.assertIs(isosceles([4, 4, 4]), True)
+
+ def test_no_sides_are_equal(self):
+ self.assertIs(isosceles([2, 3, 4]), False)
+
+ def test_first_triangle_inequality_violation(self):
+ self.assertIs(isosceles([1, 1, 3]), False)
+
+ def test_second_triangle_inequality_violation(self):
+ self.assertIs(isosceles([1, 3, 1]), False)
+
+ def test_third_triangle_inequality_violation(self):
+ self.assertIs(isosceles([3, 1, 1]), False)
+
+ def test_sides_may_be_floats(self):
+ self.assertIs(isosceles([0.5, 0.4, 0.5]), True)
+
+
+# pylint: disable=C0116
+class ScaleneTriangleTest(unittest.TestCase):
+ def test_no_sides_are_equal(self):
+ self.assertIs(scalene([5, 4, 6]), True)
+
+ def test_all_sides_are_equal(self):
+ self.assertIs(scalene([4, 4, 4]), False)
+
+ def test_first_and_second_sides_are_equal(self):
+ self.assertIs(scalene([4, 4, 3]), False)
+
+ def test_first_and_third_sides_are_equal(self):
+ self.assertIs(scalene([3, 4, 3]), False)
+
+ def test_second_and_third_sides_are_equal(self):
+ self.assertIs(scalene([4, 3, 3]), False)
+
+ def test_may_not_violate_triangle_inequality(self):
+ self.assertIs(scalene([7, 3, 2]), False)
+
+ def test_sides_may_be_floats(self):
+ self.assertIs(scalene([0.5, 0.4, 0.6]), True)