From 2bf03e9d87ba50ce43ea21b6ff0cd2ed833f561e Mon Sep 17 00:00:00 2001 From: alvaroof Date: Thu, 28 Dec 2023 10:28:29 +0000 Subject: [PATCH 01/20] Bike Class --- scripts/classes.py | 85 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 scripts/classes.py diff --git a/scripts/classes.py b/scripts/classes.py new file mode 100644 index 0000000..b6721d8 --- /dev/null +++ b/scripts/classes.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +from enum import Enum + + +class Condition(Enum): + NEW = 0 + GOOD = 1 + OKAY = 2 + BAD = 3 + + +class Bike: + def __init__( + self, + description: str = "Really Beautiful", + condition: str = "Perfect Condition", + sale_price: float = 0, + cost: float = 0, + year: int = 2015, + ): + """_summary_ + + :param description: _description_, defaults to "Really Beautiful" + :type description: str, optional + :param condition: _description_, defaults to "Perfect Condition" + :type condition: str, optional + :param sale_price: _description_, defaults to 0 + :type sale_price: float, optional + :param cost: _description_, defaults to 0 + :type cost: float, optional + :param year: _description_, defaults to 2015 + :type year: int, optional + """ + self.description = description + self.condition = condition + self.sale_price = sale_price + self.cost = cost + + self.age = 2023 - year + self.sold = False + self.premium = None + + def update_sale_price(self, sale_price: float): + if self.sold: + raise AttributeError("Cannot update sale price of a sold bike.") + if sale_price <= 0: + raise ValueError("Sale price must be greater than zero.") + self.sale_price = sale_price + + def is_premium(self): + self.premium = "Yes" + + def sell(self) -> float: + self.sold = True + profit = self.sale_price - self.cost + print(f"Sold for a profit: {profit}") + return profit + + def service(self, cost, new_sale_price=None, new_condition=None): + self.cost += cost + if new_sale_price is not None: + self.update_sale_price(new_sale_price) + if new_condition is not None: + self.condition = new_condition + + @staticmethod + def sing_the_bike_song(): + print("Singing the Bike Song") + + +if __name__ == "__main__": + my_bike = Bike( + description="Black and Beautiful", + condition=Condition.BAD, + sale_price=1000, + cost=100, + year=1965, + ) + print(my_bike) + print(type(my_bike)) + print(my_bike.condition) + my_bike.service(cost=100, new_condition=Condition.OKAY, new_sale_price=1200) + print(my_bike.condition) + my_bike.sell() + my_bike.sing_the_bike_song() From 86f3aff75da528e432e1fa67c03e750232933747 Mon Sep 17 00:00:00 2001 From: alvaroof Date: Thu, 28 Dec 2023 13:09:38 +0000 Subject: [PATCH 02/20] Bike Class --- scripts/classes.py | 99 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 13 deletions(-) diff --git a/scripts/classes.py b/scripts/classes.py index b6721d8..4137a98 100644 --- a/scripts/classes.py +++ b/scripts/classes.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +import random +import time from enum import Enum @@ -9,12 +11,19 @@ class Condition(Enum): BAD = 3 +class MethodNowAllowed(Exception): + pass + + class Bike: + num_wheels = 2 + counter = 0 + def __init__( self, - description: str = "Really Beautiful", - condition: str = "Perfect Condition", - sale_price: float = 0, + description: str, + condition: Condition = Condition.GOOD, + sale_price: float = 100, cost: float = 0, year: int = 2015, ): @@ -33,24 +42,29 @@ def __init__( """ self.description = description self.condition = condition - self.sale_price = sale_price + self._sale_price = None # Private self.cost = cost + self._slow_attribute = None self.age = 2023 - year self.sold = False self.premium = None + self.update_sale_price(sale_price) + Bike.counter += 1 def update_sale_price(self, sale_price: float): if self.sold: - raise AttributeError("Cannot update sale price of a sold bike.") + raise MethodNowAllowed("Cannot update sale price of a sold bike.") if sale_price <= 0: raise ValueError("Sale price must be greater than zero.") - self.sale_price = sale_price + self._sale_price = sale_price def is_premium(self): self.premium = "Yes" def sell(self) -> float: + if self.sold: + raise MethodNowAllowed("Cannot update sale price of a sold bike.") self.sold = True profit = self.sale_price - self.cost print(f"Sold for a profit: {profit}") @@ -63,10 +77,69 @@ def service(self, cost, new_sale_price=None, new_condition=None): if new_condition is not None: self.condition = new_condition + @property + def sale_price(self): + return self._sale_price + + @sale_price.setter + def sale_price(self, sale_price: float): + if self.sold: + raise MethodNowAllowed("Cannot update sale price of a sold bike.") + if sale_price <= 0: + raise ValueError("Sale price must be greater than zero.") + self._sale_price = sale_price + + @property + def profit(self): + return self.sale_price - self.cost + + @property # Used when an attribute is computationally intensive to calculate. Cache version is accessible + def slow_attribute(self): + if self._slow_attribute is not None: + print("Slow Attribute was already cached") + return self._slow_attribute + else: + print("Calculating Slow Attribute") + time.sleep(5) + self._slow_attribute = "Set" + return self._slow_attribute + @staticmethod def sing_the_bike_song(): print("Singing the Bike Song") + def __add__(self, other): + if isinstance(other, Bike): + self.cost += other.cost + + def __del__(self): + Bike.counter -= 1 + print("Bike deleted") + + def __str__(self): + """Called when str() or print()""" + return f"{self.description}: ${self.sale_price}" + + def __repr__(self): + return f"Bike({self.description}, {self.condition}, {self.sale_price}, {self.cost})" + + @staticmethod + def get_test_bike(): + return Bike(condition=Condition.GOOD, sale_price=1000, cost=0, description=Bike.__name__) + + @classmethod + def get_test_object(cls): + return cls( + condition=random.choice(list(Condition)), + sale_price=1000, + cost=0, + description=f"{cls.__name__}", + ) + + +class Unicycle(Bike): + num_wheels = 1 + if __name__ == "__main__": my_bike = Bike( @@ -76,10 +149,10 @@ def sing_the_bike_song(): cost=100, year=1965, ) - print(my_bike) - print(type(my_bike)) - print(my_bike.condition) - my_bike.service(cost=100, new_condition=Condition.OKAY, new_sale_price=1200) - print(my_bike.condition) - my_bike.sell() - my_bike.sing_the_bike_song() + test_unicycle = Unicycle.get_test_object() + test_bike = Bike.get_test_bike() + print(test_unicycle) + print(test_bike) + print([test_unicycle, test_bike]) + print(my_bike.slow_attribute) + print(my_bike.slow_attribute) From 02907966003a10b26fed5bf56e3033222cfa7f30 Mon Sep 17 00:00:00 2001 From: alvaroof Date: Thu, 28 Dec 2023 19:10:55 +0000 Subject: [PATCH 03/20] OOP Course from Oreilly finished. --- .../__pycache__/classes.cpython-39.pyc | Bin 0 -> 5325 bytes .../__pycache__/game_refactor.cpython-39.pyc | Bin 0 -> 1404 bytes scripts/oop_course/classes.py | 165 ++++++++++++++++++ scripts/oop_course/game_procedural.py | 16 ++ scripts/oop_course/game_refactor.py | 45 +++++ 5 files changed, 226 insertions(+) create mode 100644 scripts/oop_course/__pycache__/classes.cpython-39.pyc create mode 100644 scripts/oop_course/__pycache__/game_refactor.cpython-39.pyc create mode 100644 scripts/oop_course/classes.py create mode 100644 scripts/oop_course/game_procedural.py create mode 100644 scripts/oop_course/game_refactor.py diff --git a/scripts/oop_course/__pycache__/classes.cpython-39.pyc b/scripts/oop_course/__pycache__/classes.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..405a74ea813d404daf162c70b80643077645622f GIT binary patch literal 5325 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 b#C__2JguYq4ZR=m%q-`RROd?PR3v`^V#pL0 literal 0 HcmV?d00001 diff --git a/scripts/oop_course/classes.py b/scripts/oop_course/classes.py new file mode 100644 index 0000000..04792ce --- /dev/null +++ b/scripts/oop_course/classes.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +import random +import time +from enum import Enum + + +class Condition(Enum): + NEW = 0 + GOOD = 1 + OKAY = 2 + BAD = 3 + + +class MethodNowAllowed(Exception): + pass + + +class Bike: + num_wheels = 2 + counter = 0 + + def __init__( + self, + description: str, + condition: Condition = Condition.GOOD, + sale_price: float = 100, + cost: float = 0, + year: int = 2015, + ): + """_summary_ + + :param description: _description_, defaults to "Really Beautiful" + :type description: str, optional + :param condition: _description_, defaults to "Perfect Condition" + :type condition: str, optional + :param sale_price: _description_, defaults to 0 + :type sale_price: float, optional + :param cost: _description_, defaults to 0 + :type cost: float, optional + :param year: _description_, defaults to 2015 + :type year: int, optional + """ + self.description = description + self.condition = condition + self._sale_price = None # Private + self.cost = cost + self._slow_attribute = None + + self.age = 2023 - year + self.sold = False + self.premium = None + self.update_sale_price(sale_price) + Bike.counter += 1 + + def update_sale_price(self, sale_price: float): + if self.sold: + raise MethodNowAllowed("Cannot update sale price of a sold bike.") + if sale_price <= 0: + raise ValueError("Sale price must be greater than zero.") + self._sale_price = sale_price + + def is_premium(self): + self.premium = "Yes" + + def sell(self) -> float: + if self.sold: + raise MethodNowAllowed("Cannot update sale price of a sold bike.") + self.sold = True + profit = self.sale_price - self.cost + print(f"Sold for a profit: {profit}") + return profit + + def service(self, cost, new_sale_price=None, new_condition=None): + self.cost += cost + if new_sale_price is not None: + self.update_sale_price(new_sale_price) + if new_condition is not None: + self.condition = new_condition + + @property + def sale_price(self): + return self._sale_price + + @sale_price.setter + def sale_price(self, sale_price: float): + if self.sold: + raise MethodNowAllowed("Cannot update sale price of a sold bike.") + if sale_price <= 0: + raise ValueError("Sale price must be greater than zero.") + self._sale_price = sale_price + + @property + def profit(self): + return self.sale_price - self.cost + + @property # Used when an attribute is computationally intensive to calculate. Cache version is accessible + def slow_attribute(self): + if self._slow_attribute is not None: + print("Slow Attribute was already cached") + return self._slow_attribute + else: + print("Calculating Slow Attribute") + time.sleep(1) + self._slow_attribute = "Set" + return self._slow_attribute + + @staticmethod + def sing_the_bike_song(): + print("Singing the Bike Song") + + def __add__(self, other): + if isinstance(other, Bike): + self.cost += other.cost + + def __del__(self): + Bike.counter -= 1 + print("Bike deleted") + + def __str__(self): + """Called when str() or print()""" + return f"{self.description}: ${self.sale_price}" + + def __repr__(self): + return f"Bike({self.description}, {self.condition}, {self.sale_price}, {self.cost})" + + @staticmethod + def get_test_bike(): + return Bike(condition=Condition.GOOD, sale_price=1000, cost=0, description=Bike.__name__) + + @classmethod + def get_test_object(cls): + return cls( + condition=random.choice(list(Condition)), + sale_price=1000, + cost=0, + description=f"{cls.__name__}", + ) + + def child_method(self): + raise NotImplementedError("Method not implemented") + + +class Unicycle(Bike): + num_wheels = 1 + + def child_method(self): + print("Implemented") + + +if __name__ == "__main__": + my_bike = Bike( + description="Black and Beautiful", + condition=Condition.BAD, + sale_price=1000, + cost=100, + year=1965, + ) + test_unicycle = Unicycle.get_test_object() + test_bike = Bike.get_test_bike() + print(test_unicycle) + print(test_bike) + print([test_unicycle, test_bike]) + print(my_bike.slow_attribute) + print(my_bike.slow_attribute) + test_unicycle.child_method() diff --git a/scripts/oop_course/game_procedural.py b/scripts/oop_course/game_procedural.py new file mode 100644 index 0000000..5fd4eb2 --- /dev/null +++ b/scripts/oop_course/game_procedural.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +import random + +if __name__ == "__main__": + is_over = False + player1 = 0 + player2 = 0 + while not is_over: + print(f"Player 1: {player1}") + print(f"Player 2: {player2}") + player1 += random.randint(1, 6) + player2 += random.randint(1, 6) + if player1 >= 100 or player2 >= 100: + is_over = True + print(f"Player 1: {player1}") + print(f"Player 2: {player2}") diff --git a/scripts/oop_course/game_refactor.py b/scripts/oop_course/game_refactor.py new file mode 100644 index 0000000..b8d37ea --- /dev/null +++ b/scripts/oop_course/game_refactor.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +import random + + +class Game: + is_over = False + + def play(): + player1 = Player(id=1) + player2 = Player(id=2) + while not Game.is_over: + print(player1) + player1.play_turn() + + print(player2) + player2.play_turn() + + if player1.wins >= 100 or player2.wins >= 100: + Game.is_over = True + + print(player1) + print(player2) + + if player1.wins > player2.wins: + print("Player 1 Wins") + elif player1.wins < player2.wins: + print("Player 2 Wins") + else: + print("It's a draw.") + + +class Player: + def __init__(self, id: int): + self.id = id + self.wins = 0 + + def play_turn(self): + self.wins += random.randint(1, 6) + + def __str__(self): + return f"Player {self.id} has a total of {self.wins} Wins" + + +if __name__ == "__main__": + Game.play() From 316f7348d831213a4d421fab1abba2a96436579f Mon Sep 17 00:00:00 2001 From: Alvaro Ortiz Date: Wed, 10 Jan 2024 17:38:21 +0000 Subject: [PATCH 04/20] Add new material --- .../Coffee+Machine+Classes+Documentation.pdf | Bin 0 -> 77261 bytes .../Coffee+Machine+Program+Requirements.pdf | Bin 0 -> 68857 bytes .../coffee_maker.py | 31 ++++++ .../day16-oop-coffee-machine-start/main.py | 4 + .../day16-oop-coffee-machine-start/menu.py | 36 +++++++ .../money_machine.py | 34 +++++++ .../oop/day17-trivia/main.py | 4 + scripts/python_bootcamp_udemy/oop/dev.py | 91 ++++++++++++++++++ scripts/python_bootcamp_udemy/oop/main.py | 32 ++++++ scripts/python_bootcamp_udemy/trivia/main.py | 4 + 10 files changed, 236 insertions(+) create mode 100755 scripts/python_bootcamp_udemy/oop/Coffee+Machine+Classes+Documentation.pdf create mode 100755 scripts/python_bootcamp_udemy/oop/Coffee+Machine+Program+Requirements.pdf create mode 100755 scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/coffee_maker.py create mode 100755 scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/main.py create mode 100755 scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/menu.py create mode 100755 scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/money_machine.py create mode 100644 scripts/python_bootcamp_udemy/oop/day17-trivia/main.py create mode 100644 scripts/python_bootcamp_udemy/oop/dev.py create mode 100644 scripts/python_bootcamp_udemy/oop/main.py create mode 100644 scripts/python_bootcamp_udemy/trivia/main.py diff --git a/scripts/python_bootcamp_udemy/oop/Coffee+Machine+Classes+Documentation.pdf b/scripts/python_bootcamp_udemy/oop/Coffee+Machine+Classes+Documentation.pdf new file mode 100755 index 0000000000000000000000000000000000000000..f08f51d0ded4cf0afb34e9634d3bf1b7790de0e6 GIT binary patch literal 77261 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 literal 0 HcmV?d00001 diff --git a/scripts/python_bootcamp_udemy/oop/Coffee+Machine+Program+Requirements.pdf b/scripts/python_bootcamp_udemy/oop/Coffee+Machine+Program+Requirements.pdf new file mode 100755 index 0000000000000000000000000000000000000000..a13f19090797ad910137350fd69eca7bef1e44f6 GIT binary patch literal 68857 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 literal 0 HcmV?d00001 diff --git a/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/coffee_maker.py b/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/coffee_maker.py new file mode 100755 index 0000000..464d15f --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/coffee_maker.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +class CoffeeMaker: + """Models the machine that makes the coffee.""" + + def __init__(self): + self.resources = { + "water": 300, + "milk": 200, + "coffee": 100, + } + + def report(self): + """Prints a report of all resources.""" + print(f"Water: {self.resources['water']}ml") + print(f"Milk: {self.resources['milk']}ml") + print(f"Coffee: {self.resources['coffee']}g") + + def is_resource_sufficient(self, drink): + """Returns True when order can be made, False if ingredients are insufficient.""" + can_make = True + for item in drink.ingredients: + if drink.ingredients[item] > self.resources[item]: + print(f"Sorry there is not enough {item}.") + can_make = False + return can_make + + def make_coffee(self, order): + """Deducts the required ingredients from the resources.""" + for item in order.ingredients: + self.resources[item] -= order.ingredients[item] + print(f"Here is your {order.name} ☕️. Enjoy!") diff --git a/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/main.py b/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/main.py new file mode 100755 index 0000000..ea81928 --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/main.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +from coffee_maker import CoffeeMaker +from menu import Menu, MenuItem +from money_machine import MoneyMachine diff --git a/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/menu.py b/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/menu.py new file mode 100755 index 0000000..d177877 --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/menu.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +class MenuItem: + """Models each Menu Item.""" + + def __init__(self, name, water, milk, coffee, cost): + self.name = name + self.cost = cost + self.ingredients = {"water": water, "milk": milk, "coffee": coffee} + + +class Menu: + """Models the Menu with drinks.""" + + def __init__(self): + self.menu = [ + MenuItem(name="latte", water=200, milk=150, coffee=24, cost=2.5), + MenuItem(name="espresso", water=50, milk=0, coffee=18, cost=1.5), + MenuItem(name="cappuccino", water=250, milk=50, coffee=24, cost=3), + ] + + def get_items(self): + """Returns all the names of the available menu items.""" + options = "" + for item in self.menu: + options += f"{item.name}/" + return options + + def find_drink(self, order_name): + """Searches the menu for a particular drink by name. + + Returns that item if it exists, otherwise returns None + """ + for item in self.menu: + if item.name == order_name: + return item + print("Sorry that item is not available.") diff --git a/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/money_machine.py b/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/money_machine.py new file mode 100755 index 0000000..507c892 --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day16-oop-coffee-machine-start/money_machine.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +class MoneyMachine: + CURRENCY = "$" + + COIN_VALUES = {"quarters": 0.25, "dimes": 0.10, "nickles": 0.05, "pennies": 0.01} + + def __init__(self): + self.profit = 0 + self.money_received = 0 + + def report(self): + """Prints the current profit.""" + print(f"Money: {self.CURRENCY}{self.profit}") + + def process_coins(self): + """Returns the total calculated from coins inserted.""" + print("Please insert coins.") + for coin in self.COIN_VALUES: + self.money_received += int(input(f"How many {coin}?: ")) * self.COIN_VALUES[coin] + return self.money_received + + def make_payment(self, cost): + """Returns True when payment is accepted, or False if insufficient.""" + self.process_coins() + if self.money_received >= cost: + change = round(self.money_received - cost, 2) + print(f"Here is {self.CURRENCY}{change} in change.") + self.profit += cost + self.money_received = 0 + return True + else: + print("Sorry that's not enough money. Money refunded.") + self.money_received = 0 + return False diff --git a/scripts/python_bootcamp_udemy/oop/day17-trivia/main.py b/scripts/python_bootcamp_udemy/oop/day17-trivia/main.py new file mode 100644 index 0000000..836266a --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day17-trivia/main.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + + +URL = "https://opentdb.com/api.php?amount=10&type=boolean" diff --git a/scripts/python_bootcamp_udemy/oop/dev.py b/scripts/python_bootcamp_udemy/oop/dev.py new file mode 100644 index 0000000..1a5d9bd --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/dev.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +from prettytable import PrettyTable + + +class Requirement: + water = 20 + coffee = 5 + milk = 20 + money = 2 + + +class MachineException(Exception): + pass + + +class Machine: + operating = True + + def __init__(self) -> None: + self.water = 100 + self.milk = 50 + self.coffee = 76 + self.money = 2.5 + + @staticmethod + def prompt(): + return input("What would you like? (espresso/latte/cappuccino/): ") + + def turn_off(self): + print("Deactivating machine...") + + def print_report(self): + table = PrettyTable() + table.add_column("Object", ["Water", "Milk", "Coffee", "Money"]) + table.add_column("Amount", [self.water, self.milk, self.coffee, self.money]) + table.add_column("Unit", ["ml", "ml", "g", "$"]) + print(table) + + def make_coffee(self): + print("Making coffee...") + self.water -= Requirement.water + self.milk -= Requirement.milk + self.coffee -= Requirement.coffee + self.money -= Requirement.money + input("Please take your coffee.") + + def validate_resources(self): + return ( + (self.water > Requirement.water) + & (self.coffee > Requirement.coffee) + & (self.milk > Requirement.milk) + ) + + def validate_money(self): + return self.money > Requirement.money + + def insert_money(self, coin): + if coin == "q": + new_money = 0.25 + elif coin == "d": + new_money = 0.10 + elif coin == "n": + new_money = 0.05 + elif coin == "p": + new_money = 0.01 + elif coin == "dollar": + new_money = 1 + self.money += new_money + + +if __name__ == "__main__": + machine = Machine() + + while machine.operating: + action = machine.prompt() + if action == "espresso" or action == "latte" or action == "cappuccino": + if not machine.validate_resources(): + print("No resources") + continue + while not machine.validate_money(): + print("Not enough money. Insert more money: ") + coin = input("dollar = $1,vq = $0.25, d = $0.10, n = $0.05, p = $0.01: ") + machine.insert_money(coin) + machine.make_coffee() + elif action == "report": + machine.print_report() + elif action == "off": + machine.turn_off() + machine.operating = False + else: + print("Select a valid input") diff --git a/scripts/python_bootcamp_udemy/oop/main.py b/scripts/python_bootcamp_udemy/oop/main.py new file mode 100644 index 0000000..bce9e7e --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/main.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +""" +Note: you may need to ``apt install python3-tk`` + +How to implement OOP in python. + +First we need to know how to model a class. Think about what it ``has`` and what it ``does``. These will be its attributes and its methods. +An object is just a way of combining some piece of data and some functionality (this is the *encapsulation* property). The properties are +Abstraction, Encapsulation, Inheritance and Polymorphism. + +Many real objects and agents can be modelled using this framework. For example, a waiter in a restaurant can be modelled having as attributes whether +is carrying a plate or not, and also which tables is waiting. As methods, we could have something like take_order() and carry_order(). + +``Class`` is the blueprint. The different copies of this blueprint are called ``instances``. +""" + +from prettytable import PrettyTable +from prettytable.colortable import ColorTable, Themes + +table = ColorTable(theme=Themes.OCEAN) + +print(table) +# table = PrettyTable() + +table.header = False +table.add_column("Pokemon", ["Pikachu", "Squirtle", "Charizard"]) +table.add_column("Id", [25, 35, 100]) +table.add_column("Type", ["Electric", "Water", "Fire, Flying"]) + +print(table) +print(table.border) +print(table.header) diff --git a/scripts/python_bootcamp_udemy/trivia/main.py b/scripts/python_bootcamp_udemy/trivia/main.py new file mode 100644 index 0000000..02d05e7 --- /dev/null +++ b/scripts/python_bootcamp_udemy/trivia/main.py @@ -0,0 +1,4 @@ + + + +URL = "https://opentdb.com/api.php?amount=10&type=boolean" \ No newline at end of file From d62441fb83e44059bd9303fb66065366e2e01a8d Mon Sep 17 00:00:00 2001 From: Alvaro Ortiz Date: Wed, 10 Jan 2024 17:39:24 +0000 Subject: [PATCH 05/20] Add python_tk to dockerfile --- .devcontainer/Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 78c92c2..112afad 100755 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -41,6 +41,7 @@ RUN curl -k https://packages.microsoft.com/keys/microsoft.asc | apt-key add -\ mssql-tools\ unixodbc-dev\ libgssapi-krb5-2\ + python3-tk\ && apt-get clean\ && rm -rf /var/lib/apt/lists/* @@ -65,4 +66,6 @@ RUN yes Y | sh -c "$(curl -k -fsSL https://raw.githubusercontent.com/ohmyzsh/ohm COPY .devcontainer/config_devcontainer.sh /tmp COPY --chown=$USER:$USER . /home/$USER/python_learning WORKDIR /home/$USER/python_learning -USER $USER \ No newline at end of file +USER $USER + +ARG DEBIAN_FRONTEND=noninteractive \ No newline at end of file From c7171cafb058ee915c01659095a0334f0137b73f Mon Sep 17 00:00:00 2001 From: Alvaro Ortiz Date: Wed, 10 Jan 2024 18:14:36 +0000 Subject: [PATCH 06/20] day 17 --- .../oop/day17-trivia/api_client.py | 13 ++++++++++ .../oop/day17-trivia/main.py | 7 +++-- .../oop/day17-trivia/question.py | 8 ++++++ .../oop/day17-trivia/quiz_manager.py | 26 +++++++++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 scripts/python_bootcamp_udemy/oop/day17-trivia/api_client.py create mode 100644 scripts/python_bootcamp_udemy/oop/day17-trivia/question.py create mode 100644 scripts/python_bootcamp_udemy/oop/day17-trivia/quiz_manager.py diff --git a/scripts/python_bootcamp_udemy/oop/day17-trivia/api_client.py b/scripts/python_bootcamp_udemy/oop/day17-trivia/api_client.py new file mode 100644 index 0000000..53f575c --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day17-trivia/api_client.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +import requests + + +class ApiClient: + URL = "https://opentdb.com/api.php?amount=10&type=boolean" + + def fetch_questions(self): + response = requests.get(self.URL) + if response.status_code == 200: + data = response.json() + return data["results"] + return [] diff --git a/scripts/python_bootcamp_udemy/oop/day17-trivia/main.py b/scripts/python_bootcamp_udemy/oop/day17-trivia/main.py index 836266a..373c2ef 100644 --- a/scripts/python_bootcamp_udemy/oop/day17-trivia/main.py +++ b/scripts/python_bootcamp_udemy/oop/day17-trivia/main.py @@ -1,4 +1,7 @@ # -*- coding: utf-8 -*- +from quiz_manager import QuizManager - -URL = "https://opentdb.com/api.php?amount=10&type=boolean" +if __name__ == "__main__": + print("\n\nLET's Start the GAME!\n\n") + quiz = QuizManager() + quiz.start_quiz() diff --git a/scripts/python_bootcamp_udemy/oop/day17-trivia/question.py b/scripts/python_bootcamp_udemy/oop/day17-trivia/question.py new file mode 100644 index 0000000..ce39bb5 --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day17-trivia/question.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +class Question: + def __init__(self, content): + self.question = content["question"] + self.correct_answer = content["correct_answer"] + + def check_answer(self, answer): + return answer.lower() == self.correct_answer.lower() diff --git a/scripts/python_bootcamp_udemy/oop/day17-trivia/quiz_manager.py b/scripts/python_bootcamp_udemy/oop/day17-trivia/quiz_manager.py new file mode 100644 index 0000000..4d56ad2 --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day17-trivia/quiz_manager.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from api_client import ApiClient +from question import Question + + +class QuizManager: + self.score = 0 + self.api_client = ApiClient() + + def start_quiz(self): + question_list = self.api_client.fetch_questions() + for idx, q in enumerate(question_list): + question = Question(q) + print(f"\nQuestion number {idx+1}: ") + print(question.question) + answer = [] + while answer not in ["True", "False"]: + answer = input("Provide a True/False answer: ") + is_correct = question.check_answer(answer) + if is_correct: + print("Correct!") + self.score += 1 + else: + print("Incorrect!") + + print(f"\n\nFinal Score is: {self.score}/{len(question_list)} correct questions.") From 55fd348e9a6994867a974dbb3e0292a154b672f9 Mon Sep 17 00:00:00 2001 From: Alvaro Ortiz Date: Sat, 20 Jan 2024 11:08:34 +0100 Subject: [PATCH 07/20] yaml --- environment.yml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/environment.yml b/environment.yml index 9ea6f4e..5ec5a37 100644 --- a/environment.yml +++ b/environment.yml @@ -1,16 +1,15 @@ -name: tensorflow-cuda +name: python-bootcamp channels: - defaults - conda-forge dependencies: - python=3.9 - pip - - cudnn=7.6 - - cudatoolkit=10.1 - nb_conda - jupyter - scikit-learn + - numpy - scipy - pandas - pandas-datareader @@ -19,10 +18,5 @@ dependencies: - requests - flask - pip: - - pymc3 - - bayesian-optimization - - gym - kaggle - - kats - - tensorflow==2.3.0 # - -r requirements/dev.txt \ No newline at end of file From 28cd7fa01874e5801e400c024637fcbb1a825bb4 Mon Sep 17 00:00:00 2001 From: Alvaro Ortiz Date: Mon, 22 Jan 2024 19:27:07 +0100 Subject: [PATCH 08/20] Add movement to snake --- .../oop/day20_21-snake/main.py | 27 ++++++++++ .../oop/day20_21-snake/snake.py | 51 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 scripts/python_bootcamp_udemy/oop/day20_21-snake/main.py create mode 100644 scripts/python_bootcamp_udemy/oop/day20_21-snake/snake.py diff --git a/scripts/python_bootcamp_udemy/oop/day20_21-snake/main.py b/scripts/python_bootcamp_udemy/oop/day20_21-snake/main.py new file mode 100644 index 0000000..1c15ecd --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day20_21-snake/main.py @@ -0,0 +1,27 @@ +import time +from turtle import Turtle, Screen +from snake import Snake + +screen = Screen() +screen.setup(width=600, height=600) +screen.bgcolor('black') +screen.title("My Snake Game") +screen.tracer(0) + +snake = Snake() + +screen.listen() +screen.onkey(snake.up, "Up") +screen.onkey(snake.down, "Down") +screen.onkey(snake.left, "Left") +screen.onkey(snake.right, "Right") + +game_is_on = True +while game_is_on: + screen.update() + time.sleep(0.1) + + # snake._random_direction_change(p=0.5) + snake.move() + +screen.exitonclick() \ No newline at end of file diff --git a/scripts/python_bootcamp_udemy/oop/day20_21-snake/snake.py b/scripts/python_bootcamp_udemy/oop/day20_21-snake/snake.py new file mode 100644 index 0000000..5d63ae1 --- /dev/null +++ b/scripts/python_bootcamp_udemy/oop/day20_21-snake/snake.py @@ -0,0 +1,51 @@ +from turtle import Turtle +import numpy as np +MOVE_DISTANCE = 20 +STARTING_POSITION = [(0,0), (-20,0), (-40,0)] + +class Snake(): + "Class that represents the snake in the Snake Game." + def __init__(self, shape="square", color="white"): + self.shape = shape + self.color = color + + self.segments = None + self.initialize_segments() + + def initialize_segments(self): + """Initialize the three first segments of the snake at a fixed position.""" + snake_segment = [] + + for start_pos in STARTING_POSITION: + new_segment = Turtle() + new_segment.penup() + new_segment.shape(self.shape) + new_segment.color(self.color) + new_segment.goto(start_pos) + snake_segment.append(new_segment) + + self.segments = snake_segment + + def move(self): + """Move the snake in the direction is already heading""" + for idx in reversed(range(1, len(self.segments))): + newx, newy = self.segments[idx-1].pos() + self.segments[idx].goto(newx, newy) + self.segments[0].forward(MOVE_DISTANCE) + + def _random_direction_change(self, p): + """Randomly changes the direction the snake is heading to""" + if np.random.rand() < p: + self.segments[0].setheading(np.random.choice([0,90,180,270])) + + def up(self): + self.segments[0].setheading(90) + + def down(self): + self.segments[0].setheading(270) + + def right(self): + self.segments[0].setheading(0) + + def left(self): + self.segments[0].setheading(180) From 66750b8788565feabcebd2a21babbd436a2a7da7 Mon Sep 17 00:00:00 2001 From: Alvaro Ortiz Date: Tue, 30 Jan 2024 17:11:17 +0000 Subject: [PATCH 09/20] Oreilly intermediate classes --- scripts/intermediate_oop/classes.py | 57 ++ scripts/intermediate_oop/classes_test.py | 714 +++++++++++++++++++ scripts/intermediate_oop/dunder.py | 26 + scripts/intermediate_oop/dunder_test.py | 548 ++++++++++++++ scripts/intermediate_oop/helpers.py | 9 + scripts/intermediate_oop/inheritance.py | 61 ++ scripts/intermediate_oop/inheritance_test.py | 463 ++++++++++++ scripts/intermediate_oop/initial.py | 13 + scripts/intermediate_oop/initial_test.py | 20 + scripts/intermediate_oop/main.py | 37 + scripts/intermediate_oop/properties.py | 14 + scripts/intermediate_oop/properties_test.py | 161 +++++ scripts/intermediate_oop/refactoring.py | 66 ++ scripts/intermediate_oop/refactoring_test.py | 300 ++++++++ scripts/intermediate_oop/test.py | 88 +++ scripts/intermediate_oop/test_data.py | 75 ++ 16 files changed, 2652 insertions(+) create mode 100755 scripts/intermediate_oop/classes.py create mode 100755 scripts/intermediate_oop/classes_test.py create mode 100755 scripts/intermediate_oop/dunder.py create mode 100755 scripts/intermediate_oop/dunder_test.py create mode 100755 scripts/intermediate_oop/helpers.py create mode 100755 scripts/intermediate_oop/inheritance.py create mode 100755 scripts/intermediate_oop/inheritance_test.py create mode 100755 scripts/intermediate_oop/initial.py create mode 100755 scripts/intermediate_oop/initial_test.py create mode 100644 scripts/intermediate_oop/main.py create mode 100755 scripts/intermediate_oop/properties.py create mode 100755 scripts/intermediate_oop/properties_test.py create mode 100755 scripts/intermediate_oop/refactoring.py create mode 100755 scripts/intermediate_oop/refactoring_test.py create mode 100755 scripts/intermediate_oop/test.py create mode 100755 scripts/intermediate_oop/test_data.py diff --git a/scripts/intermediate_oop/classes.py b/scripts/intermediate_oop/classes.py new file mode 100755 index 0000000..fe018c5 --- /dev/null +++ b/scripts/intermediate_oop/classes.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +"""Class exercises.""" + + +class BankAccount: + """Bank account including an account balance.""" + + def __init__(self, balance=0): + self.balance = balance + + def deposit(self, quantity): + self.balance += quantity + + def withdraw(self, quantity): + self.balance -= quantity + + def transfer(self, BankAccount, quantity): + BankAccount.balance += quantity + self.balance -= quantity + + def __repr__(self): + return f"BankAccount(balance={self.balance})" + + def __str__(self): + return f"A Bank Account with balance={self.balance})" + + +class SuperMap: + """Data structure for quickly finding objects based on their attributes.""" + + +class MinHeap: + """Heap-like data structure.""" + + +class Flavor: + """Flavor of ice cream.""" + + +class Size: + """Ice cream size.""" + + +class IceCream: + """Ice cream to be ordered in our ice cream shop.""" + + +class Month: + """Class representing an entire month.""" + + +class MonthDelta: + """Class representing the difference between months.""" + + +class Row: + """Row class that stores all given arguments as attributes.""" diff --git a/scripts/intermediate_oop/classes_test.py b/scripts/intermediate_oop/classes_test.py new file mode 100755 index 0000000..324069c --- /dev/null +++ b/scripts/intermediate_oop/classes_test.py @@ -0,0 +1,714 @@ +# -*- coding: utf-8 -*- +"""Tests for classes exercises.""" +import random +import unittest +from contextlib import contextmanager +from datetime import date, timedelta +from itertools import cycle, permutations +from locale import LC_TIME, setlocale +from string import ascii_uppercase +from timeit import default_timer + +from classes import BankAccount, Flavor, IceCream, MinHeap, Month, MonthDelta, Row, Size, SuperMap + +random.seed(0) + +names = permutations(ascii_uppercase, 6) +colors = cycle( + [ + "purple", + "green", + "pink", + "blue", + "black", + "orange", + "yellow", + ] +) + + +class BankAccountTests(unittest.TestCase): + """Tests for BankAccount.""" + + def test_new_account_balance_default(self): + account = BankAccount() + self.assertEqual(account.balance, 0) + + def test_opening_balance(self): + account = BankAccount(balance=100) + self.assertEqual(account.balance, 100) + + def test_deposit(self): + account = BankAccount() + account.deposit(100) + self.assertEqual(account.balance, 100) + + def test_withdraw(self): + account = BankAccount(balance=100) + account.withdraw(40) + self.assertEqual(account.balance, 60) + + def test_repr(self): + account = BankAccount() + self.assertEqual(repr(account), "BankAccount(balance=0)") + account.deposit(200) + self.assertEqual(repr(account), "BankAccount(balance=200)") + + def test_transfer(self): + mary_account = BankAccount(balance=100) + dana_account = BankAccount(balance=0) + mary_account.transfer(dana_account, 20) + self.assertEqual(mary_account.balance, 80) + self.assertEqual(dana_account.balance, 20) + + @unittest.skip("BankAccount Transactions") + def test_transactions_open(self): + expected_transactions = [ + ("OPEN", 100, 100), + ] + account = BankAccount(balance=100) + self.assertEqual(account.transactions, expected_transactions) + + @unittest.skip("BankAccount Transactions") + def test_transactions_deposit(self): + expected_transactions = [ + ("OPEN", 0, 0), + ("DEPOSIT", 100, 100), + ] + account = BankAccount() + account.deposit(100) + self.assertEqual(account.transactions, expected_transactions) + + @unittest.skip("BankAccount Transactions") + def test_transactions_withdraw(self): + expected_transactions = [ + ("OPEN", 100, 100), + ("WITHDRAWAL", -40, 60), + ] + account = BankAccount(balance=100) + account.withdraw(40) + self.assertEqual(account.transactions, expected_transactions) + + @unittest.skip("BankAccount Transactions") + def test_transactions_scenario(self): + expected_transactions = [ + ("OPEN", 0, 0), + ("DEPOSIT", 100, 100), + ("WITHDRAWAL", -40, 60), + ("DEPOSIT", 95, 155), + ] + account = BankAccount() + account.deposit(100) + account.withdraw(40) + account.deposit(95) + self.assertEqual(account.transactions, expected_transactions) + + @unittest.skip("Truthy BankAccount") + def test_truthy_accounts(self): + account = BankAccount() + self.assertIs(bool(account), False) + account.deposit(100) + self.assertIs(bool(account), True) + + @unittest.skip("Comparable BankAccount") + def test_account_comparisons(self): + account1 = BankAccount() + account2 = BankAccount() + self.assertTrue(account1 == account2) + self.assertTrue(account1 >= account2) + self.assertTrue(account1 <= account2) + account1.deposit(100) + account2.deposit(10) + self.assertTrue(account1 != account2) + self.assertTrue(account2 < account1) + self.assertTrue(account1 > account2) + self.assertTrue(account2 < account1) + self.assertTrue(account1 >= account2) + self.assertTrue(account2 <= account1) + + @unittest.skip("Read-Only BankAccount") + def test_balance_cannot_be_written(self): + account1 = BankAccount() + account2 = BankAccount(100) + self.assertEqual(account1.balance, 0) + with self.assertRaises(Exception): + account1.balance = 50 + self.assertEqual(account1.balance, 0) + self.assertEqual(account2.balance, 100) + with self.assertRaises(Exception): + account2.balance = 50 + self.assertEqual(account2.balance, 100) + account1.deposit(100) + account2.deposit(10) + self.assertEqual(account1.balance, 100) + self.assertEqual(account2.balance, 110) + with self.assertRaises(Exception): + account2.balance = 500 + self.assertEqual(account2.balance, 110) + account2.transfer(account1, 50) + self.assertEqual(account1.balance, 150) + self.assertEqual(account2.balance, 60) + + @unittest.skip("Hidden Balance") + def test_dir_does_not_show_balance_attribute(self): + account = BankAccount() + account.deposit(100) + self.assertNotIn("_balance", dir(account)) + allowed = { + "accounts", + "balance", + "deposit", + "withdraw", + "transfer", + "transactions", + "name", + } | set(dir(type("", (), {})())) + self.assertEqual(set(dir(account)) - allowed, set()) + + +class Item: + __slots__ = ("id", "name", "color", "version") + + def __init__(self, id, name, color, version): + self.id = id + self.name = name + self.color = color + self.version = version + + @property + def _values(self): + return (self.id, self.name, self.color, self.version) + + def __hash__(self): + return hash(self._values) + + def __eq__(self, other): + if not isinstance(other, Item): + return NotImplemented + return self._values == other._values + + def __repr__(self): + return f"Item{self._values!r}" + + +class SuperMapTests(unittest.TestCase): + """Tests for SuperMap.""" + + def test_where_method(self): + few_items = [ + Item(i, "".join(next(names)), next(colors), random.randint(0, 5)) + for i in range(10_000, 10_100) + ] + mapping = SuperMap(few_items, indexes=("id", "color")) + matches = mapping.where("color", "pink") + self.assertEqual(set(matches), {item for item in few_items if item.color == "pink"}) + matches = mapping.where("id", 4) + self.assertEqual(len(matches), 0) + matches = mapping.where("id", 10_050) + self.assertEqual(len(matches), 1) + + def test_time_efficient_lookups(self): + many_items = [ + Item(i, "".join(next(names)), next(colors), random.randint(0, 5)) for i in range(2_000) + ] + mapping = SuperMap(many_items, indexes=("id", "color")) + with Timer() as manual_lookup: + items1 = {item for item in many_items if item.color == "pink"} + with Timer() as lookup_from_map: + items2 = mapping.where("color", "pink") + self.assertEqual(len(items1), len(items2)) + self.assertEqual(set(items1), items2) + self.assertGreater( + manual_lookup.elapsed, + lookup_from_map.elapsed * 5, + ) + + +class MinHeapTests(unittest.TestCase): + """Tests for MinHeap.""" + + @classmethod + def setUpClass(cls): + cls.big_numbers = [ + 3748, + 7250, + 140, + 7669, + 5711, + 2284, + 3322, + 6435, + 8138, + 6920, + 9634, + 7511, + 5295, + 5456, + 7458, + 5618, + 102, + 7747, + 4638, + 46, + 4532, + 1483, + 944, + 3542, + 6641, + 9091, + 693, + 836, + 3099, + 3385, + 7798, + 758, + 8407, + 4756, + 8801, + 3936, + 5301, + 5744, + 6454, + 1156, + 7686, + 5664, + 2568, + 6414, + 3469, + 2867, + 8875, + 6097, + 2546, + 4658, + 7027, + 9437, + 755, + 8536, + 8186, + 9539, + 661, + 6706, + 265, + 2254, + 2402, + 3355, + 9141, + 5091, + 1727, + 6739, + 4599, + 5599, + 9007, + 2925, + 2894, + 5333, + 9586, + 7409, + 916, + 6420, + 8493, + 9531, + 5083, + 5350, + 3346, + 1378, + 6260, + 3143, + 7216, + 684, + 170, + 6721, + 418, + 7013, + 7729, + 7484, + 5355, + 4850, + 8073, + 1389, + 2084, + 1856, + 9740, + 2747, + ] + + def test_create_heap(self): + MinHeap([322, 76, 4, 7, 2, 123, 47, 1, 18, 3, 29, 199, 11]) + MinHeap(self.big_numbers) + + def test_peek_at_smallest(self): + numbers = [11, 322, 3, 199, 29, 7, 1, 18, 76, 4, 2, 47, 123] + h = MinHeap(numbers) + self.assertEqual(h.peek(), 1) + i = MinHeap(self.big_numbers) + self.assertEqual(i.peek(), 46) + + def test_pop_from_heap(self): + numbers = [11, 322, 3, 199, 29, 7, 1, 18, 76, 4, 2, 47, 123] + h = MinHeap(numbers) + self.assertEqual(h.pop(), 1) + self.assertEqual(h.pop(), 2) + self.assertEqual(h.pop(), 3) + self.assertEqual(h.pop(), 4) + self.assertEqual(h.pop(), 7) + self.assertEqual(h.pop(), 11) + i = MinHeap(self.big_numbers) + self.assertEqual(i.pop(), 46) + + def test_push_onto_heap(self): + numbers = [11, 322, 3, 199, 29, 7, 1, 18, 76, 4, 2, 47, 123] + i = MinHeap(self.big_numbers) + i.push(17) + self.assertEqual(i.peek(), 17) + i.push(24) + self.assertEqual(i.pop(), 17) + self.assertEqual(i.pop(), 24) + self.assertEqual(i.pop(), 46) + h = MinHeap(numbers) + h.push(6) + self.assertEqual(h.pop(), 1) + self.assertEqual(h.pop(), 2) + self.assertEqual(h.pop(), 3) + self.assertEqual(h.pop(), 4) + self.assertEqual(h.pop(), 6) + + def test_faster_than_sorting(self): + many_big_numbers = [random.randint(100, 1000) for n in range(10000)] + with Timer() as sort_timer: + sorted(many_big_numbers) + heap = MinHeap(many_big_numbers) + with Timer() as min_heap_timer: + heap.push(150) + heap.push(950) + heap.push(400) + heap.push(760) + heap.push(280) + heap.push(870) + heap.push(330) + heap.push(1000) + heap.push(50) + heap.push(500) + items = [heap.pop() for _ in range(10)] + self.assertEqual(len(items), 10) + self.assertLess(min_heap_timer.elapsed, sort_timer.elapsed) + + +class FlavorTests(unittest.TestCase): + """Tests for Flavor.""" + + def test_name_attribute(self): + flavor = Flavor("vanilla") + self.assertEqual(flavor.name, "vanilla") + + def test_specifying_ingredients(self): + flavor = Flavor("vanilla", ingredients=["milk", "sugar", "vanilla"]) + self.assertEqual(flavor.ingredients, ["milk", "sugar", "vanilla"]) + flavor = Flavor("chocolate", ingredients=["milk", "sugar", "vanilla", "chocolate"]) + self.assertEqual( + flavor.ingredients, + ["milk", "sugar", "vanilla", "chocolate"], + ) + + def test_modifying_ingredients(self): + original_ingredients = ["milk", "sugar", "vanilla"] + flavor = Flavor("vanilla", ingredients=original_ingredients) + flavor.ingredients.append("red bean") + self.assertEqual(original_ingredients, ["milk", "sugar", "vanilla"]) + + def test_has_dairy_attribute(self): + flavor = Flavor("vanilla") + self.assertIs(flavor.has_dairy, True) + flavor = Flavor("vanilla", has_dairy=True) + self.assertIs(flavor.has_dairy, True) + flavor = Flavor("vanilla", has_dairy=False) + self.assertIs(flavor.has_dairy, False) + + def test_string_representation(self): + flavor = Flavor("chocolate", has_dairy=False) + self.assertEqual(repr(flavor), "Flavor(name='chocolate', ingredients=[], has_dairy=False)") + flavor = Flavor("vanilla", ingredients=["milk", "sugar", "vanilla"]) + self.assertEqual( + repr(flavor), + "Flavor(name='vanilla', ingredients=['milk', 'sugar', 'vanilla'], has_dairy=True)", + ) + + +class SizeTests(unittest.TestCase): + """Tests for Size.""" + + def test_initializer(self): + size = Size(quantity=1, unit="gram", price="5.00") + self.assertEqual(size.quantity, 1) + self.assertEqual(size.unit, "gram") + self.assertEqual(size.price, "5.00") + + def test_human_string_representation(self): + size = Size(quantity=1, unit="gram", price="5.00") + self.assertEqual(str(size), "1 gram") + size = Size(quantity=1, unit="scoop", price="5.00") + self.assertEqual(str(size), "1 scoop") + + def test_pluralization(self): + size = Size(quantity=3, unit="pint", price="9.00") + self.assertEqual(str(size), "3 pints") + size = Size(quantity=3, unit="scoop", price="4.00") + self.assertEqual(str(size), "3 scoops") + + def test_machine_string_representation(self): + size = Size(quantity=1, unit="cup", price="5") + self.assertEqual(repr(size), "Size(quantity=1, unit='cup', price='5')") + + +class IceCreamTests(unittest.TestCase): + """Tests for IceCream.""" + + def test_initializer(self): + one_quart = Size(quantity=1, unit="quart", price="$9") + vanilla = Flavor("vanilla") + quart_of_vanilla = IceCream(flavor=vanilla, size=one_quart) + self.assertEqual(quart_of_vanilla.size, one_quart) + self.assertEqual(quart_of_vanilla.flavor, vanilla) + + def test_string_representation(self): + one_quart = Size(quantity=1, unit="quart", price="$9") + vanilla = Flavor("vanilla") + quart_of_vanilla = IceCream(flavor=vanilla, size=one_quart) + self.assertEqual(str(quart_of_vanilla), "1 quart of vanilla") + self.assertEqual(str(quart_of_vanilla), "1 quart of vanilla") + two_scoops = IceCream( + flavor=Flavor("chocolate"), + size=Size(quantity=2, unit="scoop", price="$3"), + ) + self.assertEqual(str(two_scoops), "2 scoops of chocolate") + + +class MonthTests(unittest.TestCase): + """Tests for Month.""" + + def test_initialization(self): + month = Month(2019, 1) + self.assertEqual(month.year, 2019) + self.assertEqual(month.month, 1) + + def test_machine_readable_representation(self): + month = Month(2019, 1) + self.assertEqual(repr(month), "Month(year=2019, month=1)") + + def test_human_readable_representation(self): + month = Month(2019, 1) + self.assertEqual(str(month), "2019-01") + + def test_string_representations(self): + python2_eol = Month(2020, 1) + self.assertEqual(str(python2_eol), "2020-01") + new_month = eval(repr(python2_eol)) + self.assertEqual(new_month.year, python2_eol.year) + self.assertEqual(new_month.month, python2_eol.month) + + def test_first_method(self): + python2_eol = Month(2020, 1) + eol_date = python2_eol.first() + self.assertEqual(eol_date.year, 2020) + self.assertEqual(eol_date.month, 1) + self.assertEqual(eol_date.day, 1) + self.assertEqual(str(eol_date), "2020-01-01") + self.assertEqual(str(eol_date - timedelta(days=1)), "2019-12-31") + + @unittest.skip("Comparable Month") + def test_equality(self): + python2_eol = Month(2020, 1) + self.assertEqual(python2_eol, Month(2020, 1)) + self.assertNotEqual(python2_eol, Month(2020, 2)) + self.assertNotEqual(python2_eol, Month(2019, 1)) + self.assertFalse(python2_eol != Month(2020, 1)) + self.assertFalse(python2_eol == Month(2020, 2)) + self.assertNotEqual(python2_eol, date(2020, 1, 1)) + self.assertNotEqual(python2_eol, (2020, 1)) + self.assertNotEqual((2020, 1), python2_eol) # tuples aren't months + + @unittest.skip("Comparable Month") + def test_ordering(self): + python2_eol = Month(2020, 1) + pycon_2019 = Month(2019, 5) + self.assertLess(pycon_2019, python2_eol) + self.assertGreater(python2_eol, pycon_2019) + self.assertLessEqual(pycon_2019, python2_eol) + self.assertGreaterEqual(python2_eol, pycon_2019) + self.assertFalse(pycon_2019 > python2_eol) + self.assertFalse(pycon_2019 >= python2_eol) + self.assertFalse(python2_eol < pycon_2019) + self.assertFalse(python2_eol <= pycon_2019) + with self.assertRaises(TypeError): + python2_eol < (2021, 12) # tuples aren't months + with self.assertRaises(TypeError): + python2_eol >= (2021, 12) # tuples aren't months + with self.assertRaises(TypeError): + (2021, 12) < python2_eol # tuples aren't months + + @unittest.skip("Month Formatting") + def test_formatting(self): + python2_eol = Month(2020, 1) + leap_month = Month(2000, 2) + self.assertEqual("{:%Y-%m}".format(python2_eol), "2020-01") + with set_locale("C"): + self.assertEqual("{0:%b %Y}".format(leap_month), "Feb 2000") + self.assertEqual("{:%b %Y}".format(python2_eol), "Jan 2020") + + @unittest.skip("Month from_date") + def test_from_date(self): + python2_eol = Month.from_date(date(2020, 1, 1)) + self.assertEqual(python2_eol, Month(2020, 1)) + leap_month = Month.from_date(date(2000, 2, 29)) + self.assertEqual(leap_month, Month(2000, 2)) + + @unittest.skip("Memory-Efficient Month") + def test_memory_efficient(self): + python2_eol = Month(2020, 1) + with self.assertRaises(Exception): + python2_eol.__dict__ + + +@contextmanager +def set_locale(name): + saved = setlocale(LC_TIME) + try: + yield setlocale(LC_TIME, name) + finally: + setlocale(LC_TIME, saved) + + +class MonthDeltaTests(unittest.TestCase): + """Tests for MonthDelta.""" + + def test_initializer(self): + four_months = MonthDelta(4) + self.assertEqual(four_months.months, 4) + + def test_equality(self): + self.assertEqual(MonthDelta(12), MonthDelta(12)) + self.assertNotEqual(MonthDelta(11), MonthDelta(12)) + self.assertIs(MonthDelta(12) != MonthDelta(12), False) + self.assertIs(MonthDelta(11) == MonthDelta(12), False) + self.assertIs(MonthDelta(0) == timedelta(0), False) + self.assertIs(MonthDelta(0) == 0, False) + self.assertIs(MonthDelta(6) == 6, False) + + def test_adding_month_delta_to_unknown_value(self): + with self.assertRaises(TypeError): + MonthDelta(4) + 8 + with self.assertRaises(TypeError): + 8 + MonthDelta(4) + + def test_adding_and_subtracting_with_monthdeltas(self): + self.assertEqual(MonthDelta(4) + MonthDelta(2), MonthDelta(6)) + self.assertEqual(MonthDelta(4) - MonthDelta(2), MonthDelta(2)) + with self.assertRaises(TypeError): + MonthDelta(4) - 8 + with self.assertRaises(TypeError): + 8 - MonthDelta(4) + + def test_month_arithmetic_with_month_deltas(self): + python2_eol = Month(2020, 1) + python2_release = Month(2000, 10) + python2_lifetime = MonthDelta(231) + self.assertEqual(python2_eol + MonthDelta(4), Month(2020, 5)) + self.assertEqual(MonthDelta(13) + python2_eol, Month(2021, 2)) + self.assertEqual(python2_eol - MonthDelta(4), Month(2019, 9)) + self.assertEqual(python2_eol - MonthDelta(13), Month(2018, 12)) + self.assertEqual(python2_release + python2_lifetime, python2_eol) + + def test_month_subtracting_months(self): + python2_eol = Month(2020, 1) + python2_release = Month(2000, 10) + python2_lifetime = python2_eol - python2_release + self.assertEqual(python2_lifetime, MonthDelta(20 * 12 - 9)) + + def test_month_arithmetic_with_other_types(self): + python2_eol = Month(2020, 1) + python2_release = Month(2000, 10) + python2_lifetime = python2_eol - python2_release + with self.assertRaises(TypeError): + python2_eol + python2_release + with self.assertRaises(TypeError): + python2_eol * python2_release + with self.assertRaises(TypeError): + python2_eol * python2_lifetime + with self.assertRaises(TypeError): + python2_lifetime - python2_eol + with self.assertRaises(TypeError): + python2_eol - date(1999, 12, 1) + + @unittest.skip("MonthDelta Arithmetic") + def test_scaling_and_division(self): + self.assertEqual(MonthDelta(4) * 2, MonthDelta(8)) + self.assertEqual(2 * MonthDelta(4), MonthDelta(8)) + self.assertEqual(MonthDelta(4) / MonthDelta(2), 2) + self.assertEqual(MonthDelta(18) // 12, MonthDelta(1)) + self.assertEqual(MonthDelta(18) // MonthDelta(12), 1) + self.assertEqual(MonthDelta(18) % MonthDelta(12), 6) + self.assertEqual(MonthDelta(18) % 12, MonthDelta(6)) + self.assertEqual(-MonthDelta(18), MonthDelta(-18)) + with self.assertRaises(TypeError): + MonthDelta(4) * "a" + with self.assertRaises(TypeError): + MonthDelta(4) * 2.0 + with self.assertRaises(TypeError): + MonthDelta(4) / 2.0 + with self.assertRaises(TypeError): + MonthDelta(4) * MonthDelta(2) + with self.assertRaises(TypeError): + MonthDelta(4) % 0.5 + + +class RowTests(unittest.TestCase): + """Tests for Row.""" + + def test_no_arguments(self): + row = Row() + attributes = {x for x in dir(row) if not x.startswith("__")} + self.assertEqual(attributes, set()) + + def test_single_argument(self): + row = Row(a=1) + self.assertEqual(row.a, 1) + attributes = {x for x in dir(row) if not x.startswith("__")} + self.assertEqual(attributes, {"a"}) + + def test_two_arguments(self): + row = Row(a=1, b=2) + self.assertEqual(row.a, 1) + self.assertEqual(row.b, 2) + attributes = {x for x in dir(row) if not x.startswith("__")} + self.assertEqual(attributes, {"a", "b"}) + + def test_many_arguments(self): + row = Row(thing="a", item=2, stuff=True) + self.assertEqual(row.thing, "a") + self.assertEqual(row.item, 2) + self.assertEqual(row.stuff, True) + attributes = {x for x in dir(row) if not x.startswith("__")} + self.assertEqual(attributes, {"thing", "item", "stuff"}) + + def test_no_positional_arguments_accepted(self): + with self.assertRaises(Exception): + Row(1, 2) + with self.assertRaises(Exception): + Row(1) + + +class Timer: + """Context manager to time a code block.""" + + def __enter__(self): + self.start = default_timer() + return self + + def __exit__(self, *args): + self.end = default_timer() + self.elapsed = self.end - self.start + + +if __name__ == "__main__": + from helpers import error_message + + error_message() diff --git a/scripts/intermediate_oop/dunder.py b/scripts/intermediate_oop/dunder.py new file mode 100755 index 0000000..5085292 --- /dev/null +++ b/scripts/intermediate_oop/dunder.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +"""Dunder exercises.""" + + +class ReverseView: + """Lazily operate on a sequence in reverse.""" + + +class Comparator: + """Object that is equal to a very small range of numbers.""" + + +class RomanNumeral: + """Class for treating Roman Numerals like numbers.""" + + +class Timer: + """Utility for timing the execution of code.""" + + +class FancyDict: + """Dictioray-like class supporting attribute lookups.""" + + +class reloopable: + """Iterable which resets a file each time it's looped over.""" diff --git a/scripts/intermediate_oop/dunder_test.py b/scripts/intermediate_oop/dunder_test.py new file mode 100755 index 0000000..0c7ffec --- /dev/null +++ b/scripts/intermediate_oop/dunder_test.py @@ -0,0 +1,548 @@ +# -*- coding: utf-8 -*- +"""Tests for dunder exercises.""" +import unittest +from collections.abc import Generator, Iterable, Mapping +from io import StringIO +from sys import getsizeof +from textwrap import dedent +from time import sleep + +from dunder import Comparator, FancyDict, ReverseView, RomanNumeral, Timer, reloopable + + +class ReverseViewTests(unittest.TestCase): + """Tests for ReverseView.""" + + def test_can_iterate_at_least_once(self): + numbers = [2, 1, 3, 4, 7, 11, 18] + view = ReverseView(numbers) + self.assertEqual(list(view), [18, 11, 7, 4, 3, 1, 2]) + + def test_can_iterate_more_than_once(self): + numbers = [2, 1, 3, 4, 7, 11, 18] + view = ReverseView(numbers) + self.assertEqual(list(view), [18, 11, 7, 4, 3, 1, 2]) + self.assertEqual(list(view), [18, 11, 7, 4, 3, 1, 2]) + self.assertEqual(list(view), list(view)) + + def test_updating_sequence_updates_view(self): + numbers = [2, 1, 3, 4, 7, 11, 18] + view = ReverseView(numbers) + self.assertEqual(list(view), [18, 11, 7, 4, 3, 1, 2]) + numbers.append(29) + self.assertEqual(list(view), [29, 18, 11, 7, 4, 3, 1, 2]) + numbers.pop(0) + self.assertEqual(list(view), [29, 18, 11, 7, 4, 3, 1]) + + def test_no_memory_used(self): + numbers = list(range(10000)) + view = ReverseView(numbers) + next(iter(view)) + if isinstance(view, Generator): + size = sum(get_size(obj) for obj in view.gi_frame.f_locals.values()) + else: + size = get_size(view) + self.assertLess(size, 400000, "Too much memory used") + self.assertNotEqual(type(view), list) + self.assertNotEqual(type(view), tuple) + + def test_does_not_slice_sequence(self): + class UnsliceableList(list): + def __getitem__(self, index): + if not isinstance(index, int): + return NotImplemented("Only indexes accepted") + return super().__getitem__(index) + + numbers = UnsliceableList([2, 1, 3, 4, 7, 11, 18]) + view = ReverseView(numbers) + self.assertEqual(list(view), [18, 11, 7, 4, 3, 1, 2]) + + def test_has_length_and_repr_and_is_indexable(self): + numbers = [2, 1, 3, 4, 7, 11, 18] + view = ReverseView(numbers) + + # Has length + self.assertEqual(len(view), 7) + self.assertEqual(len(view), 7) + numbers.append(29) + self.assertEqual(len(view), 8) + numbers.pop() + self.assertEqual(len(view), 7) + + # Is indexable + self.assertEqual(view[0], 18) + self.assertEqual(view[-1], 2) + self.assertEqual(view[2], 7) + self.assertEqual(view[-2], 1) + numbers.append(29) + self.assertEqual(view[0], 29) + self.assertEqual(view[-1], 2) + numbers.pop(0) + self.assertEqual(view[0], 29) + self.assertEqual(view[-1], 1) + + # Has a nice string representation + self.assertEqual(list(view), [29, 18, 11, 7, 4, 3, 1]) + self.assertEqual(str(view), "[29, 18, 11, 7, 4, 3, 1]") + + +class ComparatorTests(unittest.TestCase): + """Tests for Comparator.""" + + def test_equality_with_delta(self): + self.assertEqual(5.5, Comparator(6, delta=0.5)) + self.assertEqual(6.5, Comparator(6, delta=0.5)) + self.assertNotEqual(6.51, Comparator(6, delta=0.5)) + self.assertNotEqual(5.49, Comparator(6, delta=0.5)) + + def test_equality_with_default_delta(self): + self.assertEqual(Comparator(5), 4.99999999) + self.assertEqual(Comparator(5), 5.00000001) + self.assertEqual(5, Comparator(4.99999999)) + self.assertEqual(5, Comparator(5.00000001)) + self.assertNotEqual(Comparator(5), 4.99999) + self.assertNotEqual(Comparator(5), 5.00001) + + def test_negative_numbers(self): + self.assertNotEqual(-5.5, Comparator(-6, delta=0.25)) + self.assertEqual(-5.75, Comparator(-6, delta=0.25)) + self.assertEqual(-6.25, Comparator(-6, delta=0.25)) + self.assertNotEqual(-6.3, Comparator(-6, delta=0.25)) + + def test_very_small_delta(self): + self.assertEqual(-6.000000000000001, Comparator(-6, delta=1e-15)) + self.assertNotEqual(-6.000000000000002, Comparator(-6, delta=1e-15)) + + def test_string_representation(self): + self.assertEqual( + repr(Comparator(5, delta=0.1)), + "Comparator(5, delta=0.1)", + ) + self.assertEqual(repr(Comparator(5)), "Comparator(5, delta=1e-07)") + self.assertEqual(str(Comparator(5)), "Comparator(5, delta=1e-07)") + + def test_addition_and_subtraction(self): + self.assertEqual(Comparator(5, delta=0.1) + 6, 11.1) + self.assertEqual(6 + Comparator(5, delta=0.1), 10.9) + self.assertNotEqual(Comparator(5, delta=0.1) + 6, 11.2) + self.assertNotEqual(6 + Comparator(5, delta=0.1) + 6, 10.8) + self.assertEqual(Comparator(7, delta=0.1) - 6, 1.05) + self.assertNotEqual(Comparator(7, delta=0.1) - 6, 1.2) + self.assertEqual(7 - Comparator(7, delta=0.1), 0.05) + self.assertNotEqual(7 - Comparator(7, delta=0.1), 0.11) + self.assertEqual(6 - Comparator(7, delta=0.1), -1.05) + + def test_arithmetic_and_comparisons_with_comparators(self): + five = Comparator(5, delta=0.1) + six = Comparator(6, delta=0.1) + seven = Comparator(7, delta=0.5) + self.assertEqual(five + six, 11.1) + self.assertNotEqual(five + six, 11.2) + self.assertEqual(five + seven, 12.1) + self.assertEqual(five + seven, 12.5) + self.assertEqual(seven + five, 12.5) + self.assertNotEqual(five + seven, 12.6) + + +class RomanNumeralTests(unittest.TestCase): + """Tests for RomanNumeral.""" + + def verify(self, integer, numeral): + self.assertEqual(int(RomanNumeral(numeral)), integer) + self.assertNotEqual(int(RomanNumeral(numeral)), integer + 1) + self.assertNotEqual(int(RomanNumeral(numeral)), integer - 1) + + def test_single_digit(self): + self.verify(1, "I") + self.verify(5, "V") + self.verify(10, "X") + self.verify(50, "L") + self.verify(100, "C") + self.verify(500, "D") + self.verify(1000, "M") + + def test_two_digits_ascending(self): + self.verify(2, "II") + self.verify(6, "VI") + self.verify(11, "XI") + self.verify(15, "XV") + self.verify(20, "XX") + self.verify(60, "LX") + self.verify(101, "CI") + self.verify(105, "CV") + self.verify(110, "CX") + self.verify(150, "CL") + self.verify(550, "DL") + self.verify(600, "DC") + self.verify(1100, "MC") + self.verify(2000, "MM") + + def test_three_digits_ascending(self): + self.verify(3, "III") + self.verify(7, "VII") + self.verify(12, "XII") + self.verify(16, "XVI") + self.verify(21, "XXI") + self.verify(25, "XXV") + self.verify(30, "XXX") + + def test_four_digits_ascending(self): + self.verify(8, "VIII") + self.verify(13, "XIII") + self.verify(17, "XVII") + self.verify(22, "XXII") + self.verify(26, "XXVI") + self.verify(31, "XXXI") + self.verify(35, "XXXV") + + def test_many_digits(self): + self.verify(1888, "MDCCCLXXXVIII") + + def test_subtractive(self): + self.verify(4, "IV") + self.verify(9, "IX") + self.verify(14, "XIV") + self.verify(19, "XIX") + self.verify(24, "XXIV") + self.verify(29, "XXIX") + self.verify(40, "XL") + self.verify(90, "XC") + self.verify(44, "XLIV") + self.verify(94, "XCIV") + self.verify(49, "XLIX") + self.verify(99, "XCIX") + self.verify(1999, "MCMXCIX") + self.verify(1948, "MCMXLVIII") + + def test_string_representation(self): + self.assertEqual(str(RomanNumeral("I")), "I") + self.assertEqual(repr(RomanNumeral("CD")), "RomanNumeral('CD')") + # Some conversion happens for some numbers + fourteen = RomanNumeral("XIIII") + self.assertEqual(str(fourteen), "XIV") + self.assertEqual(repr(fourteen), "RomanNumeral('XIV')") + + def test_adding(self): + sixty_five = RomanNumeral("LXV") + eighty_seven = RomanNumeral("LXXXVII") + self.assertEqual(int(sixty_five + eighty_seven), 152) + self.assertEqual(type(sixty_five + eighty_seven), RomanNumeral) + self.assertEqual(int(sixty_five + 87), 152) + self.assertEqual(type(sixty_five + 87), RomanNumeral) + self.assertEqual(str(sixty_five + 87), str("CLII")) + + def test_equality_and_ordering(self): + self.assertEqual(RomanNumeral("I"), 1) + self.assertNotEqual(RomanNumeral("I"), 2) + self.assertEqual(RomanNumeral("I"), "I") + self.assertLess(RomanNumeral("MCMXLVIII"), RomanNumeral("MCMXCIX")) + self.assertGreater(RomanNumeral("MCMXCIX"), RomanNumeral("MCMXLVIII")) + self.assertGreaterEqual(RomanNumeral("IX"), RomanNumeral("III")) + self.assertLessEqual(RomanNumeral("III"), RomanNumeral("IX")) + self.assertGreaterEqual(RomanNumeral("X"), RomanNumeral("X")) + self.assertLessEqual(RomanNumeral("IIII"), RomanNumeral("IV")) + self.assertFalse(RomanNumeral("V") < RomanNumeral("IV")) + self.assertFalse(RomanNumeral("V") > RomanNumeral("IX")) + self.assertFalse(RomanNumeral("V") <= RomanNumeral("IV")) + self.assertFalse(RomanNumeral("V") >= RomanNumeral("IX")) + with self.assertRaises(TypeError): + RomanNumeral("X") < "XX" + with self.assertRaises(TypeError): + RomanNumeral("X") <= "XX" + with self.assertRaises(TypeError): + RomanNumeral("X") > "XX" + with self.assertRaises(TypeError): + RomanNumeral("X") >= "XX" + self.assertFalse(RomanNumeral("V") < 4) + self.assertFalse(RomanNumeral("V") > 9) + self.assertFalse(RomanNumeral("V") <= 4) + self.assertFalse(RomanNumeral("V") >= 9) + with self.assertRaises(TypeError): + RomanNumeral("X") < "XX" + with self.assertRaises(TypeError): + RomanNumeral("X") <= "XX" + with self.assertRaises(TypeError): + RomanNumeral("X") > "XX" + with self.assertRaises(TypeError): + RomanNumeral("X") >= "XX" + + @unittest.skip("RomanNumeral from_int") + def test_from_int(self): + numeral = RomanNumeral.from_int(1) + self.assertEqual(numeral, "I") + self.assertEqual(numeral, 1) + self.assertEqual(type(numeral), RomanNumeral) + self.assertEqual(str(RomanNumeral.from_int(10)), "X") + self.assertEqual(str(RomanNumeral.from_int(21)), "XXI") + self.assertEqual(str(RomanNumeral.from_int(600)), "DC") + self.assertEqual(str(RomanNumeral.from_int(2000)), "MM") + self.assertEqual(str(RomanNumeral.from_int(12)), "XII") + self.assertEqual(str(RomanNumeral.from_int(25)), "XXV") + self.assertEqual(str(RomanNumeral.from_int(6)), "VI") + self.assertEqual(str(RomanNumeral.from_int(4)), "IV") + self.assertEqual(str(RomanNumeral.from_int(9)), "IX") + self.assertEqual(str(RomanNumeral.from_int(14)), "XIV") + self.assertEqual(str(RomanNumeral.from_int(1888)), "MDCCCLXXXVIII") + self.assertEqual(str(RomanNumeral.from_int(1999)), "MCMXCIX") + self.assertEqual(str(RomanNumeral.from_int(1948)), "MCMXLVIII") + + +class TimerTests(unittest.TestCase): + """Tests for Timer.""" + + _baseline = None + + @staticmethod + def get_baseline(count=5): + times = 0 + for i in range(count): + with Timer() as timer: + sleep(0) + times += timer.elapsed + return times / count + + def assertTimeEqual(self, actual, expected): + if self._baseline is None: + self._baseline = self.get_baseline() + self.assertAlmostEqual(actual, self._baseline + expected, delta=0.005) + + def test_short_time(self): + with Timer() as timer: + sleep(0.01) + self.assertGreater(timer.elapsed, 0.01) + self.assertLess(timer.elapsed, 1) + + def test_very_short_time(self): + with Timer() as timer: + pass + self.assertTimeEqual(timer.elapsed, 0) + + def test_two_timers(self): + with Timer() as timer1: + sleep(0.005) + with Timer() as timer2: + sleep(0.005) + sleep(0.005) + self.assertLess(timer2.elapsed, timer1.elapsed) + + def test_reusing_same_timer(self): + timer = Timer() + with timer: + sleep(0.0005) + elapsed1 = timer.elapsed + with timer: + sleep(0.004) + self.assertLess(elapsed1, timer.elapsed) + + +class FancyDictTests(unittest.TestCase): + """Tests for FancyDict.""" + + def test_constructor(self): + FancyDict() + FancyDict({"a": 2, "b": 3}) + + def test_key_access(self): + d = FancyDict({"a": 2, "b": 3}) + self.assertEqual(d["a"], 2) + self.assertEqual(d["b"], 3) + + def test_attribute_access(self): + d = FancyDict({"a": 2, "b": 3}) + self.assertEqual(d.a, 2) + self.assertEqual(d.b, 3) + + def test_original_dictionary_unchanged(self): + mapping = {"a": 2, "b": 3} + d = FancyDict(mapping) + d.c = 4 + self.assertEqual(mapping, {"a": 2, "b": 3}) + + def test_allow_setting_keys_and_attributes(self): + d = FancyDict({"a": 2, "b": 3}) + d["a"] = 4 + self.assertEqual(d["a"], 4) + self.assertEqual(d.a, 4) + d.c = 9 + self.assertEqual(d["c"], 9) + self.assertEqual(d.c, 9) + self.assertEqual(d["b"], 3) + x = FancyDict() + y = FancyDict() + x.a = 4 + y.a = 5 + self.assertEqual(x.a, 4) + + def test_keyword_arguments_equality_and_get_method(self): + d = FancyDict(a=2, b=3, c=4, d=5) + self.assertEqual(d.a, 2) + self.assertEqual(d.b, 3) + self.assertEqual(d["c"], 4) + self.assertEqual(d["d"], 5) + x = FancyDict({"a": 2, "b": 3}) + y = FancyDict({"a": 2, "b": 4}) + self.assertNotEqual(x, y) + y.b = 3 + self.assertEqual(x, y) + x.c = 5 + self.assertNotEqual(x, y) + y.c = 5 + self.assertEqual(x, y) + self.assertIsNone(y.get("d")) + self.assertEqual(y.get("c"), 5) + self.assertEqual(y.get("d", 5), 5) + + def test_keys_values_items_containment_and_length(self): + d = FancyDict(a=2, b=3, c=4, d=5) + self.assertEqual(set(d.keys()), {"a", "b", "c", "d"}) + self.assertEqual(set(d.values()), {2, 3, 4, 5}) + self.assertEqual( + set(d.items()), + {("a", 2), ("b", 3), ("c", 4), ("d", 5)}, + ) + self.assertEqual(len(d), 4) + self.assertTrue("a" in d) + self.assertFalse("a" not in d) + self.assertFalse("e" in d) + self.assertTrue("e" not in d) + self.assertNotIn("get", d) + with self.assertRaises(KeyError): + d["get"] + with self.assertRaises(KeyError): + d["keys"] + self.assertNotIn("values", d) + self.assertNotIn("setdefault", d) + self.assertEqual(d.pop("b", None), 3) + self.assertNotIn("b", d) + self.assertEqual(d.pop("b", None), None) + self.assertNotIn("b", d) + with self.assertRaises(KeyError): + d.pop("b") + + def test_normalize_attribute(self): + d = FancyDict({"greeting 1": "hi"}, normalize=True) + self.assertEqual(d["greeting 1"], "hi") + self.assertEqual(d.greeting_1, "hi") + d.greeting_2 = "hello" + self.assertEqual(d["greeting 2"], "hello") + self.assertEqual(d.greeting_2, "hello") + d["greeting 2"] = "hey" + self.assertEqual(d["greeting 2"], "hey") + self.assertEqual(d.get("greeting 2"), "hey") + self.assertEqual(d.greeting_2, "hey") + with self.assertRaises(AttributeError): + d.greeting2 + d = FancyDict({"greeting 1": "hi"}) + self.assertEqual(d["greeting 1"], "hi") + with self.assertRaises(AttributeError): + d.greeting_1 + + def test_dir(self): + d = FancyDict(a=2, b=3, c=4, d=5) + self.assertIn("a", dir(d)) + self.assertIn("b", dir(d)) + self.assertIn("c", dir(d)) + self.assertIn("d", dir(d)) + + +class ReloopableTests(unittest.TestCase): + """Tests for reloopable.""" + + one_line = "hello\n" + + two_lines = "line 1\nline 2\n" + + no_final_newline = "line 1\nline 2\nline 3" + + simone = dedent( + """ + Picket lines, school boycotts + They try to say it's a communist plot + All I want is equality + For my sister, my brother, my people, and me + """.lstrip( + "\n" + ) + ) + + many_lines = "This is a file\n" * 1000 + + empty = "" + + def test_empty_file(self): + f = StringIO(self.empty) + reloop = reloopable(f) + self.assertEqual(list(reloop), []) + self.assertEqual(list(reloop), []) + self.assertEqual(list(reloop), []) + + def test_one_line(self): + f = StringIO(self.one_line) + reloop = reloopable(f) + self.assertEqual(list(reloop), ["hello\n"]) + self.assertEqual(list(reloop), ["hello\n"]) + self.assertEqual(list(reloop), ["hello\n"]) + + def test_two_lines(self): + f = StringIO(self.two_lines) + reloop = reloopable(f) + self.assertEqual(list(reloop), ["line 1\n", "line 2\n"]) + self.assertEqual(list(reloop), ["line 1\n", "line 2\n"]) + self.assertEqual(list(reloop), ["line 1\n", "line 2\n"]) + + def test_no_final_newlines(self): + f = StringIO(self.no_final_newline) + reloop = reloopable(f) + self.assertEqual(list(reloop), ["line 1\n", "line 2\n", "line 3"]) + self.assertEqual(list(reloop), ["line 1\n", "line 2\n", "line 3"]) + self.assertEqual(list(reloop), ["line 1\n", "line 2\n", "line 3"]) + + def test_many_lines(self): + f = StringIO(self.many_lines) + reloop = reloopable(f) + self.assertEqual(list(reloop), ["This is a file\n"] * 1000) + self.assertEqual(list(reloop), ["This is a file\n"] * 1000) + self.assertEqual(list(reloop), ["This is a file\n"] * 1000) + + def test_data_in_file_is_not_stored(self): + f = StringIO(self.many_lines) + reloop = reloopable(f) + + self.assertEqual(list(reloop), ["This is a file\n"] * 1000) + + # Put new contents in the file + f.seek(0) + f.write(self.two_lines) + f.truncate() + + self.assertEqual(list(reloop), ["line 1\n", "line 2\n"]) + + +def get_size(obj, seen=None): + """Return size of any Python object.""" + if seen is None: + seen = set() + size = getsizeof(obj) + if id(obj) in seen: + return 0 + seen.add(id(obj)) + if hasattr(obj, "__dict__"): + size += get_size(obj.__dict__, seen) + if hasattr(obj, "__slots__"): + size += sum( + get_size(getattr(obj, attr), seen) for attr in obj.__slots__ if hasattr(obj, attr) + ) + if isinstance(obj, Mapping): + size += sum(get_size(k, seen) + get_size(v, seen) for k, v in obj.items()) + elif isinstance(obj, Iterable) and not isinstance(obj, (str, bytes)): + size += sum(get_size(item, seen) for item in obj) + return size + + +if __name__ == "__main__": + unittest.main(verbosity=2) + +if __name__ == "__main__": + from helpers import error_message + + error_message() diff --git a/scripts/intermediate_oop/helpers.py b/scripts/intermediate_oop/helpers.py new file mode 100755 index 0000000..ae3c71a --- /dev/null +++ b/scripts/intermediate_oop/helpers.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +"""Test helpers.""" +import sys + + +def error_message(): + print("Cannot run {} from the command-line.".format(sys.argv[0])) + print() + print("Run python test.py instead") diff --git a/scripts/intermediate_oop/inheritance.py b/scripts/intermediate_oop/inheritance.py new file mode 100755 index 0000000..faee490 --- /dev/null +++ b/scripts/intermediate_oop/inheritance.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +"""Inheritance exercises.""" + + +class CyclicList: + """Class with list-like structure that loops cyclicly.""" + + +class EasyDict: + """Class which allows both attribute and get/set item syntax.""" + + +class MinimumBalanceAccount: + """Bank account which does not allow balance to drop below zero.""" + + +class Node: + """Nodes for use in making hierarchies or trees.""" + + def __init__(self, name, *, ancestors=[]): + self.ancestors = list(ancestors) + self.name = name + + def ancestors_and_self(self): + """Return iterable with our ordered ancestors and our own node.""" + return [*self.ancestors, self] + + def make_child(self, *args, **kwargs): + """Create and return a child node of the current node.""" + return type(self)(*args, ancestors=self.ancestors_and_self(), **kwargs) + + def __str__(self): + """Return a slash-delimited ancestors hierarchy for this node.""" + return " / ".join([node.name for node in [*self.ancestors, self]]) + + def __repr__(self): + return self.name + + +class DoublyLinkedNode: + """Class with Nodes that are doubly-linked.""" + + +class Tree: + """Tree-like object.""" + + +class FieldTrackerMixin: + """Mixin for tracking specific attribute changes.""" + + +class LastUpdatedDictionary: + """Dictionary that maintains last-updated order of items.""" + + +class OrderedCounter: + """Counter that maintains last-updated item order.""" + + +class MaxCounter: + """Counter-like class that allows retrieving all maximums.""" diff --git a/scripts/intermediate_oop/inheritance_test.py b/scripts/intermediate_oop/inheritance_test.py new file mode 100755 index 0000000..c727887 --- /dev/null +++ b/scripts/intermediate_oop/inheritance_test.py @@ -0,0 +1,463 @@ +# -*- coding: utf-8 -*- +"""Tests for inheritance exercises.""" +import unittest + +from inheritance import ( + CyclicList, + DoublyLinkedNode, + EasyDict, + FieldTrackerMixin, + LastUpdatedDictionary, + MaxCounter, + MinimumBalanceAccount, + Node, + OrderedCounter, + Tree, +) + + +class CyclicListTests(unittest.TestCase): + """Tests for CyclicList.""" + + def test_constructor(self): + CyclicList([1, 2, 3, 4]) + + def test_accepts_non_lists(self): + numbers = CyclicList({1, 2, 3}) + self.assertEqual(next(iter(numbers)), 1) + letters = CyclicList("hello") + self.assertEqual(next(iter(letters)), "h") + + def test_iterate_to_length(self): + numbers = CyclicList([1, 2, 3]) + i = iter(numbers) + self.assertEqual([next(i), next(i), next(i)], [1, 2, 3]) + + def test_iterate_past_length(self): + numbers = CyclicList([1, 2, 3]) + new_list = [x for x, _ in zip(numbers, range(10))] + self.assertEqual(new_list, [1, 2, 3, 1, 2, 3, 1, 2, 3, 1]) + + def test_iterators_are_independent(self): + numbers = CyclicList([1, 2, 3, 4]) + i1 = iter(numbers) + i2 = iter(numbers) + self.assertEqual(next(i1), 1) + self.assertEqual(next(i1), 2) + self.assertEqual(next(i2), 1) + self.assertEqual(next(i2), 2) + + def test_length_append_and_pop(self): + numbers = CyclicList([1, 2, 3]) + self.assertEqual(len(numbers), 3) + numbers.append(4) + self.assertEqual(numbers.pop(), 4) + self.assertEqual(numbers.pop(0), 1) + + def test_supports_indexing(self): + numbers = CyclicList([1, 2, 3, 4]) + self.assertEqual(numbers[2], 3) + numbers = CyclicList([1, 2, 3, 4]) + self.assertEqual(numbers[4], 1) + self.assertEqual(numbers[-1], 4) + numbers[5] = 0 + self.assertEqual(numbers[1], 0) + + +class EasyDictTests(unittest.TestCase): + """Tests for EasyDict.""" + + def test_constructor(self): + EasyDict() + EasyDict({"a": 2, "b": 3}) + + def test_key_access(self): + d = EasyDict({"a": 2, "b": 3}) + self.assertEqual(d["a"], 2) + self.assertEqual(d["b"], 3) + + def test_attribute_access(self): + d = EasyDict({"a": 2, "b": 3}) + self.assertEqual(d.a, 2) + self.assertEqual(d.b, 3) + + def test_keyword_arguments(self): + d = EasyDict(a=2, b=3, c=4, d=5) + self.assertEqual(d.a, 2) + self.assertEqual(d.b, 3) + self.assertEqual(d["c"], 4) + self.assertEqual(d["d"], 5) + + def test_equality(self): + x = EasyDict({"a": 2, "b": 3}) + y = EasyDict({"a": 2, "b": 4}) + self.assertNotEqual(x, y) + self.assertNotEqual(x, {"a": 2, "b": 4}) + self.assertEqual(y, {"a": 2, "b": 4}) + y = EasyDict({"a": 2, "b": 3}) + self.assertEqual(x, y) + x = EasyDict({"a": 2, "b": 3, "c": 5}) + self.assertNotEqual(x, y) + y = EasyDict({"a": 2, "b": 3, "c": 5}) + self.assertEqual(x, y) + self.assertNotEqual(x, (1, 2)) + + def test_get_method(self): + x = EasyDict({"a": 2, "b": 4}) + self.assertIsNone(x.get("d")) + self.assertEqual(x.get("b"), 4) + self.assertEqual(x.get("c", 5), 5) + + def test_original_dictionary_unchanged(self): + mapping = {"a": 2, "b": 3} + d = EasyDict(mapping) + mapping["c"] = 4 + self.assertEqual(d, {"a": 2, "b": 3}) + + +class MinimumBalanceAccountTests(unittest.TestCase): + """Tests for MinimumBalanceAccount.""" + + def test_withdraw_from_new_account(self): + account = MinimumBalanceAccount() + with self.assertRaises(ValueError): + account.withdraw(1) + + def test_exception_message(self): + account = MinimumBalanceAccount() + with self.assertRaises(ValueError) as cm: + account.withdraw(1000) + self.assertEqual(str(cm.exception), "Balance cannot be less than $0") + + def test_withdraw_above_zero(self): + account = MinimumBalanceAccount() + account.deposit(100) + account.withdraw(99) + self.assertEqual(account.balance, 1) + + def test_withdraw_to_exactly_zero(self): + account = MinimumBalanceAccount() + account.deposit(100) + account.withdraw(100) + self.assertEqual(account.balance, 0) + + def test_withdraw_to_below_zero(self): + account = MinimumBalanceAccount() + account.deposit(100) + with self.assertRaises(ValueError): + account.withdraw(101) + + def test_repr(self): + account = MinimumBalanceAccount() + self.assertEqual(repr(account), "MinimumBalanceAccount(balance=0)") + + +class NodeTests(unittest.TestCase): + """Tests for Node.""" + + def test_single_node(self): + self.assertEqual(str(Node("A")), "A") + + def test_multiple_nodes(self): + expected = ( + "Animalia / Chordata / Mammalia / Carnivora / Ailuridae " "/ Ailurus / A. fulgens" + ) + red_panda = ( + Node("Animalia") + .make_child("Chordata") + .make_child("Mammalia") + .make_child("Carnivora") + .make_child("Ailuridae") + .make_child("Ailurus") + .make_child("A. fulgens") + ) + self.assertEqual(str(red_panda), expected) + + +class DoublyLinkedNodeTests(unittest.TestCase): + """Tests for DoublyLinkedNode.""" + + def test_single_node(self): + t = DoublyLinkedNode("A") + leaves = [node.name for node in t.leaves()] + self.assertEqual(leaves, ["A"]) + self.assertIs(t.is_leaf(), True) + + def test_multiple_nodes(self): + root = DoublyLinkedNode("A") + child1 = root.make_child("1") + grandchild1 = child1.make_child("a") + grandchild2 = child1.make_child("b") + child2 = root.make_child("2") + leaves0 = [node.name for node in root.leaves()] + leaves1 = [node.name for node in child1.leaves()] + leaves2 = [node.name for node in child2.leaves()] + self.assertEqual(leaves0, ["a", "b", "2"]) + self.assertEqual(leaves1, ["a", "b"]) + self.assertEqual(leaves2, ["2"]) + self.assertIs(grandchild1.is_leaf(), True) + self.assertIs(grandchild2.is_leaf(), True) + self.assertIs(child1.is_leaf(), False) + self.assertIs(child2.is_leaf(), True) + + +class DBModel: + def __init__(self, **kwargs): + self.id = None + for name, value in kwargs.items(): + setattr(self, name, value) + + def save(self): + self.id = 4 # This would be auto-generated normally + self.stored = True # Pretending to put stuff in a database + + +class TreeTests(unittest.TestCase): + """Tests for Tree.""" + + def test_set_and_delete_item(self): + felidae = Tree() + felidae["panthera"] = ["lion"] + felidae["felis"] = ["cat"] + self.assertEqual(felidae["panthera"], ["lion"]) + self.assertEqual(felidae["felis"], ["cat"]) + del felidae["felis"] + self.assertNotEqual(felidae["felis"], ["cat"]) + + def test_get_missing_item(self): + artiodactyla = Tree() + cetacea = artiodactyla["cetacea"] + self.assertEqual(artiodactyla["cetacea"], cetacea) + self.assertIsNot(artiodactyla["camelids"], cetacea) + + def test_modifying_deeply_nested_items(self): + mammals = Tree() + mammals["carnivora"]["canidae"]["canis"] = ["coyote"] + mammals["carnivora"]["canidae"]["canis"].append("wolf") + self.assertEqual( + mammals["carnivora"]["canidae"]["canis"], + ["coyote", "wolf"], + ) + + def test_repr(self): + mammals = Tree() + mammals["artiodactyla"]["camelidae"]["lama"] = ["Guanaco", "llama"] + dictionary = { + "artiodactyla": { + "camelidae": { + "lama": ["Guanaco", "llama"], + }, + }, + } + self.assertIn("artiodactyla", repr(mammals)) + self.assertIn("camelidae", repr(mammals)) + self.assertIn("lama", repr(mammals)) + self.assertIn("['Guanaco', 'llama']", repr(mammals)) + + def test_getting_and_setting_and_deleting_attributes(self): + mammals = Tree() + + # Accessing as attributes + mammals["artiodactyla"]["camelidae"]["lama"] = ["Guanaco", "llama"] + self.assertEqual( + mammals.artiodactyla.camelidae.lama, + ["Guanaco", "llama"], + ) + + # Assigning as attributes + mammals.carnivora.canidae.canis = ["coyote"] + mammals["carnivora"]["canidae"]["canis"].append("wolf") + self.assertEqual( + mammals["carnivora"]["canidae"]["canis"], + ["coyote", "wolf"], + ) + self.assertEqual( + mammals.carnivora.canidae.canis, + ["coyote", "wolf"], + ) + + def test_initialize_and_update_should_copy(self): + # Initializing tree-of-trees with dict-of-dicts + mammals = Tree( + { + "artiodactyla": { + "camelidae": { + "lama": ["Guanaco", "llama"], + }, + }, + } + ) + self.assertEqual( + mammals.artiodactyla.camelidae.lama, + ["Guanaco", "llama"], + ) + mammals.carnivora.canidae.canis = ["coyote", "wolf"] + self.assertEqual( + mammals["carnivora"]["canidae"]["canis"], + ["coyote", "wolf"], + ) + self.assertEqual( + mammals.carnivora.canidae.canis, + ["coyote", "wolf"], + ) + + # Updating with dict-of-dicts + mammals.update( + { + "carnivora": { + "prionodontidae": {"prionodon": ["pardicolor", "linsang"]}, + "canidae": { + "otocyon": ["megalotis"], + }, + }, + } + ) + self.assertEqual( + mammals.carnivora.prionodontidae.prionodon, + ["pardicolor", "linsang"], + ) + self.assertEqual(mammals.carnivora.canidae.otocyon, ["megalotis"]) + + # Still has the previous values also + self.assertEqual( + mammals.carnivora.canidae.canis, + ["coyote", "wolf"], + ) + + +class FieldTrackerMixinTests(unittest.TestCase): + """Tests for FieldTrackerMixin.""" + + def test_initializer(self): + class Person(FieldTrackerMixin, DBModel): + fields = ("id", "name", "email") + + trey = Person(name="Trey", email="trey@trey.com") + self.assertEqual(trey.name, "Trey") + self.assertEqual(trey.email, "trey@trey.com") + self.assertIsNone(trey.id) + + def test_previous_pre_and_post_save(self): + class Person(FieldTrackerMixin, DBModel): + fields = ("id", "name", "email") + + trey = Person(name="Trey", email="trey@trey.com") + self.assertEqual(trey.previous("email"), "trey@trey.com") + trey.email = "trey@gmail.com" + self.assertEqual(trey.previous("email"), "trey@trey.com") + trey.save() + self.assertEqual(trey.previous("email"), "trey@gmail.com") + self.assertEqual(trey.name, "Trey") + self.assertEqual(trey.email, "trey@gmail.com") + self.assertEqual(trey.id, 4) + + def test_has_changed_pre_and_post_save(self): + class Person(FieldTrackerMixin, DBModel): + fields = ("id", "name", "email") + + trey = Person(name="Trey", email="trey@trey.com") + self.assertFalse(trey.has_changed("email")) + trey.email = "trey@gmail.com" + self.assertTrue(trey.has_changed("email")) + trey.save() + self.assertFalse(trey.has_changed("email")) + self.assertEqual(trey.name, "Trey") + self.assertEqual(trey.email, "trey@gmail.com") + self.assertEqual(trey.id, 4) + + def test_changed_pre_and_post_save(self): + class Person(FieldTrackerMixin, DBModel): + fields = ("id", "name", "email") + + trey = Person(name="Trey", email="trey@trey.com") + self.assertEqual(trey.changed(), {}) + trey.email = "trey@gmail.com" + self.assertEqual(trey.changed(), {"email": "trey@trey.com"}) + trey.save() + self.assertEqual(trey.changed(), {}) + self.assertEqual(trey.name, "Trey") + self.assertEqual(trey.email, "trey@gmail.com") + self.assertEqual(trey.id, 4) + + +class LastUpdatedDictionaryTests(unittest.TestCase): + """Tests for LastUpdatedDictionary.""" + + def test_initial_order(self): + d = LastUpdatedDictionary([("a", 1), ("c", 3), ("b", 2), ("d", 4)]) + self.assertEqual(list(d.keys()), ["a", "c", "b", "d"]) + self.assertEqual(list(d.values()), [1, 3, 2, 4]) + + def test_order_after_insertion(self): + d = LastUpdatedDictionary([("a", 1), ("c", 3), ("b", 2), ("d", 4)]) + d["e"] = 5 + self.assertEqual(list(d.keys()), ["a", "c", "b", "d", "e"]) + self.assertEqual(list(d.values()), [1, 3, 2, 4, 5]) + + def test_order_after_update(self): + d = LastUpdatedDictionary([("a", 1), ("c", 3), ("b", 2), ("d", 4)]) + d["c"] = 0 + self.assertEqual(list(d.keys()), ["a", "b", "d", "c"]) + self.assertEqual(list(d.values()), [1, 2, 4, 0]) + + +class OrderedCounterTests(unittest.TestCase): + """Tests for OrderedCounter.""" + + def test_initial_order(self): + c = OrderedCounter("hello world") + self.assertEqual( + list(c.keys()), + ["h", "e", " ", "w", "o", "r", "l", "d"], + ) + self.assertEqual(list(c.values()), [1, 1, 1, 1, 2, 1, 3, 1]) + + def test_order_after_insertion(self): + c = OrderedCounter("hello world") + c.update("cat") + self.assertEqual( + list(c.keys()), + ["h", "e", " ", "w", "o", "r", "l", "d", "c", "a", "t"], + ) + self.assertEqual(list(c.values()), [1, 1, 1, 1, 2, 1, 3, 1, 1, 1, 1]) + + def test_order_after_update(self): + c = OrderedCounter("hello world") + c.update("hey") + self.assertEqual( + list(c.keys()), + [" ", "w", "o", "r", "l", "d", "h", "e", "y"], + ) + self.assertEqual(list(c.values()), [1, 1, 2, 1, 3, 1, 2, 2, 1]) + + +class MaxCounterTests(unittest.TestCase): + """Tests for MaxCounter.""" + + def test_works_like_counter(self): + counts = MaxCounter("hello") + self.assertEqual(counts, {"h": 1, "e": 1, "l": 2, "o": 1}) + self.assertEqual(counts["h"], 1) + self.assertEqual(counts["!"], 0) + + def test_single_maximum(self): + counts = MaxCounter("hello") + self.assertEqual(set(counts.max_keys()), {"l"}) + + def test_multiple_maximums(self): + counts = MaxCounter("no banana") + self.assertEqual(set(counts.max_keys()), {"a", "n"}) + + def test_all_maximums(self): + counts = MaxCounter("abcd") + self.assertEqual(set(counts.max_keys()), set("abcd")) + + def test_empty(self): + counts = MaxCounter("") + self.assertEqual(set(counts.max_keys()), set()) + + +if __name__ == "__main__": + from helpers import error_message + + error_message() diff --git a/scripts/intermediate_oop/initial.py b/scripts/intermediate_oop/initial.py new file mode 100755 index 0000000..473dda9 --- /dev/null +++ b/scripts/intermediate_oop/initial.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +"""Function for Initial Framework Test Check.""" + + +def is_ok(): + """Confirm Test Framework.""" + return ( + "Congrats and welcome to the Test Framework! \n" + "The message confirms the Test Framework is working! Yay! \n" + "Pat yourself on the back for successful installation! \n" + "Continue reading this section of the course instructions. \n" + "We will get started in a moment." + ) diff --git a/scripts/intermediate_oop/initial_test.py b/scripts/intermediate_oop/initial_test.py new file mode 100755 index 0000000..a35d58b --- /dev/null +++ b/scripts/intermediate_oop/initial_test.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +"""Test to confirm that the test framework is working.""" +import unittest + +from initial import is_ok + + +class InitialTests(unittest.TestCase): + """Tests for is_ok.""" + + def test_confirm_test(self): + """Test passed.""" + confirm = is_ok() + print(confirm) + + +if __name__ == "__main__": + from helpers import error_message + + error_message() diff --git a/scripts/intermediate_oop/main.py b/scripts/intermediate_oop/main.py new file mode 100644 index 0000000..0338c50 --- /dev/null +++ b/scripts/intermediate_oop/main.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +from classes import BankAccount + +trey_account = BankAccount(20) + +print(trey_account.balance) +# 20 + +trey_account.deposit(100) +print(trey_account.balance) + +# trey_account.balance +# 120 + +trey_account.withdraw(40) +print(trey_account.balance) + +# trey_account.balance +# 80 +print(trey_account) +# BankAccount(balance=80) + +print(repr(trey_account)) + +mary_account = BankAccount(100) +print(mary_account.balance) + +dana_account = BankAccount() +print(dana_account.balance) + +mary_account.transfer(dana_account, 20) + +print(mary_account.balance) +print(dana_account.balance) + +print("Print all attributes") +print(trey_account.__dict__) diff --git a/scripts/intermediate_oop/properties.py b/scripts/intermediate_oop/properties.py new file mode 100755 index 0000000..5a59432 --- /dev/null +++ b/scripts/intermediate_oop/properties.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +"""Property exercises.""" + + +class Circle: + """Circle with radius, area, etc.""" + + +class Vector: + """Class representing a 3 dimensional vector.""" + + +class Person: + """Person with first and last name.""" diff --git a/scripts/intermediate_oop/properties_test.py b/scripts/intermediate_oop/properties_test.py new file mode 100755 index 0000000..c5f2917 --- /dev/null +++ b/scripts/intermediate_oop/properties_test.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +"""Tests for property exercises.""" +import math +import unittest + +from properties import Circle, Person, Vector + + +class CircleTests(unittest.TestCase): + """Tests for Circle.""" + + def test_radius(self): + circle = Circle(5) + self.assertEqual(circle.radius, 5) + + def test_default_radius(self): + circle = Circle() + self.assertEqual(circle.radius, 1) + + def test_diameter_changes(self): + circle = Circle(2) + self.assertEqual(circle.diameter, 4) + circle.radius = 3 + self.assertEqual(circle.diameter, 6) + + def test_set_diameter(self): + circle = Circle(2) + self.assertEqual(circle.diameter, 4) + circle.diameter = 3 + self.assertEqual(circle.radius, 1.5) + + def test_area(self): + circle = Circle(2) + self.assertEqual(circle.area, math.pi * 4) + + @unittest.skip("Log Radius Changes") + def test_radius_changes_logged(self): + circle = Circle(2) + self.assertEqual(circle.radius_changes, [2]) + circle.radius = 3 + self.assertEqual(circle.radius_changes, [2, 3]) + circle.diameter = 3 + self.assertEqual(circle.radius_changes, [2, 3, 1.5]) + + @unittest.skip("Set Radius Error") + def test_no_negative_radius(self): + circle = Circle(2) + with self.assertRaises(ValueError) as context: + circle.radius = -10 + self.assertEqual(str(context.exception), "Radius cannot be negative!") + + +class VectorTests(unittest.TestCase): + """Tests for Vector.""" + + def test_attributes(self): + v = Vector(1, 2, 3) + self.assertEqual((v.x, v.y, v.z), (1, 2, 3)) + + def test_magnitude_property(self): + v = Vector(2, 3, 6) + self.assertEqual(v.magnitude, 7.0) + try: + v.y = 9 + except AttributeError: + v = Vector(2, 9, 6) + self.assertEqual(v.magnitude, 11.0) + + def test_no_weird_extras(self): + v1 = Vector(1, 2, 3) + v2 = Vector(4, 5, 6) + with self.assertRaises(TypeError): + len(v1) + with self.assertRaises(TypeError): + v1 < v2 + with self.assertRaises(TypeError): + v1 > v2 + with self.assertRaises(TypeError): + v1 <= v2 + with self.assertRaises(TypeError): + v1 >= v2 + with self.assertRaises(TypeError): + v1 + (1, 2, 3) + with self.assertRaises(TypeError): + (1, 2, 3) + v1 + with self.assertRaises(TypeError): + v1 - (1, 2, 3) + with self.assertRaises(TypeError): + v1 * "a" + with self.assertRaises(TypeError): + v1 / v2 + + @unittest.skip("Vector Equality") + def test_equality_and_inequality(self): + self.assertNotEqual(Vector(1, 2, 3), Vector(1, 2, 4)) + self.assertEqual(Vector(1, 2, 3), Vector(1, 2, 3)) + self.assertFalse(Vector(1, 2, 3) != Vector(1, 2, 3)) + v1 = Vector(1, 2, 3) + v2 = Vector(1, 2, 4) + v3 = Vector(1, 2, 3) + self.assertNotEqual(v1, v2) + self.assertEqual(v1, v3) + + @unittest.skip("Vector Adding") + def test_shifting(self): + v1 = Vector(1, 2, 3) + v2 = Vector(4, 5, 6) + v3 = v2 + v1 + v4 = v3 - v1 + self.assertEqual((v3.x, v3.y, v3.z), (5, 7, 9)) + self.assertEqual((v4.x, v4.y, v4.z), (v2.x, v2.y, v2.z)) + + @unittest.skip("Vector Multiplying") + def test_scaling(self): + v1 = Vector(1, 2, 3) + v2 = Vector(4, 5, 6) + v3 = v1 * 4 + v4 = 2 * v2 + self.assertEqual((v3.x, v3.y, v3.z), (4, 8, 12)) + self.assertEqual((v4.x, v4.y, v4.z), (8, 10, 12)) + + @unittest.skip("Vector Iterability") + def test_multiple_assignment(self): + x, y, z = Vector(x=1, y=2, z=3) + self.assertEqual((x, y, z), (1, 2, 3)) + + @unittest.skip("Vector Immutability") + def test_immutability(self): + v1 = Vector(1, 2, 3) + with self.assertRaises(Exception): + v1.x = 4 + self.assertEqual(v1.x, 1) + + +class PersonTests(unittest.TestCase): + """Tests for Person.""" + + def test_construct(self): + Person("Trey", "Hunner") + + def test_first_and_last_name_attributes(self): + trey = Person("Trey", "Hunner") + self.assertEqual(trey.first_name, "Trey") + self.assertEqual(trey.last_name, "Hunner") + + def test_name_attribute(self): + trey = Person("Trey", "Hunner") + self.assertEqual(trey.name, "Trey Hunner") + + def test_change_names(self): + trey = Person("Trey", "Hunner") + trey.last_name = "Smith" + self.assertEqual(trey.name, "Trey Smith") + trey.first_name = "John" + self.assertEqual(trey.name, "John Smith") + + +if __name__ == "__main__": + from helpers import error_message + + error_message() diff --git a/scripts/intermediate_oop/refactoring.py b/scripts/intermediate_oop/refactoring.py new file mode 100755 index 0000000..2f6b13a --- /dev/null +++ b/scripts/intermediate_oop/refactoring.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +"""Refactoring exercises.""" +from email.parser import Parser + +try: + # Some versions of Anaconda are missing IMAP4_SSL + from imaplib import IMAP4_SSL +except ImportError: + pass + + +class Weekday: + """Class with attributes representing weekdays.""" + + +class NextDate: + """Answers questions about the next Monday/Tuesday/etc.""" + + +def next_date(): + """Returns next Monday/Tuesday/etc.""" + + +def days_until(): + """Returns days until next Monday/Tuesday/etc.""" + + +def next_tuesday(): + """Returns date of next Tuesday.""" + + +def days_to_tuesday(): + """Returns days until next Tuesday.""" + + +class IMAPChecker: + """Facilitate connection to IMAP server.""" + + +# Refactor the below functions into IMAPChecker methods + + +def get_connection(host, username, password): + """Initialize IMAP server and login.""" + server = IMAP4_SSL(host) + server.login(username, password) + server.select("inbox") + return server + + +def close_connection(server): + server.close() + server.logout() + + +def get_message_uids(server): + """Return unique identifiers for each message.""" + return server.uid("search", None, "ALL")[1][0].split() + + +def get_message(server, uid): + """Get email message identified by given UID.""" + result, data = server.uid("fetch", uid, "(RFC822)") + (_, message_text), _ = data + message = Parser().parsestr(message_text) + return message diff --git a/scripts/intermediate_oop/refactoring_test.py b/scripts/intermediate_oop/refactoring_test.py new file mode 100755 index 0000000..9cd0dab --- /dev/null +++ b/scripts/intermediate_oop/refactoring_test.py @@ -0,0 +1,300 @@ +# -*- coding: utf-8 -*- +"""Tests for refactoring exercises.""" +import unittest +from contextlib import contextmanager +from datetime import date +from unittest.mock import call, patch, sentinel + +from refactoring import IMAPChecker, Weekday + + +class NextDateTests(unittest.TestCase): + """Tests for NextDate.""" + + def setUp(self): + self.patched_date = patch_date(2019, 9, 3, 10, 30) + self.set_date = self.patched_date.__enter__() + + def tearDown(self): + self.patched_date.__exit__(None, None, None) + + def test_date_for_changing_time(self): + monday = NextDate(Weekday.MONDAY) + tuesday = NextDate(Weekday.TUESDAY) + wednesday = NextDate(Weekday.WEDNESDAY) + thursday = NextDate(Weekday.THURSDAY) + friday = NextDate(Weekday.FRIDAY) + saturday = NextDate(Weekday.SATURDAY) + sunday = NextDate(Weekday.SUNDAY) + self.set_date(2019, 9, 3) # Tuesday + self.assertEqual(monday.date(), date(2019, 9, 9)) + self.assertEqual(tuesday.date(), date(2019, 9, 3)) + self.assertEqual(wednesday.date(), date(2019, 9, 4)) + self.assertEqual(thursday.date(), date(2019, 9, 5)) + self.assertEqual(friday.date(), date(2019, 9, 6)) + self.assertEqual(saturday.date(), date(2019, 9, 7)) + self.assertEqual(sunday.date(), date(2019, 9, 8)) + self.set_date(2019, 9, 5) # Thursday + self.assertEqual(monday.date(), date(2019, 9, 9)) + self.assertEqual(tuesday.date(), date(2019, 9, 10)) + self.assertEqual(wednesday.date(), date(2019, 9, 11)) + self.assertEqual(thursday.date(), date(2019, 9, 5)) + self.assertEqual(friday.date(), date(2019, 9, 6)) + self.assertEqual(saturday.date(), date(2019, 9, 7)) + self.assertEqual(sunday.date(), date(2019, 9, 8)) + + def test_days_until(self): + self.set_date(2019, 9, 3) # Tuesday + self.assertEqual(NextDate(Weekday.MONDAY).days_until(), 6) + self.assertEqual(NextDate(Weekday.TUESDAY).days_until(), 0) + self.assertEqual(NextDate(Weekday.WEDNESDAY).days_until(), 1) + + +class NextDateFunctionTests(unittest.TestCase): + """Tests for next_date.""" + + def setUp(self): + self.patched_date = patch_date(2019, 9, 3, 10, 30) + self.set_date = self.patched_date.__enter__() + + def tearDown(self): + self.patched_date.__exit__(None, None, None) + + def test_next_date(self): + self.set_date(2019, 9, 3) # Tuesday + self.assertEqual(next_date(Weekday.MONDAY), date(2019, 9, 9)) + self.assertEqual(next_date(Weekday.TUESDAY), date(2019, 9, 3)) + self.assertEqual(next_date(Weekday.WEDNESDAY), date(2019, 9, 4)) + self.assertEqual(next_date(Weekday.THURSDAY), date(2019, 9, 5)) + self.assertEqual(next_date(Weekday.FRIDAY), date(2019, 9, 6)) + self.assertEqual(next_date(Weekday.SATURDAY), date(2019, 9, 7)) + self.assertEqual(next_date(Weekday.SUNDAY), date(2019, 9, 8)) + self.set_date(2019, 9, 5) # Thursday + self.assertEqual(next_date(Weekday.MONDAY), date(2019, 9, 9)) + self.assertEqual(next_date(Weekday.TUESDAY), date(2019, 9, 10)) + self.assertEqual(next_date(Weekday.WEDNESDAY), date(2019, 9, 11)) + self.assertEqual(next_date(Weekday.THURSDAY), date(2019, 9, 5)) + self.assertEqual(next_date(Weekday.FRIDAY), date(2019, 9, 6)) + self.assertEqual(next_date(Weekday.SATURDAY), date(2019, 9, 7)) + self.assertEqual(next_date(Weekday.SUNDAY), date(2019, 9, 8)) + + +class DaysUntilFunctionTests(unittest.TestCase): + """Tests for days_until.""" + + def setUp(self): + self.patched_date = patch_date(2019, 9, 3, 10, 30) + self.set_date = self.patched_date.__enter__() + + def tearDown(self): + self.patched_date.__exit__(None, None, None) + + def test_days_until(self): + self.set_date(2019, 9, 3) # Tuesday + self.assertEqual(days_until(Weekday.MONDAY), 6) + self.assertEqual(days_until(Weekday.TUESDAY), 0) + self.assertEqual(days_until(Weekday.WEDNESDAY), 1) + self.assertEqual(days_until(Weekday.THURSDAY), 2) + self.assertEqual(days_until(Weekday.FRIDAY), 3) + self.assertEqual(days_until(Weekday.SATURDAY), 4) + self.assertEqual(days_until(Weekday.SUNDAY), 5) + self.set_date(2019, 9, 5) # Thursday + self.assertEqual(days_until(Weekday.MONDAY), 4) + self.assertEqual(days_until(Weekday.TUESDAY), 5) + self.assertEqual(days_until(Weekday.WEDNESDAY), 6) + self.assertEqual(days_until(Weekday.THURSDAY), 0) + self.assertEqual(days_until(Weekday.FRIDAY), 1) + self.assertEqual(days_until(Weekday.SATURDAY), 2) + self.assertEqual(days_until(Weekday.SUNDAY), 3) + + +class DaysToTuesdayTests(unittest.TestCase): + """Tests for days_to_tuesday.""" + + def setUp(self): + self.patched_date = patch_date(2019, 9, 3, 10, 30) + self.set_date = self.patched_date.__enter__() + + def tearDown(self): + self.patched_date.__exit__(None, None, None) + + def test_days_to_tuesday(self): + self.set_date(2019, 9, 3) # Tuesday + self.assertEqual(days_to_tuesday(), 0) + self.assertEqual(days_to_tuesday(after_today=True), 7) + self.set_date(2019, 9, 5) # Thursday + self.assertEqual(days_to_tuesday(), 5) + self.assertEqual(days_to_tuesday(after_today=True), 5) + + +class NextTuesdayTests(unittest.TestCase): + """Tests for next_tuesday.""" + + def setUp(self): + self.patched_date = patch_date(2019, 9, 3, 10, 30) + self.set_date = self.patched_date.__enter__() + + def tearDown(self): + self.patched_date.__exit__(None, None, None) + + def test_next_tuesday(self): + self.set_date(2019, 9, 3) # Tuesday + self.assertEqual(next_tuesday(), date(2019, 9, 3)) + self.assertEqual(next_tuesday(after_today=True), date(2019, 9, 10)) + self.set_date(2019, 9, 5) # Thursday + self.assertEqual(next_tuesday(), date(2019, 9, 10)) + self.assertEqual(next_tuesday(after_today=True), date(2019, 9, 10)) + + +def NextDate(*args, **kwargs): + """Call a fresh import of the nextdate.NextDate class.""" + from importlib import reload + + import refactoring + + reload(refactoring) + return refactoring.NextDate(*args, **kwargs) + + +def next_date(*args, **kwargs): + """Call a fresh import of the nextdate.next_date function.""" + from importlib import reload + + import refactoring + + reload(refactoring) + return refactoring.next_date(*args, **kwargs) + + +def days_until(*args, **kwargs): + """Call a fresh import of the nextdate.days_until function.""" + from importlib import reload + + import refactoring + + reload(refactoring) + return refactoring.days_until(*args, **kwargs) + + +def days_to_tuesday(*args, **kwargs): + """Call a fresh import of the nextdate.days_to_tuesday function.""" + from importlib import reload + + import refactoring + + reload(refactoring) + return refactoring.days_to_tuesday(*args, **kwargs) + + +def next_tuesday(*args, **kwargs): + """Call a fresh import of the nextdate.next_tuesday function.""" + from importlib import reload + + import refactoring + + reload(refactoring) + return refactoring.next_tuesday(*args, **kwargs) + + +@contextmanager +def patch_date(year, month, day, hour=0, minute=0): + """Monkey patch the current time to be the given time.""" + import datetime + from unittest.mock import patch + + date_args = year, month, day + time_args = hour, minute + + class FakeDate(datetime.date): + """A datetime.date class with mocked today method.""" + + @classmethod + def today(cls): + return cls(*date_args) + + class FakeDateTime(datetime.datetime): + """A datetime.datetime class with mocked today, now methods.""" + + @classmethod + def today(cls): + return cls(*date_args, *time_args) + + @classmethod + def now(cls): + return cls.today() + + def set_date(year, month, day, *rest): + nonlocal date_args, time_args + date_args = year, month, day + time_args = rest + + with patch("datetime.datetime", FakeDateTime): + with patch("datetime.date", FakeDate): + yield set_date + + +class IMAPCheckerTests(unittest.TestCase): + """Tests for IMAPChecker.""" + + def test_initialization(self): + host = "example.com" + with patch("classes.IMAP4_SSL", autospec=True) as imap_mock: + IMAPChecker(host) + self.assertEqual(imap_mock.mock_calls, [call(host)]) + + def test_authentication(self): + host = "example.com" + username = "user@example.com" + password = "password" + with patch("classes.IMAP4_SSL", autospec=True) as imap_mock: + checker = IMAPChecker(host) + checker.authenticate(username, password) + self.assertEqual( + imap_mock.mock_calls, + [ + call(host), + call().login(username, password), + call().select("inbox"), + ], + ) + + def test_get_message_uids(self): + host = "example.com" + with patch("classes.IMAP4_SSL", autospec=True) as imap_mock: + checker = IMAPChecker(host) + uids = checker.get_message_uids() + imap_mock.assert_has_calls([call().uid("search", None, "ALL")]) + self.assertEqual( + uids, + ( + imap_mock.return_value.uid.return_value.__getitem__.return_value.__getitem__.return_value.split.return_value + ), + ) + + def test_get_message(self): + host = "example.com" + uid = "uid1" + with patch("classes.IMAP4_SSL", autospec=True) as imap_mock: + with patch("classes.Parser", autospec=True) as parser_mock: + imap_mock.return_value.uid.return_value = [ + "", + (("", sentinel.MessageText), ""), + ] + parser_mock.return_value.parsestr.return_value = sentinel.M + checker = IMAPChecker(host) + message = checker.get_message(uid) + self.assertEqual(imap_mock.mock_calls, [call(host), call().uid("fetch", uid, "(RFC822)")]) + self.assertEqual( + parser_mock.mock_calls, + [ + call(), + call().parsestr(sentinel.MessageText), + ], + ) + self.assertEqual(message, sentinel.M) + + +if __name__ == "__main__": + from helpers import error_message + + error_message() diff --git a/scripts/intermediate_oop/test.py b/scripts/intermediate_oop/test.py new file mode 100755 index 0000000..bf6ea7d --- /dev/null +++ b/scripts/intermediate_oop/test.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from __future__ import print_function + +import sys +import unittest + +from test_data import MODULES, TESTS + + +def get_test(obj_name): + if obj_name not in TESTS: + raise SystemExit("Test for {} doesn't exist.".format(obj_name)) + return unittest.defaultTestLoader.loadTestsFromName(TESTS[obj_name]) + + +def run_tests(tests): + test_suite = unittest.TestSuite(tests) + unittest.TextTestRunner().run(test_suite) + + +def print_object_names(): + for module, objects in MODULES.items(): + print("\n{}:\n".format(module)) + for obj in objects: + print(obj) + print() + + +def main(*arguments): + if not arguments: + print("Please select a thing to test") + print_object_names() + elif len(arguments) > 1: + print( + """ +Can only call test.py with one argument: the name of the exercise being tested + +Examples: + +- python test.py get_hypotenuse +- python test.py hello.py +- python test.py BankAccount + +This test script runs Trey's tests against your code. +The tests are written in files that end in "_test.py". + +If you'd like to test your code manually, you can either: + +1. Open a Python REPL, import your code, and execute it with specific arguments +2. Write your own test code at the bottom of your file (e.g. functions.py) and +run that file (e.g. "python functions.py"). + +Consult the website for instructions for running the exercises and ask Trey +for help when you get stuck. + """.strip() + ) + elif " " in arguments[0] or "(" in arguments[0] or "," in arguments[0]: + print("Invalid characters found: {}\n".format(arguments[0])) + print("This test script doesn't accept code, just an exercise name.\n") + print("Example usage:") + print("python test.py \n") + else: + [argument] = arguments + if argument.startswith(("modules/", "modules\\", "./modules/")): + argument = argument.split("/", 1)[1] + if argument == "--all": + arguments = list(TESTS) + else: + arguments = [argument] + tests = [get_test(arg) for arg in arguments] + print("Testing {}\n".format(", ".join(arguments))) + test_classes = set( + tuple(test.id().split(".")[:-1]) for suite in tests for test in suite._tests + ) + for module, cls in test_classes: + print("Running {} test class in {}.py\n".format(cls, module)) + run_tests(tests) + + +if __name__ == "__main__": + # Version check before all else + major, minor, micro, releaselevel, serial = sys.version_info + if (major, minor) < (3, 5): + print("You are running Python {0}.{1}".format(major, minor)) + print("Must use Python version 3.5 or above") + sys.exit(1) + main(*sys.argv[1:]) diff --git a/scripts/intermediate_oop/test_data.py b/scripts/intermediate_oop/test_data.py new file mode 100755 index 0000000..2e031d6 --- /dev/null +++ b/scripts/intermediate_oop/test_data.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +TESTS = { + "days_to_tuesday": "refactoring_test.DaysToTuesdayTests", + "days_until": "refactoring_test.DaysUntilFunctionTests", + "IMAPChecker": "refactoring_test.IMAPCheckerTests", + "next_date": "refactoring_test.NextDateFunctionTests", + "NextDate": "refactoring_test.NextDateTests", + "next_tuesday": "refactoring_test.NextTuesdayTests", + "is_ok": "initial_test.InitialTests", + "Comparator": "dunder_test.ComparatorTests", + "FancyDict": "dunder_test.FancyDictTests", + "reloopable": "dunder_test.ReloopableTests", + "ReverseView": "dunder_test.ReverseViewTests", + "RomanNumeral": "dunder_test.RomanNumeralTests", + "Timer": "dunder_test.TimerTests", + "CyclicList": "inheritance_test.CyclicListTests", + "DoublyLinkedNode": "inheritance_test.DoublyLinkedNodeTests", + "EasyDict": "inheritance_test.EasyDictTests", + "FieldTrackerMixin": "inheritance_test.FieldTrackerMixinTests", + "LastUpdatedDictionary": "inheritance_test.LastUpdatedDictionaryTests", + "MaxCounter": "inheritance_test.MaxCounterTests", + "MinimumBalanceAccount": "inheritance_test.MinimumBalanceAccountTests", + "Node": "inheritance_test.NodeTests", + "OrderedCounter": "inheritance_test.OrderedCounterTests", + "Tree": "inheritance_test.TreeTests", + "Circle": "properties_test.CircleTests", + "Person": "properties_test.PersonTests", + "Vector": "properties_test.VectorTests", + "BankAccount": "classes_test.BankAccountTests", + "Flavor": "classes_test.FlavorTests", + "IceCream": "classes_test.IceCreamTests", + "MinHeap": "classes_test.MinHeapTests", + "MonthDelta": "classes_test.MonthDeltaTests", + "Month": "classes_test.MonthTests", + "Row": "classes_test.RowTests", + "Size": "classes_test.SizeTests", + "SuperMap": "classes_test.SuperMapTests", +} + +MODULES = { + "classes": [ + "BankAccount", + "Flavor", + "IceCream", + "MinHeap", + "MonthDelta", + "Month", + "Row", + "Size", + "SuperMap", + ], + "dunder": ["Comparator", "FancyDict", "reloopable", "ReverseView", "RomanNumeral", "Timer"], + "inheritance": [ + "CyclicList", + "DoublyLinkedNode", + "EasyDict", + "FieldTrackerMixin", + "LastUpdatedDictionary", + "MaxCounter", + "MinimumBalanceAccount", + "Node", + "OrderedCounter", + "Tree", + ], + "initial": ["is_ok"], + "properties": ["Circle", "Person", "Vector"], + "refactoring": [ + "days_to_tuesday", + "days_until", + "IMAPChecker", + "next_date", + "NextDate", + "next_tuesday", + ], +} From 284cd6237e10abb2cf6715950ab719b78589596d Mon Sep 17 00:00:00 2001 From: Alvaro Ortiz Date: Thu, 1 Feb 2024 16:58:39 +0000 Subject: [PATCH 10/20] Solve some exercises in Intermediate OOP in python --- scripts/intermediate_oop/classes.py | 25 ++++++++++----- scripts/intermediate_oop/main.py | 42 +++++++++++++++++++++----- scripts/intermediate_oop/properties.py | 18 +++++++++++ 3 files changed, 70 insertions(+), 15 deletions(-) diff --git a/scripts/intermediate_oop/classes.py b/scripts/intermediate_oop/classes.py index fe018c5..0ebbaa8 100755 --- a/scripts/intermediate_oop/classes.py +++ b/scripts/intermediate_oop/classes.py @@ -1,28 +1,37 @@ # -*- coding: utf-8 -*- """Class exercises.""" +import math class BankAccount: """Bank account including an account balance.""" def __init__(self, balance=0): - self.balance = balance + self._balance = balance + self.transactions = [] + self.transactions.append(("OPEN", balance, balance)) + + @property + def balance(self): + return self._balance def deposit(self, quantity): - self.balance += quantity + self._balance += quantity + self.transactions.append(("DEPOSIT", quantity, self._balance)) def withdraw(self, quantity): - self.balance -= quantity + self._balance -= quantity + self.transactions.append(("WITHDRAWAL", -quantity, self._balance)) - def transfer(self, BankAccount, quantity): - BankAccount.balance += quantity - self.balance -= quantity + def transfer(self, other, quantity): + other._balance += quantity + self._balance -= quantity def __repr__(self): - return f"BankAccount(balance={self.balance})" + return f"BankAccount(balance={self._balance})" def __str__(self): - return f"A Bank Account with balance={self.balance})" + return f"A Bank Account with balance={self._balance})" class SuperMap: diff --git a/scripts/intermediate_oop/main.py b/scripts/intermediate_oop/main.py index 0338c50..ba32b3e 100644 --- a/scripts/intermediate_oop/main.py +++ b/scripts/intermediate_oop/main.py @@ -1,19 +1,20 @@ # -*- coding: utf-8 -*- from classes import BankAccount +from properties import Circle trey_account = BankAccount(20) -print(trey_account.balance) +print(trey_account._balance) # 20 trey_account.deposit(100) -print(trey_account.balance) +print(trey_account._balance) # trey_account.balance # 120 trey_account.withdraw(40) -print(trey_account.balance) +print(trey_account._balance) # trey_account.balance # 80 @@ -23,15 +24,42 @@ print(repr(trey_account)) mary_account = BankAccount(100) -print(mary_account.balance) +print(mary_account._balance) dana_account = BankAccount() -print(dana_account.balance) +print(dana_account._balance) mary_account.transfer(dana_account, 20) -print(mary_account.balance) -print(dana_account.balance) +print(mary_account._balance) +print(dana_account._balance) print("Print all attributes") print(trey_account.__dict__) + +my_account = BankAccount(10) + +my_account.deposit(100) + +my_account.withdraw(40) + +my_account.deposit(95) + +print(my_account.transactions) +[("OPEN", 10, 10), ("DEPOSIT", 100, 110), ("WITHDRAWAL", -40, 70), ("DEPOSIT", 95, 165)] + +circle = Circle() + +print(circle.radius) +print(circle.diameter) +print(circle.area) + +circle.radius = 10 +print(circle.radius) +print(circle.diameter) +print(circle.area) + +circle.diameter = 10 +print(circle.radius) +print(circle.diameter) +print(circle.area) diff --git a/scripts/intermediate_oop/properties.py b/scripts/intermediate_oop/properties.py index 5a59432..260a4cf 100755 --- a/scripts/intermediate_oop/properties.py +++ b/scripts/intermediate_oop/properties.py @@ -1,10 +1,28 @@ # -*- coding: utf-8 -*- """Property exercises.""" +import math class Circle: """Circle with radius, area, etc.""" + def __init__(self, radius=1): + self.radius = radius + + @property + def area(self): + area = math.pi * self.radius**2 + return area + + @property + def diameter(self): + diameter = self.radius * 2 + return diameter + + @diameter.setter + def diameter(self, diameter): + self.radius = diameter / 2 + class Vector: """Class representing a 3 dimensional vector.""" From de1ceaaaba6d124902fbe6a41634829d41b7801a Mon Sep 17 00:00:00 2001 From: Alvaro Ortiz Date: Fri, 2 Feb 2024 07:13:58 +0000 Subject: [PATCH 11/20] dunder exercises --- scripts/intermediate_oop/dunder.py | 16 ++++++++++++++-- scripts/intermediate_oop/main_dunder.py | 12 ++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 scripts/intermediate_oop/main_dunder.py diff --git a/scripts/intermediate_oop/dunder.py b/scripts/intermediate_oop/dunder.py index 5085292..3aba869 100755 --- a/scripts/intermediate_oop/dunder.py +++ b/scripts/intermediate_oop/dunder.py @@ -1,10 +1,22 @@ # -*- coding: utf-8 -*- """Dunder exercises.""" - class ReverseView: """Lazily operate on a sequence in reverse.""" - + def __init__(self, sequence): + self._sequence = sequence + + def __getitem__(self, index): # If it were a dictionary we would use 'key' instead + return self._sequence[-index-1] + + def __str__(self): + return f'{self._sequence[::-1]}' + + def __len__(self): + return len(self._sequence) + +class NotImplemented(BaseException): + pass class Comparator: """Object that is equal to a very small range of numbers.""" diff --git a/scripts/intermediate_oop/main_dunder.py b/scripts/intermediate_oop/main_dunder.py new file mode 100644 index 0000000..9682f9f --- /dev/null +++ b/scripts/intermediate_oop/main_dunder.py @@ -0,0 +1,12 @@ +from dunder import ReverseView + +numbers = [2, 1, 3, 4, 7, 11] +reverse_numbers = ReverseView(numbers) + +print(list(reverse_numbers)) +# [11, 7, 4, 3, 1, 2] + +print(str(reverse_numbers)) +# '[11, 7, 4, 3, 1, 2]' + +print(len(reverse_numbers)) \ No newline at end of file From 27521e76a58c0745452fb3792e4b7b61b87dc6d9 Mon Sep 17 00:00:00 2001 From: Alvaro Ortiz Date: Fri, 2 Feb 2024 07:16:10 +0000 Subject: [PATCH 12/20] dunder exercises --- scripts/intermediate_oop/main_dataclass.py | 35 ++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 scripts/intermediate_oop/main_dataclass.py diff --git a/scripts/intermediate_oop/main_dataclass.py b/scripts/intermediate_oop/main_dataclass.py new file mode 100644 index 0000000..d2c5797 --- /dev/null +++ b/scripts/intermediate_oop/main_dataclass.py @@ -0,0 +1,35 @@ +from dataclasses import dataclass, field +from decimal import Decimal +import math + +# ``dataclass`` provides many functionalities automatically +@dataclass +class Point: + x: float + y: float + + @property + def magnitude(self): + return math.sqrt(self.x**2 + self.y**2) + +@dataclass(frozen=True) +class Item: + """ + frozen: inmutable + """ + name: str + price: Decimal = Decimal(0) + colors: str = field(default_factory=list) + + @property + def amount(self): + return f"€{self.price:,.2f}" + +if __name__ == "__main__": + point = Point(x=1, y=1) + print(point) + print(point.magnitude) + + duck = Item(name='duck', price=5, colors=['purple', 'red']) + + \ No newline at end of file From 9f9797b8b5ed59ee6111eee63c619b7a571d7699 Mon Sep 17 00:00:00 2001 From: Alvaro Ortiz Date: Fri, 2 Feb 2024 09:21:36 +0000 Subject: [PATCH 13/20] Update --- scripts/intermediate_oop/classes.py | 2 +- .../intermediate_oop/main_abstract_classes.py | 31 +++++++++++++++++ .../intermediate_oop/main_class_methods.py | 34 +++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 scripts/intermediate_oop/main_abstract_classes.py create mode 100644 scripts/intermediate_oop/main_class_methods.py diff --git a/scripts/intermediate_oop/classes.py b/scripts/intermediate_oop/classes.py index 0ebbaa8..eddb3c9 100755 --- a/scripts/intermediate_oop/classes.py +++ b/scripts/intermediate_oop/classes.py @@ -31,7 +31,7 @@ def __repr__(self): return f"BankAccount(balance={self._balance})" def __str__(self): - return f"A Bank Account with balance={self._balance})" + return f"A Bank Account with balance={self._balance}" class SuperMap: diff --git a/scripts/intermediate_oop/main_abstract_classes.py b/scripts/intermediate_oop/main_abstract_classes.py new file mode 100644 index 0000000..3df2b92 --- /dev/null +++ b/scripts/intermediate_oop/main_abstract_classes.py @@ -0,0 +1,31 @@ +from classes import BankAccount +import abc + +class MinimumBankAccount(BankAccount): + + def withdraw(self, amount): + if amount > self.balance: + raise ValueError(f"Can't withdraw {amount} (balance is €{self._balance})") + super().withdraw(amount) + +class Duck(abc.ABC): + @abc.abstractmethod + def quack(self): + pass + +class Goose(Duck): + def quack(self): + print("Quack") + + +if __name__ == '__main__': + account = MinimumBankAccount(10) + try: + account.withdraw(100) + except: + account.withdraw(5) + print(account.balance) + print(account) + + goose = Goose() + goose.quack() \ No newline at end of file diff --git a/scripts/intermediate_oop/main_class_methods.py b/scripts/intermediate_oop/main_class_methods.py new file mode 100644 index 0000000..dc16ac3 --- /dev/null +++ b/scripts/intermediate_oop/main_class_methods.py @@ -0,0 +1,34 @@ + +import math + +class Circle: + """Circle with radius, area, etc.""" + + def __init__(self, radius=1): + self.radius = radius + + @property + def area(self): + area = math.pi * self.radius**2 + return area + + @property + def diameter(self): + diameter = self.radius * 2 + return diameter + + @diameter.setter + def diameter(self, diameter): + self.radius = diameter / 2 + + @classmethod # Typically used Alternative creation class methods, different from __init__ + def from_area(cls, area): + radius = math.sqrt(area / math.pi) + return cls(radius=radius) + + +if __name__ == '__main__': + circle = Circle.from_area(area=100) + print(circle.radius) + print(circle.diameter) + print(circle.area) \ No newline at end of file From 97990b5f8397af2396809e68fb070de4a35882d9 Mon Sep 17 00:00:00 2001 From: Alvaro Ortiz Date: Wed, 7 Feb 2024 08:30:42 +0000 Subject: [PATCH 14/20] Add oreilly decorator course --- .../exercises/decorators.py | 21 ++ .../exercises/decorators_test.py | 247 ++++++++++++++++++ .../oreilly_decorators/exercises/functions.py | 34 +++ .../exercises/functions_test.py | 130 +++++++++ .../oreilly_decorators/exercises/helpers.py | 8 + .../oreilly_decorators/exercises/initial.py | 12 + .../exercises/initial_test.py | 17 ++ .../exercises/main_functions.py | 48 ++++ scripts/oreilly_decorators/exercises/more.py | 17 ++ .../oreilly_decorators/exercises/more_test.py | 214 +++++++++++++++ scripts/oreilly_decorators/exercises/test.py | 209 +++++++++++++++ .../oreilly_decorators/exercises/test_data.py | 45 ++++ scripts/oreilly_decorators/main.py | 29 ++ 13 files changed, 1031 insertions(+) create mode 100644 scripts/oreilly_decorators/exercises/decorators.py create mode 100644 scripts/oreilly_decorators/exercises/decorators_test.py create mode 100644 scripts/oreilly_decorators/exercises/functions.py create mode 100644 scripts/oreilly_decorators/exercises/functions_test.py create mode 100644 scripts/oreilly_decorators/exercises/helpers.py create mode 100644 scripts/oreilly_decorators/exercises/initial.py create mode 100644 scripts/oreilly_decorators/exercises/initial_test.py create mode 100644 scripts/oreilly_decorators/exercises/main_functions.py create mode 100644 scripts/oreilly_decorators/exercises/more.py create mode 100644 scripts/oreilly_decorators/exercises/more_test.py create mode 100644 scripts/oreilly_decorators/exercises/test.py create mode 100644 scripts/oreilly_decorators/exercises/test_data.py create mode 100644 scripts/oreilly_decorators/main.py diff --git a/scripts/oreilly_decorators/exercises/decorators.py b/scripts/oreilly_decorators/exercises/decorators.py new file mode 100644 index 0000000..6ce8033 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/decorators.py @@ -0,0 +1,21 @@ +"""Decorator exercises""" + + +def count_calls(): + """Record calls to the given function.""" + + +def jsonify(): + """Decorate function to JSON-encode return value.""" + + +def groot(): + """Return function which prints 'Groot' (ignore decoratee).""" + + +def four(): + """Return 4 (ignore decorated function).""" + + +def record_calls(): + """Recording number of times a decorated function is called.""" diff --git a/scripts/oreilly_decorators/exercises/decorators_test.py b/scripts/oreilly_decorators/exercises/decorators_test.py new file mode 100644 index 0000000..f8547a5 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/decorators_test.py @@ -0,0 +1,247 @@ +"""Tests for decorator exercises""" +from contextlib import redirect_stdout +from io import StringIO +import unittest + +from decorators import ( + count_calls, + jsonify, + groot, + four, + record_calls, +) + + +class CountCallsTests(unittest.TestCase): + + """Test for count_calls.""" + + def test_accepts_a_function(self): + # Function value is returned + def one(): return 1 + decorated = count_calls(one) + self.assertEqual(decorated(), 1) + self.assertEqual(decorated.calls, 1) + + def test_calls_a_function(self): + # Function is called each time + recordings = [] + def my_func(): + recordings.append('call') + return recordings + decorated = count_calls(my_func) + self.assertEqual(recordings, []) + self.assertEqual(decorated.calls, 0) + self.assertEqual(decorated(), ['call']) + self.assertEqual(decorated.calls, 1) + self.assertEqual(decorated(), ['call', 'call']) + self.assertEqual(decorated.calls, 2) + + def test_accepts_arguments(self): + # Function accepts positional arguments + @count_calls + def add(x, y): + return x + y + self.assertEqual(add(1, 2), 3) + self.assertEqual(add(1, 3), 4) + + # Function accepts keyword arguments + recordings = [] + @count_calls + def my_func(*args, **kwargs): + recordings.append((args, kwargs)) + return recordings + self.assertEqual(my_func(), [((), {})]) + self.assertEqual(my_func(1, 2, a=3), [((), {}), ((1, 2), {'a': 3})]) + + # Exceptions are still counted as calls + @count_calls + def my_func(): + raise AssertionError("Function called too soon") + self.assertEqual(my_func.calls, 0) + with self.assertRaises(AssertionError): + my_func() + self.assertEqual(my_func.calls, 1) + self.assertEqual(my_func.calls, 1) + with self.assertRaises(AssertionError): + my_func() + self.assertEqual(my_func.calls, 2) + + +class JSONifyTests(unittest.TestCase): + + """Tests for jsonify.""" + + def test_serialize_none(self): + func = jsonify(lambda: None) + self.assertEqual(func(), 'null') + + def test_serialize_list(self): + def make_list(): return [4, 'hi', True, 5.5] + make_list = jsonify(make_list) + self.assertEqual(make_list(), '[4, "hi", true, 5.5]') + + def test_returned(self): + def return_hi(): return 'hi' + return_hi = jsonify(return_hi) + self.assertEqual(return_hi(), '"hi"') + + def test_takes_arguments(self): + def add(x, y): return x + y + add = jsonify(add) + self.assertEqual(add(1, 2), '3') + self.assertEqual(add(x=1, y=2), '3') + + +class GrootTests(unittest.TestCase): + + """Tests for groot.""" + + def test_print_groot(self): + def greet(name): print("Hello {}".format(name)) + greet = groot(greet) + with redirect_stdout(StringIO()) as stdout: + greet("Trey") + self.assertEqual(stdout.getvalue(), "Groot\n") + + def test_nothing_returned(self): + def return_hi(): return 'hi' + return_hi = groot(return_hi) + with redirect_stdout(StringIO()) as stdout: + self.assertEqual(return_hi(), None) + + def test_function_ignored(self): + def return_hi(): dictionary['key'] = 'value' + dictionary = {} + return_hi = groot(return_hi) + with redirect_stdout(StringIO()) as stdout: + self.assertEqual(return_hi(), None) + self.assertEqual(dictionary, {}) + + def test_takes_arguments(self): + def add(x, y): return x + y + add = groot(add) + with redirect_stdout(StringIO()) as stdout: + add(1, 2) + add(x=1, y=2) + + +class FourTests(unittest.TestCase): + + """Tests for four.""" + + def test_return_four(self): + def greet(name): print("Hello {}".format(name)) + with redirect_stdout(StringIO()) as stdout: + greet = four(greet) + self.assertEqual(stdout.getvalue(), '') + self.assertEqual(greet, 4) + + +class RecordCallsTests(unittest.TestCase): + + """Tests for record_calls.""" + + def test_call_count_starts_at_zero(self): + decorated = record_calls(lambda: None) + self.assertEqual(decorated.call_count, 0) + + def test_not_called_on_decoration_time(self): + def my_func(): + raise AssertionError("Function called too soon") + record_calls(my_func) + + def test_function_still_callable(self): + recordings = [] + def my_func(): + recordings.append('call') + decorated = record_calls(my_func) + self.assertEqual(recordings, []) + decorated() + self.assertEqual(recordings, ['call']) + decorated() + self.assertEqual(recordings, ['call', 'call']) + + def test_return_value(self): + def one(): return 1 + one = record_calls(one) + self.assertEqual(one(), 1) + + def test_takes_arguments(self): + def add(x, y): return x + y + add = record_calls(add) + self.assertEqual(add(1, 2), 3) + self.assertEqual(add(1, 3), 4) + + def test_takes_keyword_arguments(self): + recordings = [] + @record_calls + def my_func(*args, **kwargs): + recordings.append((args, kwargs)) + return recordings + self.assertEqual(my_func(), [((), {})]) + self.assertEqual(my_func(1, 2, a=3), [((), {}), ((1, 2), {'a': 3})]) + + def test_call_count_increments(self): + decorated = record_calls(lambda: None) + self.assertEqual(decorated.call_count, 0) + decorated() + self.assertEqual(decorated.call_count, 1) + decorated() + self.assertEqual(decorated.call_count, 2) + + def test_different_functions(self): + my_func1 = record_calls(lambda: None) + my_func2 = record_calls(lambda: None) + my_func1() + self.assertEqual(my_func1.call_count, 1) + self.assertEqual(my_func2.call_count, 0) + my_func2() + self.assertEqual(my_func1.call_count, 1) + self.assertEqual(my_func2.call_count, 1) + + def test_docstring_and_name_preserved(self): + import pydoc + decorated = record_calls(example) + self.assertIn('function example', str(decorated)) + documentation = pydoc.render_doc(decorated) + self.assertIn('function example', documentation) + self.assertIn('Example function.', documentation) + self.assertIn('(a, b=True)', documentation) + + def test_record_arguments(self): + @record_calls + def my_func(*args, **kwargs): return args, kwargs + self.assertEqual(my_func.calls, []) + my_func() + self.assertEqual(len(my_func.calls), 1) + self.assertEqual(my_func.calls[0].args, ()) + self.assertEqual(my_func.calls[0].kwargs, {}) + my_func(1, 2, a=3) + self.assertEqual(len(my_func.calls), 2) + self.assertEqual(my_func.calls[1].args, (1, 2)) + self.assertEqual(my_func.calls[1].kwargs, {'a': 3}) + + def test_record_return_values(self): + from decorators import NO_RETURN + @record_calls + def my_func(*args, **kwargs): return sum(args), kwargs + my_func() + self.assertEqual(my_func.calls[0].return_value, (0, {})) + my_func(1, 2, a=3) + self.assertEqual(my_func.calls[1].return_value, (3, {'a': 3})) + self.assertIs(my_func.calls[1].exception, None) + with self.assertRaises(TypeError) as context: + my_func(1, 'hi', a=3) + self.assertIs(my_func.calls[2].return_value, NO_RETURN) + self.assertEqual(my_func.calls[2].exception, context.exception) + + +def example(a, b=True): + """Example function.""" + print('hello world') + + +if __name__ == "__main__": + from helpers import error_message + error_message() diff --git a/scripts/oreilly_decorators/exercises/functions.py b/scripts/oreilly_decorators/exercises/functions.py new file mode 100644 index 0000000..73ad1e1 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/functions.py @@ -0,0 +1,34 @@ +"""Function object exercises""" + + +def call(func, *args, **kwargs): + """Call the function provided with the given arguments.""" + return func(*args, **kwargs) + + +def call_later(func, *args, **kwargs): + """Return a function to call given function with provided arguments.""" + def new_func(): + return func(*args, **kwargs) + return new_func + +def exclude(func, iterable): + """Only keep items which fail a given predicate test""" + return [x for x in iterable if not func(x)] + +# def call_logger(func): +# """Return a new function that calls func and prints when it was called.""" +# def new_func(): +# print("Function started") +# x = func() +# print("Function returned") +# return x +# return new_func + + +def call_again(): + """Return function return value and a function to call again.""" + + +def only_once(): + """Return new version of the function that can only be called once.""" diff --git a/scripts/oreilly_decorators/exercises/functions_test.py b/scripts/oreilly_decorators/exercises/functions_test.py new file mode 100644 index 0000000..132ae31 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/functions_test.py @@ -0,0 +1,130 @@ +"""Tests for function object exercises""" +from contextlib import redirect_stdout +from io import StringIO +from textwrap import dedent +import unittest + +from functions import ( + call, + call_later, + exclude, + call_logger, + call_again, + only_once, +) + + +class CallTests(unittest.TestCase): + + """Tests for call.""" + + def test_int_call(self): + self.assertEqual(call(int), 0) + + def test_five_call(self): + self.assertEqual(call(int, "5"), 5) + + def test_hello_call(self): + self.assertEqual(call(len, "hello"), 5) + + def test_zip_call(self): + self.assertEqual(list(call(zip, [1, 2], [3, 4])), [(1, 3), (2, 4)]) + + +class CallLaterTests(unittest.TestCase): + + """Tests for call_later.""" + + def test_append_to_list(self): + names = [] + append_name = call_later(names.append, "Trey") + self.assertIsNone(append_name()) + self.assertEqual(names, ['Trey']) + append_name() + self.assertEqual(names, ['Trey', 'Trey']) + + def test_zip_later(self): + call_zip = call_later(zip, [1, 2], [3, 4]) + self.assertEqual(list(call_zip()), [(1, 3), (2, 4)]) + + +class ExcludeTests(unittest.TestCase): + + """Tests for exclude.""" + + def test_bool_exclude(self): + self.assertEqual( + exclude(bool, [False, True, False]), + [False, False], + ) + + def test_lambda_exclude(self): + self.assertEqual( + exclude(lambda x: len(x) > 3, ["red", "blue", "green"]), + ['red'], + ) + + +class CallLoggerTests(unittest.TestCase): + + """Tests for call_logger.""" + + def test_prints_before_and_after(self): + def greet(): print("Hello") + greet = call_logger(greet) + with redirect_stdout(StringIO()) as stdout: + greet() + self.assertEqual(stdout.getvalue(), dedent(""" + Function started + Hello + Function returned + """).lstrip()) + + def test_returned(self): + def return_hi(): return 'hi' + return_hi = call_logger(return_hi) + with redirect_stdout(StringIO()) as stdout: + self.assertEqual(return_hi(), 'hi') + self.assertEqual(stdout.getvalue(), dedent(""" + Function started + Function returned + """).lstrip()) + + def test_takes_arguments(self): + def add(x, y): return x + y + add = call_logger(add) + with redirect_stdout(StringIO()) as stdout: + self.assertEqual(add(1, 2), 3) + self.assertEqual(add(x=1, y=2), 3) + + +class CallAgainTests(unittest.TestCase): + + """Tests for call_again.""" + + def test_str_on_list(self): + names = [] + response, names_as_str = call_again(str, names) + self.assertEqual(response, '[]') + names.append("Diane") + self.assertEqual(names_as_str(), "['Diane']") + + +class OnlyOnceTests(unittest.TestCase): + + """Tests for only_once.""" + + def test_do_once(self): + def do_something(x, y): + return x * 2 + y ** 2 + + do_something_once = only_once(do_something) + do_something_once(1, 2) + with self.assertRaises(ValueError): + do_something_once(1, 2) + + +if __name__ == "__main__": + from helpers import error_message + error_message() + diff --git a/scripts/oreilly_decorators/exercises/helpers.py b/scripts/oreilly_decorators/exercises/helpers.py new file mode 100644 index 0000000..186a2a8 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/helpers.py @@ -0,0 +1,8 @@ +"""Test helpers""" +import sys + + +def error_message(): + print("Cannot run {} from the command-line.".format(sys.argv[0])) + print() + print("Run python test.py instead") diff --git a/scripts/oreilly_decorators/exercises/initial.py b/scripts/oreilly_decorators/exercises/initial.py new file mode 100644 index 0000000..992d23c --- /dev/null +++ b/scripts/oreilly_decorators/exercises/initial.py @@ -0,0 +1,12 @@ +"""Function for Initial Framework Test Check.""" + + +def is_ok(): + """Confirm Test Framework.""" + return ( + 'Congrats and welcome to the Test Framework! \n' + 'The message confirms the Test Framework is working! Yay! \n' + 'Pat yourself on the back for successful installation! \n' + 'Continue reading this section of the course instructions. \n' + 'We will get started in a moment.' + ) diff --git a/scripts/oreilly_decorators/exercises/initial_test.py b/scripts/oreilly_decorators/exercises/initial_test.py new file mode 100644 index 0000000..6e2caf0 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/initial_test.py @@ -0,0 +1,17 @@ +"""Test to confirm that the test framework is working.""" +import unittest +from initial import is_ok + + +class InitialTests(unittest.TestCase): + """Tests for is_ok.""" + + def test_confirm_test(self): + """Test passed.""" + confirm = is_ok() + print(confirm) + + +if __name__ == '__main__': + from helpers import error_message + error_message() diff --git a/scripts/oreilly_decorators/exercises/main_functions.py b/scripts/oreilly_decorators/exercises/main_functions.py new file mode 100644 index 0000000..8aa2637 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/main_functions.py @@ -0,0 +1,48 @@ +from functions import call +from functions import call_later +from functions import exclude +from functions import call_logger + +print(call(int)) +# 0 + +print(call(int, "5")) +# 5 + +print(call(len, "hello")) +# 5 + +print(list(call(zip, [1, 2], [3, 4]))) +# [(1, 3), (2, 4)] + +names = [] +append_name = call_later(names.append, "Trey") +append_name() +print(names) +# ['Trey'] + +append_name() +print(names) +# ['Trey', 'Trey'] + +call_zip = call_later(zip, [1, 2], [3, 4]) + +print(list(call_zip())) +# [(1, 3), (2, 4)] + + +print(exclude(bool, [False, True, False])) +# [False, False] + +print(exclude(lambda x: len(x) > 3, ["red", "blue", "green"])) +# ['red'] + + +def greet(): print("Hello") + +greet_now = call_logger(greet) + +print(greet_now()) +# Function started +# Hello +# Function returned \ No newline at end of file diff --git a/scripts/oreilly_decorators/exercises/more.py b/scripts/oreilly_decorators/exercises/more.py new file mode 100644 index 0000000..401c693 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/more.py @@ -0,0 +1,17 @@ +"""More advanced decorator exercises""" + + +def coalesce_all(): + """Decorator to coalesces arguments to default when sentinel is given.""" + + +def lazy_repr(): + """Class decorator that adds a __repr__ method automatically.""" + + +def positional_only(): + """Specify arguments to a function positionally only.""" + + +def at(): + """Chain all given decorators together.""" diff --git a/scripts/oreilly_decorators/exercises/more_test.py b/scripts/oreilly_decorators/exercises/more_test.py new file mode 100644 index 0000000..80ffd2b --- /dev/null +++ b/scripts/oreilly_decorators/exercises/more_test.py @@ -0,0 +1,214 @@ +"""Tests for more advanced decorator exercises""" +import math +import unittest + +from more import ( + coalesce_all, + lazy_repr, + positional_only, + at, +) + + +class CoalesceAllTests(unittest.TestCase): + + """Tests for coalesce_all.""" + + def test_coalesce_one_argument(self): + @coalesce_all("world") + def greet(greet="world"): + return "Hello {}".format(greet) + self.assertEqual(greet("Trey"), "Hello Trey") + self.assertEqual(greet("someone"), "Hello someone") + self.assertEqual(greet(""), "Hello ") + self.assertEqual(greet(None), "Hello world") + self.assertEqual(greet(), "Hello world") + + def test_coalesce_one_argument_from_empty_string(self): + @coalesce_all("world", sentinel="") + def greet(greet="world"): + return "Hello {}".format(greet) + self.assertEqual(greet("Trey"), "Hello Trey") + self.assertEqual(greet("someone"), "Hello someone") + self.assertEqual(greet(""), "Hello world") + self.assertEqual(greet(None), "Hello None") + self.assertEqual(greet(), "Hello world") + + def test_coalesce_multiple_arguments(self): + @coalesce_all(1) + def multiply(x, y): + return x * y + self.assertEqual(multiply(2, 3), 6) + self.assertEqual(multiply(None, None), 1) + self.assertEqual(multiply(2, None), 2) + self.assertEqual(multiply(None, 3), 3) + with self.assertRaises(TypeError): + multiply(4) + with self.assertRaises(TypeError): + multiply() + + def test_coalesce_keyword_arguments(self): + @coalesce_all(1) + def multiply(x, y): + return x * y + self.assertEqual(multiply(x=2, y=3), 6) + self.assertEqual(multiply(2, y=3), 6) + self.assertEqual(multiply(x=None, y=None), 1) + self.assertEqual(multiply(x=2, y=None), 2) + self.assertEqual(multiply(2, y=None), 2) + self.assertEqual(multiply(x=None, y=3), 3) + self.assertEqual(multiply(None, y=3), 3) + with self.assertRaises(TypeError): + multiply(x=4) + with self.assertRaises(TypeError): + multiply(y=4) + + +class LazyReprTests(unittest.TestCase): + + """Tests for lazy_repr.""" + + def test_with_concrete_attributes(self): + @lazy_repr + class Point: + def __init__(self, x, y, z): + self.x, self.y, self.z = x, y, z + self.assertEqual(str(Point(1, 2, 3)), "Point(x=1, y=2, z=3)") + self.assertEqual(repr(Point(x=3, y=4, z=5)), "Point(x=3, y=4, z=5)") + + def test_argument_without_an_attribute(self): + @lazy_repr + class BankAccount: + def __init__(self, opening_balance): + self.balance = opening_balance + + self.assertEqual(str(BankAccount(10)), "BankAccount(balance=10)") + self.assertEqual(repr(BankAccount(10)), "BankAccount(balance=10)") + + +class PositionalOnlyTests(unittest.TestCase): + + """Tests for positional_only.""" + + def test_restrict_all_arguments_to_positional(self): + @positional_only + def divide(x, y): return x / y + self.assertEqual(divide(21, 3), 7) + self.assertEqual(divide(5, 2), 2.5) + with self.assertRaises(TypeError): + divide(x=5, y=2) + with self.assertRaises(TypeError): + divide(5, y=2) + with self.assertRaises(TypeError): + divide(5, x=2) + with self.assertRaises(TypeError): + divide(1, 2, 3) + with self.assertRaises(TypeError): + divide(1) + + def test_no_keyword_arguments_allowed(self): + @positional_only + def my_func(a, b=2, **kwargs): return a + self.assertEqual(my_func(3), 3) + with self.assertRaises(TypeError): + my_func(3, b=3) + with self.assertRaises(TypeError): + my_func(3, a=3) + + def test_any_number_of_positional_arguments(self): + @positional_only + def add(a, b, *args): return a + b + sum(args) + self.assertEqual(add(1, 2), 3) + self.assertEqual(add(1, 2, 3, 4), 10) + self.assertEqual(add(1, 2, 3), 6) + @positional_only + def product(*numbers): + total = 1 + for n in numbers: + total *= n + return total + self.assertEqual(product(1, 2, 3, 4), 24) + self.assertEqual(product(1, 2), 2) + self.assertEqual(product(10), 10) + self.assertEqual(product(), 1) + with self.assertRaises(TypeError): + add(1, 2, b=3) + with self.assertRaises(TypeError): + product(numbers=3) + + def test_with_positional_argument_count(self): + @positional_only(3) + def add(a, b, c, d): return a + b + c + d + self.assertEqual(add(1, 2, 3, 4), 10) + self.assertEqual(add(1, 2, 3, d=4), 10) + with self.assertRaises(TypeError): + add(1, 2, c=3, d=4) + with self.assertRaises(TypeError): + add(1, b=2, c=3, d=4) + with self.assertRaises(TypeError): + add(a=1, b=2, c=3, d=4) + @positional_only(2) + def divide(x=1, y=1): return x / y + self.assertEqual(divide(3, 2), 1.5) + with self.assertRaises(TypeError): + divide(x=3, y=2) + with self.assertRaises(TypeError): + divide(3, y=2) + + +class AtTests(unittest.TestCase): + + """Tests for at.""" + + def test_one_decorator(self): + @at(reprify) + def add(x, y): + return x + y + self.assertEqual(add(1, 2), '3') + self.assertEqual(add(1, 3), '4') + self.assertEqual(add(x=2, y=3), '5') + + def test_two_decorators(self): + @at(sqrtd, reprify) + def add(x, y): + return x + y + self.assertEqual(add(1, 3), '2.0') + self.assertEqual(add(x=20, y=5), '5.0') + + @at(sqrtd, of_squares) + def add(x, y): + return x + y + self.assertEqual(add(3, 4), 5) + self.assertEqual(add(8, 15), 17) + + def test_three_decorators(self): + @at(sqrtd, of_squares, reprify) + def add(x, y): + return x + y + self.assertEqual(add(3, 4), '5.0') + self.assertEqual(add(8, 15), '17.0') + + +def reprify(func): + def wrapper(*args, **kwargs): + return repr(func(*args, **kwargs)) + return wrapper + + +def sqrtd(func): + def wrapper(*args, **kwargs): + return math.sqrt(func(*args, **kwargs)) + return wrapper + + +def of_squares(func): + def wrapper(*args, **kwargs): + args = (n**2 for n in args) + kwargs = {k: v**2 for k, v in kwargs.items()} + return func(*args, **kwargs) + return wrapper + + +if __name__ == "__main__": + from helpers import error_message + error_message() diff --git a/scripts/oreilly_decorators/exercises/test.py b/scripts/oreilly_decorators/exercises/test.py new file mode 100644 index 0000000..c1ca3a6 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/test.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +from __future__ import print_function +import ast +from functools import partial +from inspect import getsource +import os +import re +import sys +from textwrap import dedent +import unittest + +from test_data import MODULES, TESTS + + +class Color: + BOLD = "\033[1m" + UNDER = "\033[4m" + END = "\033[0m" + RED = "\033[91m" + + +def _assert_op_sub(op, match): + node = ast.parse(match[0]) + expression1, expression2, *_ = node.body[0].value.args + if hasattr(ast, "unparse"): + expression1 = ast.unparse(expression1) + expression2 = ast.unparse(expression2) + else: + expression1 = ast.get_source_segment(match[0], expression1) + expression2 = ast.get_source_segment(match[0], expression2) + return f"assert {expression1} {op} {expression2}" + + +def reformat_source(method_source): + # Remove first line and dedent + source = dedent("".join(method_source.splitlines(keepends=True)[1:])) + + # Convert common assertions + source = re.sub( + r"self.assertEqual\((.*), (.*)\)", + partial(_assert_op_sub, "=="), + source + ) + source = re.sub( + r"self.assertIs\((.*), (.*)\)", + partial(_assert_op_sub, "is"), + source + ) + source = re.sub( + r"self.assertIn\((.*), (.*)\)", + partial(_assert_op_sub, "in"), + source + ) + + source = re.sub( + r"self.assertIsNone\((.*)\)", + r"assert \1 is None", + source + ) + source = re.sub( + r"self.assert(True|False)\((.*)\)", + r"assert \2 == \1", + source + ) + + # characters + source = re.sub( + r"self.assertEqual\(\n *(.*),\n *(.*?),?\n[ ]*\)\n", + r"assert \1 == \2\n", + source + ) + + # tags_equal + source = re.sub( + r"self.assert(True|False)\((.*\()\n(.*)\n(.*)\n([ ]*\))\)\n", + r"assert \2\n\3\n\4\n\5 == \1\n", + source + ) + + # count.py + source = re.sub( + r"self.assertEqual\((.*), (\[\n[^]]+\])\)\n", + r"assert \1 == \2\n", + source + ) + return source + + +def reformat_error(traceback_message): + *lines, last_line = str(traceback_message).splitlines(keepends=True) + extra = "" + if last_line.startswith("AssertionError: None !="): + extra += "Maybe your function didn't return anything?" + extra += "\nMore on return values at: https://trey.io/ret" + if "takes 0 positional arguments but" in last_line: + extra += "Your function doesn't accept any arguments yet." + extra += "\nMore on arguments at: https://trey.io/args" + if extra: + extra = f"\n{Color.BOLD}HINT:{Color.END} {extra}\n" + return "".join([*lines, Color.RED + last_line + Color.END, extra]) + + +class VerboseTestResult(unittest.TextTestResult): + def printErrorList(self, flavor, errors): + for test, err in errors: + self.stream.writeln(self.separator1) + description = self.getDescription(test) + self.stream.writeln(f"{flavor}: {description}") + self.stream.writeln(self.separator2) + if hasattr(test, "_testMethodName"): + full_source = getsource( + getattr(type(test), test._testMethodName) + ) + self.stream.writeln( + Color.BOLD + + reformat_source(full_source) + + Color.END + ) + self.stream.writeln(reformat_error(err)) + self.stream.flush() + + +class VerboseTestRunner(unittest.TextTestRunner): + resultclass = VerboseTestResult + + +def get_test(obj_name): + if obj_name not in TESTS: + raise SystemExit("Test for {} doesn't exist.".format(obj_name)) + return unittest.defaultTestLoader.loadTestsFromName(TESTS[obj_name]) + + +def run_tests(tests): + test_suite = unittest.TestSuite(tests) + return VerboseTestRunner(verbosity=2).run(test_suite).wasSuccessful() + + +def print_object_names(): + for module, objects in MODULES.items(): + print("\n{}:\n".format(module)) + for obj in objects: + print(obj) + print() + + +def main(*arguments): + os.system("") # Enables ANSI escape characters in terminal + if not arguments: + print("Please select a thing to test") + print_object_names() + elif len(arguments) > 1: + print(""" +Can only call test.py with one argument: the name of the exercise being tested + +Examples: + +- python test.py get_hypotenuse +- python test.py hello.py +- python test.py BankAccount + +This test script runs Trey's tests against your code. +The tests are written in files that end in "_test.py". + +If you'd like to test your code manually, you can either: + +1. Open a Python REPL, import your code, and execute it with specific arguments +2. Write your own test code at the bottom of your file (e.g. functions.py) and +run that file (e.g. "python functions.py"). + +Consult the website for instructions for running the exercises and ask Trey +for help when you get stuck. + """.strip()) + elif ' ' in arguments[0] or '(' in arguments[0] or ',' in arguments[0]: + print("Invalid characters found: {}\n".format(arguments[0])) + print("This test script doesn't accept code, just an exercise name.\n") + print("Example usage:") + print("python test.py \n") + else: + [argument] = arguments + if argument.startswith(('modules/', 'modules\\', './modules/')): + argument = argument.split('/', 1)[1] + if argument == '--all': + arguments = list(TESTS) + else: + arguments = [argument] + tests = [ + get_test(arg) + for arg in arguments + ] + print("Testing {}\n".format(', '.join(arguments))) + test_classes = set( + tuple(test.id().split('.')[:-1]) + for suite in tests + for test in suite._tests + ) + for module, cls in test_classes: + print("Running {} test class in {}.py\n".format(cls, module)) + success = run_tests(tests) + sys.exit(not success) + + +if __name__ == "__main__": + # Version check before all else + major, minor, micro, releaselevel, serial = sys.version_info + if (major, minor) < (3, 5): + print("You are running Python {0}.{1}".format(major, minor)) + print("Must use Python version 3.5 or above") + sys.exit(1) + main(*sys.argv[1:]) diff --git a/scripts/oreilly_decorators/exercises/test_data.py b/scripts/oreilly_decorators/exercises/test_data.py new file mode 100644 index 0000000..f5bdef0 --- /dev/null +++ b/scripts/oreilly_decorators/exercises/test_data.py @@ -0,0 +1,45 @@ +TESTS = { + "count_calls": "decorators_test.CountCallsTests", + "four": "decorators_test.FourTests", + "groot": "decorators_test.GrootTests", + "jsonify": "decorators_test.JSONifyTests", + "record_calls": "decorators_test.RecordCallsTests", + "call_again": "functions_test.CallAgainTests", + "call_later": "functions_test.CallLaterTests", + "call_logger": "functions_test.CallLoggerTests", + "call": "functions_test.CallTests", + "exclude": "functions_test.ExcludeTests", + "only_once": "functions_test.OnlyOnceTests", + "is_ok": "initial_test.InitialTests", + "at": "more_test.AtTests", + "coalesce_all": "more_test.CoalesceAllTests", + "lazy_repr": "more_test.LazyReprTests", + "positional_only": "more_test.PositionalOnlyTests" +} + +MODULES = { + "decorators": [ + "count_calls", + "jsonify", + "groot", + "four", + "record_calls" + ], + "functions": [ + "call", + "call_later", + "exclude", + "call_logger", + "call_again", + "only_once" + ], + "more": [ + "coalesce_all", + "lazy_repr", + "positional_only", + "at" + ], + "initial": [ + "is_ok" + ] +} diff --git a/scripts/oreilly_decorators/main.py b/scripts/oreilly_decorators/main.py new file mode 100644 index 0000000..3ca2e47 --- /dev/null +++ b/scripts/oreilly_decorators/main.py @@ -0,0 +1,29 @@ +from functools import partial + +def custom_map(func, iterable): # same as map built-in function + return (func(x) for x in iterable) + +def custom_filter(func, iterable): # same as built-in filter function + return [x for x in iterable if func(x)] + +def custom_partial(func, *first_args): # same as functools.partial + """"Let's us preload a function with some arguments""" + def new_func(*args): + new_args = first_args + args + return func(*new_args) + return new_func + +def add_two(x, y): + return x+y + +if __name__ == '__main__': + l = [-4, -5, 1, 3, 4, 5] + print(list(custom_filter(lambda x: x > 0, l))) + partial_sum_four = partial(add_two, 4) + print(partial_sum_four(5)) + + count_lengths = partial(map, len) + word_lengths = count_lengths(["some", "words", "banana"]) + print(list(word_lengths)) + + \ No newline at end of file From 72a85eaf161d062a6188f604a61a897267bb6b9e Mon Sep 17 00:00:00 2001 From: Alvaro Ortiz Date: Thu, 8 Feb 2024 11:04:30 +0000 Subject: [PATCH 15/20] First part of decorator exercises --- .../exercises/decorators.py | 51 +++++++++-- .../oreilly_decorators/exercises/functions.py | 32 ++++--- .../exercises/main_decorators.py | 86 +++++++++++++++++++ .../exercises/main_functions.py | 29 ++++++- 4 files changed, 178 insertions(+), 20 deletions(-) create mode 100644 scripts/oreilly_decorators/exercises/main_decorators.py diff --git a/scripts/oreilly_decorators/exercises/decorators.py b/scripts/oreilly_decorators/exercises/decorators.py index 6ce8033..a286512 100644 --- a/scripts/oreilly_decorators/exercises/decorators.py +++ b/scripts/oreilly_decorators/exercises/decorators.py @@ -1,21 +1,58 @@ """Decorator exercises""" +from functools import wraps +from dataclasses import dataclass +import json +NO_RETURN = object() -def count_calls(): +def count_calls(func): """Record calls to the given function.""" + def decorated_func(*args, **kwargs): + decorated_func.calls += 1 + return func(*args, **kwargs) + decorated_func.calls = 0 + return decorated_func - -def jsonify(): +def jsonify(func): """Decorate function to JSON-encode return value.""" + @wraps(func) + def dec_func(*args, **kwargs): + result = func(*args, **kwargs) + return json.dumps(result) + return dec_func -def groot(): +def groot(func): """Return function which prints 'Groot' (ignore decoratee).""" + @wraps(func) + def dec_func(*args, **kwargs): + print("Groot") + return dec_func - -def four(): +def four(func): """Return 4 (ignore decorated function).""" + return 4 +@dataclass +class Call: + args: tuple + kwargs: dict -def record_calls(): +def record_calls(func): """Recording number of times a decorated function is called.""" + @wraps(func) + def dec_func(*args, **kwargs): + dec_func.call_count += 1 + call = Call(args=args, kwargs=kwargs) + dec_func.calls.append(call) + try: + call.exception = None + call.return_value = func(*args, **kwargs) + except Exception as e: + call.exception = e + call.return_value = NO_RETURN + raise + return call.return_value + dec_func.call_count = 0 + dec_func.calls = [] + return dec_func diff --git a/scripts/oreilly_decorators/exercises/functions.py b/scripts/oreilly_decorators/exercises/functions.py index 73ad1e1..264fe30 100644 --- a/scripts/oreilly_decorators/exercises/functions.py +++ b/scripts/oreilly_decorators/exercises/functions.py @@ -16,19 +16,29 @@ def exclude(func, iterable): """Only keep items which fail a given predicate test""" return [x for x in iterable if not func(x)] -# def call_logger(func): -# """Return a new function that calls func and prints when it was called.""" -# def new_func(): -# print("Function started") -# x = func() -# print("Function returned") -# return x -# return new_func - +def call_logger(func): + """Return a new function that calls func and prints when it was called.""" + def new_func(*args, **kwargs): + print("Function started") + result = func(*args, **kwargs) + print("Function returned") + return result + return new_func -def call_again(): +def call_again(func, *args): """Return function return value and a function to call again.""" + def partial_func(): + return func(*args) + return (func(*args), partial_func) -def only_once(): +def only_once(func): """Return new version of the function that can only be called once.""" + func.call_count = 0 + def new_func(*args, **kwargs): + func.call_count += 1 + if func.call_count <= 1: + return func(*args, **kwargs) + else: + raise ValueError("You can't call this function twice!") + return new_func diff --git a/scripts/oreilly_decorators/exercises/main_decorators.py b/scripts/oreilly_decorators/exercises/main_decorators.py new file mode 100644 index 0000000..e4ff92a --- /dev/null +++ b/scripts/oreilly_decorators/exercises/main_decorators.py @@ -0,0 +1,86 @@ +from math import sqrt +from decorators import count_calls +from decorators import jsonify +from decorators import groot +from decorators import four +from decorators import record_calls +import json + +def call_logger(func): + """Return a new function that calls func and prints when it was called.""" + def new_func(*args, **kwargs): + print("Function started") + result = func(*args, **kwargs) + print(result) + print("Function returned") + return result + return new_func + +def alvaro(func): + """leave your mark in the function""" + func.alvaro_was_here = "Definitely True" + func.__doc__ = """This is the new docstring. Made definitely by Alvaro""" + return func + +if __name__ == '__main__': + + # @call_logger + # def add(x,y): + # return x+y + + # add(4,5) + + # @alvaro + # def add(x, y): + # return x+y + + # print(add.__doc__) + # print(add.alvaro_was_here) + + @count_calls + def quadratic(a, b, c): + x1 = -b / (2*a) + x2 = sqrt(b**2 - 4*a*c) / (2*a) + return (x1 + x2), (x1 - x2) + print(quadratic.calls) + print(quadratic(2, 8, 6)) + # (-1.0, -3.0) + print(quadratic(a=4, b=9, c=2)) + # (-0.25, -2.0) + print(quadratic.calls) + # 2 + + + @jsonify + def get_thing(): + return {'trey': "red", 'diane': "purple"} + + print(get_thing()) + # '{"trey": "red", "diane": "purple"} + + @groot + def greet(name): + print("Hello {}".format(name)) + + a = greet("Trey") + print(a) + + @record_calls + def greet(name="world"): + """Greet someone by their name.""" + print(f"Hello {name}") + + greet("Trey") + # Hello Trey + + print(greet.call_count) + # 1 + + print(greet(name="Trey")) + # Hello Trey + + print(greet.call_count) + # 2 + + print(greet.calls) + # [Call(args=('Trey',), kwargs={}), Call(args=(), kwargs={'name': 'Trey'})] \ No newline at end of file diff --git a/scripts/oreilly_decorators/exercises/main_functions.py b/scripts/oreilly_decorators/exercises/main_functions.py index 8aa2637..cf6dc73 100644 --- a/scripts/oreilly_decorators/exercises/main_functions.py +++ b/scripts/oreilly_decorators/exercises/main_functions.py @@ -2,6 +2,9 @@ from functions import call_later from functions import exclude from functions import call_logger +from functions import call_again +from functions import only_once + print(call(int)) # 0 @@ -42,7 +45,29 @@ def greet(): print("Hello") greet_now = call_logger(greet) -print(greet_now()) +greet_now() # Function started # Hello -# Function returned \ No newline at end of file +# Function returned + +names = [] +response, names_as_str = call_again(str, names) +print(response) +# '[]' +names.append("Diane") +print(names_as_str()) +# "['Diane']" + + +def do_something(x, y): + print(f"doing something with {x} and {y}") + return x * 2 + y ** 2 +do_something_once = only_once(do_something) +print(do_something_once(1, 2)) +# doing something with 1 and 2 +# 6 + +try: + do_something_once(1, 2) +except ValueError as e: + print(e) \ No newline at end of file From 9c0b62bc3a25821d15faf7b35973c712455a6611 Mon Sep 17 00:00:00 2001 From: Alvaro Ortiz Date: Sat, 2 Mar 2024 14:18:56 +0000 Subject: [PATCH 16/20] Add folder for python training --- scripts/python_training/__init__.py | 15 + scripts/python_training/__main__.py | 24 ++ .../m1_python_basics/__init__.py | 2 + .../m1_python_basics/task_1.py | 20 ++ .../m1_python_basics/task_2.py | 46 +++ .../m2_external_libraries/__init__.py | 7 + .../m2_external_libraries/_preprocess.py | 195 +++++++++++++ .../m2_external_libraries/_utils.py | 17 ++ .../m3_internal_libraries/__init__.py | 2 + .../m3_internal_libraries/query.sql | 12 + .../m3_internal_libraries/task_1.py | 36 +++ .../m4_useful_tools/__init__.py | 2 + .../python_training/m4_useful_tools/task_1.py | 43 +++ .../python_training/m4_useful_tools/task_2.py | 32 ++ .../m4_useful_tools/utils/__init__.py | 2 + .../m4_useful_tools/utils/book_tools.py | 12 + .../m5_best_practices/__init__.py | 2 + .../m5_best_practices/task_1_a.py | 47 +++ .../m5_best_practices/task_1_b.py | 15 + .../m5_best_practices/task_1_c.py | 50 ++++ .../m5_best_practices/task_1_d.py | 30 ++ .../m6_python_for_pros/__init__.py | 2 + .../m7_resource_exploitation/__init__.py | 2 + .../m8_the_world_of_docker/__init__.py | 2 + .../m9_more_libraries_and_tools/__init__.py | 2 + scripts/python_training/solutions/__init__.py | 2 + .../solutions/m1_python_basics.py | 275 ++++++++++++++++++ .../solutions/m3_internal_libraries.py | 47 +++ .../solutions/m5_best_practices_a.py | 35 +++ .../solutions/m5_best_practices_b.py | 15 + .../solutions/m5_best_practices_c.py | 50 ++++ .../solutions/m5_best_practices_d.py | 30 ++ .../solutions/tests/conftest.py | 46 +++ .../tests/integration/test_preprocess.py | 27 ++ .../solutions/tests/unit/test_add_lags.py | 32 ++ .../test_enhance_with_technical_indicators.py | 38 +++ .../unit/test_prepare_and_normalize_data.py | 35 +++ 37 files changed, 1251 insertions(+) create mode 100755 scripts/python_training/__init__.py create mode 100755 scripts/python_training/__main__.py create mode 100755 scripts/python_training/m1_python_basics/__init__.py create mode 100755 scripts/python_training/m1_python_basics/task_1.py create mode 100755 scripts/python_training/m1_python_basics/task_2.py create mode 100755 scripts/python_training/m2_external_libraries/__init__.py create mode 100755 scripts/python_training/m2_external_libraries/_preprocess.py create mode 100755 scripts/python_training/m2_external_libraries/_utils.py create mode 100755 scripts/python_training/m3_internal_libraries/__init__.py create mode 100755 scripts/python_training/m3_internal_libraries/query.sql create mode 100755 scripts/python_training/m3_internal_libraries/task_1.py create mode 100755 scripts/python_training/m4_useful_tools/__init__.py create mode 100755 scripts/python_training/m4_useful_tools/task_1.py create mode 100755 scripts/python_training/m4_useful_tools/task_2.py create mode 100755 scripts/python_training/m4_useful_tools/utils/__init__.py create mode 100755 scripts/python_training/m4_useful_tools/utils/book_tools.py create mode 100755 scripts/python_training/m5_best_practices/__init__.py create mode 100755 scripts/python_training/m5_best_practices/task_1_a.py create mode 100755 scripts/python_training/m5_best_practices/task_1_b.py create mode 100755 scripts/python_training/m5_best_practices/task_1_c.py create mode 100755 scripts/python_training/m5_best_practices/task_1_d.py create mode 100755 scripts/python_training/m6_python_for_pros/__init__.py create mode 100755 scripts/python_training/m7_resource_exploitation/__init__.py create mode 100755 scripts/python_training/m8_the_world_of_docker/__init__.py create mode 100755 scripts/python_training/m9_more_libraries_and_tools/__init__.py create mode 100755 scripts/python_training/solutions/__init__.py create mode 100755 scripts/python_training/solutions/m1_python_basics.py create mode 100755 scripts/python_training/solutions/m3_internal_libraries.py create mode 100755 scripts/python_training/solutions/m5_best_practices_a.py create mode 100755 scripts/python_training/solutions/m5_best_practices_b.py create mode 100755 scripts/python_training/solutions/m5_best_practices_c.py create mode 100755 scripts/python_training/solutions/m5_best_practices_d.py create mode 100755 scripts/python_training/solutions/tests/conftest.py create mode 100755 scripts/python_training/solutions/tests/integration/test_preprocess.py create mode 100755 scripts/python_training/solutions/tests/unit/test_add_lags.py create mode 100755 scripts/python_training/solutions/tests/unit/test_enhance_with_technical_indicators.py create mode 100755 scripts/python_training/solutions/tests/unit/test_prepare_and_normalize_data.py diff --git a/scripts/python_training/__init__.py b/scripts/python_training/__init__.py new file mode 100755 index 0000000..993a15b --- /dev/null +++ b/scripts/python_training/__init__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +"""Top level package for Novartis Python Training.""" +import os +from importlib import metadata +from pathlib import Path + +__version__ = metadata.version("python_training") + + +# Base path of python_training module +# (to be used when accessing non .py files in Novartis Python Training/) +WORKDIR = Path(os.getenv("WORKDIR", Path.cwd())) +BASEPATH = Path(__file__).parent +ASSET_DIR = BASEPATH / "assets" +M3_PATH = BASEPATH / "m3_internal_libraries" diff --git a/scripts/python_training/__main__.py b/scripts/python_training/__main__.py new file mode 100755 index 0000000..d94a23d --- /dev/null +++ b/scripts/python_training/__main__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# ruff: noqa: D401 +"""Entry point.""" +import click + +from . import __version__ +from .scripts import dummy + + +def _main() -> None: + """Run main function for entrypoint.""" + + @click.group(chain=True) + @click.version_option(__version__) + def entry_point() -> None: + """Package entry point.""" + + entry_point.add_command(dummy.main) + + entry_point() + + +if __name__ == "__main__": + _main() diff --git a/scripts/python_training/m1_python_basics/__init__.py b/scripts/python_training/m1_python_basics/__init__.py new file mode 100755 index 0000000..b362491 --- /dev/null +++ b/scripts/python_training/m1_python_basics/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tasks for the module 1: Python basics.""" diff --git a/scripts/python_training/m1_python_basics/task_1.py b/scripts/python_training/m1_python_basics/task_1.py new file mode 100755 index 0000000..c02f083 --- /dev/null +++ b/scripts/python_training/m1_python_basics/task_1.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +"""MODULE 1: Python Basics. + +TASK 1: This task is to practice the basic concepts learnt in the first module, found here: + + +Create a python class called "Vehicle". The class will have the following attributes: + * Brand + * Color + * Max_Capacity + * Speed + * Power_Source + * Current_Position + +The class will also have the following functions: + * Accelerate (acceleration, time) + * Brake (acceleration, time) + * Repaint (new_color) + * Maintain_Speed (time) +""" diff --git a/scripts/python_training/m1_python_basics/task_2.py b/scripts/python_training/m1_python_basics/task_2.py new file mode 100755 index 0000000..85a7df8 --- /dev/null +++ b/scripts/python_training/m1_python_basics/task_2.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +"""MODULE 1: Python Basics. + +TASK 2: This task is to practice the more advanced concepts learnt in the first module, found here: + + +Create several python classes that inherit from Vehicle. The classes are: + * CAR + - Attributes: + * Wheels + * License_Plate + * Max_Fuel + * Fuel_Left + * Max_Trunk_Space + * Trunk_Space_Left + * Consumption_Per_Km + - Functions: + * Register_License (new_license) + * Refuel (liters) + * Fill_Trunk (volume) + * Empty_Trunk (volume) + + * BOAT + - Attributes: + * Engines + * Food_Supply_Kg + * Passengers + * Destination + * Crew [formed by several employees: A captain, a chief engineer, a deckhand and a mate] + - Functions: + * Board (num_passengers) + * Ressupply (kg_food) + * Dock () + * Update_destination (new_dest) + + * BIKE + - Attributes: + * Wheels + * Owner + * Size + * Bike_Type + * Gear_Transmission + - Functions: + * Change_Owner (new_owner) + * Change_Transmission (new_trans) +""" diff --git a/scripts/python_training/m2_external_libraries/__init__.py b/scripts/python_training/m2_external_libraries/__init__.py new file mode 100755 index 0000000..491874a --- /dev/null +++ b/scripts/python_training/m2_external_libraries/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +"""Subpackage to implement the methods for Python external libraries training.""" + +from ._preprocess import preprocess +from ._utils import db_connector + +__all__ = ["db_connector", "preprocess"] diff --git a/scripts/python_training/m2_external_libraries/_preprocess.py b/scripts/python_training/m2_external_libraries/_preprocess.py new file mode 100755 index 0000000..d31bf15 --- /dev/null +++ b/scripts/python_training/m2_external_libraries/_preprocess.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- +"""Functions for testing exercises. + +This module is designed to perform various manipulations and analyses on stock price data. +The data is expected to be in a pandas DataFrame with the following columns: + +- Date: The date of the stock data, in 'YYYY-MM-DD' format. +- Open: The opening price of the stock for the day. +- High: The highest price of the stock for the day. +- Low: The lowest price of the stock for the day. +- Close: The closing price of the stock for the day. +- Adj Close: The adjusted closing price of the stock for the day (adjusted for splits and dividends). +- Volume: The number of shares traded during the day. + +DataFrame example: + + Date Open High Low Close Adj Close Volume + 2020-01-01 100 110 95 105 105 10000 + 2020-01-02 105 115 100 110 110 15000 + ... + +These functions are designed to prepare stock price data for analysis and modeling. +They enable users to clean and transform the data, extract meaningful features, and ensure the data is +in a suitable format for various analytical tasks. +""" + +from typing import Union + +import pandas as pd + +from ._utils import db_connector + + +def preprocess( + start_date: Union[str, pd.Timestamp], + end_date: Union[str, pd.Timestamp], + window: int = 20, + lag_days: int = 5, +): + """Applies a series of preprocessing steps to stock price data. + + This function integrates several preprocessing steps: initial data preparation and normalization, + technical indicator enhancement, and the addition of lag features. It's designed to prepare stock + price data for further analysis or modeling. + + :param df: The input DataFrame containing stock price data. + :param start_date: The start date for filtering the data, in 'YYYY-MM-DD' format. + :param end_date: The end date for filtering the data, in 'YYYY-MM-DD' format. + :param window: The window size for calculating moving averages and volatility. Defaults to 20. + :param lag_days: The number of days to create lag features for. Defaults to 5. + + :returns: The processed DataFrame, ready for analysis or modeling, with normalized dates, + filtered by the specified date range, enhanced with technical indicators, and augmented with lag features. + """ + df = db_connector().read_from_db() + + if df is None: + return None + + return ( + df.pipe(_prepare_and_normalize_data, start_date=start_date, end_date=end_date) + .pipe(_enhance_with_technical_indicators, window=window) + .pipe(_add_lags, lag_days=lag_days) + ) + + +def _prepare_and_normalize_data( + df: pd.DataFrame, start_date: Union[str, pd.Timestamp], end_date: Union[str, pd.Timestamp] +): + """Prepares and normalizes stock price data for analysis. + + This function standardizes the date format, filters the dataset based on a specified date range, + generates additional date-related features, calculates daily returns, and ensures that the dataset + has no missing dates within the specified range by forward filling missing values. This comprehensive + preprocessing step is crucial for subsequent financial analysis and modeling tasks. + + :param df: DataFrame with stock price data, expecting columns 'Date' and 'Adj Close'. + :param start_date: String representing the start date in 'YYYY-MM-DD' format. + :param end_date: String representing the end date in 'YYYY-MM-DD' format. + + :return: A DataFrame that has been processed to include only the specified date range, + with added 'Month' and 'DayOfWeek' features, daily returns calculated, and missing dates + forward-filled. The DataFrame is indexed by the 'Date' column. + + :raises: ValueError: If the input DataFrame does not contain a 'Date' column or if the 'Date' column + cannot be converted to datetime format. Also raised if the specified start_date or end_date + are not in the correct format. + """ + if "Date" not in df.columns: + raise ValueError("DataFrame must contain a 'Date' column.") + try: + df["Date"] = pd.to_datetime(df["Date"]) + except ValueError as e: + raise ValueError( + "Error converting 'Date' column to datetime. Ensure the dates are in a correct format." + ) from e + + # Filter Date Range + start_date = pd.to_datetime(start_date) + end_date = pd.to_datetime(end_date) + df = df[(df["Date"] >= start_date) & (df["Date"] <= end_date)] + + # Generate Date Features + df["Month"] = df["Date"].dt.month + df["DayOfWeek"] = df["Date"].dt.dayofweek + + # Calculate Daily Returns + df["Daily Returns"] = df["Adj Close"].pct_change() + + # Fill Missing Dates (forward fill for simplicity) + df = df.set_index("Date").resample("D").ffill() + + return df + + +def _enhance_with_technical_indicators(df: pd.DataFrame, window: int = 20): + """Enriches a DataFrame with key technical indicators to aid in financial analysis. + + This function calculates and appends several technical indicators to the input DataFrame, + specifically a moving average and volatility measure. These indicators are widely used in + stock market analysis to understand price trends and market volatility over a specified + window of time. Additionally, the function ensures that the necessary prerequisites for + calculation are met, such as the presence of required columns. + + :param df: DataFrame after initial preprocessing, with 'Date' and 'Close' columns. + :param window: Integer representing the window size for the moving average. + + :return: The original DataFrame augmented with two new columns: + - 'MA_{window}': The moving average of the 'Close' prices over the specified window. + - 'Volatility': The rolling standard deviation of 'Daily Returns', representing price + volatility over the same window. + + :raises: + - ValueError: If the input DataFrame lacks the required 'Close' or 'Daily Returns' columns, + or if the 'window' parameter is not a positive integer. + - RuntimeError: If an unexpected error occurs during the calculation of technical indicators. + """ + # Validate required columns + required_columns = ["Close", "Daily Returns"] + missing_columns = [col for col in required_columns if col not in df.columns] + if missing_columns: + raise ValueError( + f"DataFrame is missing required columns for technical indicator calculations: {', '.join(missing_columns)}" + ) + + # Validate window size + if not isinstance(window, int) or window <= 0: + raise ValueError("'window' must be a positive integer.") + + try: + # Add Moving Average + df[f"MA_{window}"] = df["Close"].rolling(window=window).mean() + # Calculate Volatility (rolling standard deviation of daily returns) + df["Volatility"] = df["Daily Returns"].rolling(window=window).std() + except Exception as e: + raise RuntimeError(f"An error occurred while calculating technical indicators: {e}") + + return df + + +def _add_lags(df: pd.DataFrame, lag_days: int = 5): + """Enhances a DataFrame by adding lagged features for the 'Close' price column. + + This function creates new columns in the DataFrame, each representing the 'Close' price + shifted by a number of days specified by the 'lag_days' parameter. These lagged features + are useful for time series analysis and forecasting models, as they allow the model to + consider historical price movements. + + :param df: DataFrame with stock price data. + :param lag_days: Number of days to lag features by. + + :return: A DataFrame identical to the input but with additional columns for each + lagged feature. The names of these new columns follow the pattern 'Close_lag_X', where + X is the number of days the 'Close' price is lagged by. + + :raises: ValueError: If the input DataFrame does not contain a 'Close' column or if the 'lag_days' + parameter is not a positive integer. + """ + # Validate 'Close' column presence + if "Close" not in df.columns: + raise ValueError("DataFrame must contain a 'Close' column for lag feature calculations.") + + # Validate lag_days parameter + if not isinstance(lag_days, int) or lag_days <= 0: + raise ValueError("'lag_days' must be a positive integer.") + + try: + # Create Lag Features for 'Close' Price + for i in range(1, lag_days + 1): + df[f"Close_lag_{i}"] = df["Close"].shift(i) + except Exception as e: + # Catching a broad exception to handle unexpected errors during lag feature creation + raise RuntimeError(f"An error occurred while adding lag features: {e}") + + return df diff --git a/scripts/python_training/m2_external_libraries/_utils.py b/scripts/python_training/m2_external_libraries/_utils.py new file mode 100755 index 0000000..cc58c32 --- /dev/null +++ b/scripts/python_training/m2_external_libraries/_utils.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +from loguru import logger + + +class db_connector: + def __init__(self) -> None: + pass + + def read_from_db(self): + """This function is a placeholder for database reading functionality. It's not implemented + yet and is intended to be mocked during testing to return a DataFrame with stock price data. + + :returns: None. Placeholder return value. When mocked, this should return a pd.DataFrame. + """ + logger.warning( + "This function is not implemented yet. For testing purposes, try mocking the read from db with data from the assets folder." + ) diff --git a/scripts/python_training/m3_internal_libraries/__init__.py b/scripts/python_training/m3_internal_libraries/__init__.py new file mode 100755 index 0000000..86d208c --- /dev/null +++ b/scripts/python_training/m3_internal_libraries/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tasks for the module 3: Internal libraries.""" diff --git a/scripts/python_training/m3_internal_libraries/query.sql b/scripts/python_training/m3_internal_libraries/query.sql new file mode 100755 index 0000000..41d1842 --- /dev/null +++ b/scripts/python_training/m3_internal_libraries/query.sql @@ -0,0 +1,12 @@ +WITH all_data AS ( + SELECT DISTINCT drug_id AS drug_id, + ims_id AS ims_id, + country AS country, + molecule AS molecule + FROM SNOWFLAKE_TABLE_WITH_INFO +) +SELECT drug_id, + ims_id, + country, + molecule +FROM all_data diff --git a/scripts/python_training/m3_internal_libraries/task_1.py b/scripts/python_training/m3_internal_libraries/task_1.py new file mode 100755 index 0000000..1630b18 --- /dev/null +++ b/scripts/python_training/m3_internal_libraries/task_1.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +"""MODULE 3: Internal Libraries. + +TASK 1: This task is to practice the basic concepts learnt in the third module, found here: + + +Create a python class that inherits from Extractor (can be found in the nvs-tk) that: + * Gets information from a database. + * Validates that the data is not empty. + * Validates that the data has the correct columns (Found in the sample QUERY). + * Validates that there are no duplicates. + * Doesn't manipulate the data, just returns it as is. + +You can find the query in this folder. +Assume that the SQL Repository has already been created and +you receive it in the Extractor as a parameter. +""" +import pandas as pd +from nvs_sql import SqlRepository +from nvs_tk import Extractor + + +class TaskExtractor(Extractor): + """Extractor designed for the task of module 3.""" + + def __init__(self, repo: SqlRepository) -> None: + """Your code here.""" + + def get(self) -> pd.DataFrame: + """Your code here.""" + + def validate(self, data: pd.DataFrame) -> None: + """Your code here.""" + + def clean(self, data: pd.DataFrame) -> pd.DataFrame: + """Your code here.""" diff --git a/scripts/python_training/m4_useful_tools/__init__.py b/scripts/python_training/m4_useful_tools/__init__.py new file mode 100755 index 0000000..8bdadea --- /dev/null +++ b/scripts/python_training/m4_useful_tools/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tasks for the module 4.""" diff --git a/scripts/python_training/m4_useful_tools/task_1.py b/scripts/python_training/m4_useful_tools/task_1.py new file mode 100755 index 0000000..1ee7732 --- /dev/null +++ b/scripts/python_training/m4_useful_tools/task_1.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +"""Task 1 of Module 4: Useful tools.""" +from datetime import datetime + +import pandas as pd +from loguru import logger + +from python_training.m4_useful_tools.utils.book_tools import insert_book_info_to_frame + + +def main() -> None: + """Get info from book.""" + programming_book = Book("Python Programming", "Jane Doe", 300, 2015) + logger.info(programming_book.book_info()) + + conelly_book = Book("Black Ice", "Michael Conelly", 400, 209) + library = pd.DataFrame(columns=["title", "author", "years_since_publication"]) + library = insert_book_info_to_frame(library, conelly_book.book_info()) + logger.info(library) + + +class Book: + """Class Book.""" + + def __init__(self, title: str, author: str, pages: int, year_of_publication: int) -> None: + self.title = title + self.author = author + self.pages = pages + self.year_of_publication = year_of_publication + + def book_info(self) -> dict: + """Extract the year_since_publication into a method using refactoring.""" + current_year = datetime.now().year + years_since_publication = current_year - self.year_of_publication + return { + "title": self.title, + "author": self.author, + "years_since_publication": years_since_publication, + } + + +if __name__ == "__main__": + main() diff --git a/scripts/python_training/m4_useful_tools/task_2.py b/scripts/python_training/m4_useful_tools/task_2.py new file mode 100755 index 0000000..3499936 --- /dev/null +++ b/scripts/python_training/m4_useful_tools/task_2.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +"""Code to test breakpoints. + +Test the following kinds of breakpoints within the code: +- Conditional breakpoint. +- Hit Breakpoint. +- Log point. +""" +from loguru import logger + + +def main() -> None: + """Execute main function.""" + numbers = list(range(1, 21)) + squared_numbers = calculate_squares(numbers) + logger.info("Squared Numbers:", squared_numbers) + + +def calculate_squares(numbers: list) -> list: + """Find the squares of a list of numbers. + + :param numbers: List of numbers we want to square. + """ + squared_numbers = [] + for number in numbers: + squared = "I'm a Bug! 🐞" if number % 4 == 0 else number**2 + squared_numbers.append(squared) + return squared_numbers + + +if __name__ == "__main__": + main() diff --git a/scripts/python_training/m4_useful_tools/utils/__init__.py b/scripts/python_training/m4_useful_tools/utils/__init__.py new file mode 100755 index 0000000..bbedbce --- /dev/null +++ b/scripts/python_training/m4_useful_tools/utils/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Utils for module 4: Useful tools.""" diff --git a/scripts/python_training/m4_useful_tools/utils/book_tools.py b/scripts/python_training/m4_useful_tools/utils/book_tools.py new file mode 100755 index 0000000..ff31a5c --- /dev/null +++ b/scripts/python_training/m4_useful_tools/utils/book_tools.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +"""Utils for Module 4: Useful tools.""" +import pandas as pd + + +def insert_book_info_to_frame(frame: pd.DataFrame, book_info: dict) -> pd.DataFrame: + """Insert book information into a dataframe. + + :param frame: Data frame to insert book info. + :param book_info: Dictionary with book information. + """ + return frame.append(book_info, ignore_index=True) diff --git a/scripts/python_training/m5_best_practices/__init__.py b/scripts/python_training/m5_best_practices/__init__.py new file mode 100755 index 0000000..d274e41 --- /dev/null +++ b/scripts/python_training/m5_best_practices/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tasks for the module 5: Best practices.""" diff --git a/scripts/python_training/m5_best_practices/task_1_a.py b/scripts/python_training/m5_best_practices/task_1_a.py new file mode 100755 index 0000000..14d4630 --- /dev/null +++ b/scripts/python_training/m5_best_practices/task_1_a.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +"""MODULE 5: Best practices. + +TASK 1: This task is to practice the basic concepts learnt in the fifth module, found here: + + +In this script you will find several snippets of code. Some of them are properly coded, +but others are not. This doesn't mean that they wouldn't work, but they can be +improved in several ways. Some code snippets are in other scripts, because they use specific imports. +""" + +def example_1(num_1, num_2): + """Given two numbers, return true if the first one is bigger than the second one. + + :param num_1: First number. + :param num_2: Second number. + """ + return num_1 > num_2 + + +def example_2(num): + """Convert a number from 1 to 5 into a vowel. + + :param num: Number to convert. + """ + match num: + case 1: + return "a" + case 2: + return "e" + case 3: + return "i" + case 4: + return "o" + case 5: + return "u" + case _: + print("Sorry, invalid number.") + return 0 + + +def example_3(): + f = open("file.txt") + try: + f.read() + finally: + f.close() diff --git a/scripts/python_training/m5_best_practices/task_1_b.py b/scripts/python_training/m5_best_practices/task_1_b.py new file mode 100755 index 0000000..6766e25 --- /dev/null +++ b/scripts/python_training/m5_best_practices/task_1_b.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +"""This code snippet is separated from the rest, but is still part of task 1.""" + +from typing import * + + +class NumberContainer: + """Class NumberContainer.""" + + def __init__(self, value: Union[int, float]) -> None: + self.value = value + + def display_value(self): + """Display the value of the number.""" + print(f"Current value: {self.value}") diff --git a/scripts/python_training/m5_best_practices/task_1_c.py b/scripts/python_training/m5_best_practices/task_1_c.py new file mode 100755 index 0000000..824e525 --- /dev/null +++ b/scripts/python_training/m5_best_practices/task_1_c.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +"""This code snippet is separated from the rest, but is still part of task 1.""" + +from nvs_tk import Extractor + + +class TestExtractor(Extractor): + """Extractor used to retrieve data. + + :param repo: Repository used to retrieve data. + :param param1: Parameter 1. + :param param2: Parameter 2. + """ + + _QUERY_FILE = QUERIES_DIR / "file.sql" + + COLUMNS = frozenset( + { + "col_a", + "col_b", + "col_c", + "col_d", + } + ) + + def __init__(self, repo: SqlRepository, param1: None | int, param2: int) -> None: + self._repo = repo + self._param1 = param1 + self._param2 = param2 + + def get(self) -> pd.DataFrame: + try: + return self._repo.get( + self._QUERY_FILE, + params=utils.filter_version_id( + ("param1", self._param1), + ("param2", self._param2), + ), + ) + + except SqlRepository as exc: + raise ExtractorError("Extractor was unable to retrieve data from DB.") from exc + + def validate(self, data: pd.DataFrame) -> None: + val.validate_not_empty(data) + val.validate_correct_columns(data, self.COLUMNS) + val.validate_no_duplicates(data) + + def clean(self, data: pd.DataFrame) -> pd.DataFrame: + return data \ No newline at end of file diff --git a/scripts/python_training/m5_best_practices/task_1_d.py b/scripts/python_training/m5_best_practices/task_1_d.py new file mode 100755 index 0000000..5e8216a --- /dev/null +++ b/scripts/python_training/m5_best_practices/task_1_d.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +"""This code snippet is separated from the rest, but is still part of task 1.""" + +import sys + +import pandas as pd +from db_tools import start_connection, upload_to_db +from project import BASE_DIR + + +def upload_to_db(): + """Upload system information to database.""" + with open(BASE_DIR / "config.yaml") as file: + config = yaml.safe_load(file) + username = config["credentials"]["username"] + pwd = config["credentials"]["password"] + db = config["credentials"]["database"] + + version_info_dict = { + "major": sys.version_info.major, + "minor": sys.version_info.minor, + "micro": sys.version_info.micro, + "releaselevel": sys.version_info.releaselevel, + "serial": sys.version_info.serial, + } + + version_info_df = pd.DataFrame([version_info_dict]) + + conn = start_connection(db, username, pwd) + upload_to_db(conn, version_info_df) diff --git a/scripts/python_training/m6_python_for_pros/__init__.py b/scripts/python_training/m6_python_for_pros/__init__.py new file mode 100755 index 0000000..4f84c93 --- /dev/null +++ b/scripts/python_training/m6_python_for_pros/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tasks for the module 6: Python for pros.""" diff --git a/scripts/python_training/m7_resource_exploitation/__init__.py b/scripts/python_training/m7_resource_exploitation/__init__.py new file mode 100755 index 0000000..e03515a --- /dev/null +++ b/scripts/python_training/m7_resource_exploitation/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tasks for the module 7: Resource exploitation basics.""" diff --git a/scripts/python_training/m8_the_world_of_docker/__init__.py b/scripts/python_training/m8_the_world_of_docker/__init__.py new file mode 100755 index 0000000..99031bf --- /dev/null +++ b/scripts/python_training/m8_the_world_of_docker/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tasks for the module 8: The world of Docker.""" diff --git a/scripts/python_training/m9_more_libraries_and_tools/__init__.py b/scripts/python_training/m9_more_libraries_and_tools/__init__.py new file mode 100755 index 0000000..07f67be --- /dev/null +++ b/scripts/python_training/m9_more_libraries_and_tools/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Tasks for the module 9: More libraries & tools.""" diff --git a/scripts/python_training/solutions/__init__.py b/scripts/python_training/solutions/__init__.py new file mode 100755 index 0000000..ef529ee --- /dev/null +++ b/scripts/python_training/solutions/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +"""Solutions to the exercises.""" diff --git a/scripts/python_training/solutions/m1_python_basics.py b/scripts/python_training/solutions/m1_python_basics.py new file mode 100755 index 0000000..3928feb --- /dev/null +++ b/scripts/python_training/solutions/m1_python_basics.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- +"""Solutions for the tasks 1 and 2 of module 1: Python Basics.""" +from loguru import logger + +"""TASK 1""" + + +class Vehicle: + """Class Vehicle.""" + + def __init__( + self, + brand: str, + color: str, + max_capacity: int, + speed: float, + power_source: str, + current_position: float, + ) -> None: + self.brand = brand + self.color = color + self.max_capacity = max_capacity + self.speed = speed + self.power_source = power_source + self.current_position = current_position + + def accelerate(self, acc: float, time: int) -> None: + """Calculate the new speed after accelerating. + + :param acc: Rate at which the vehicle is accelerating, in m/s**2. + :param time: Time that the acceleration lasts, in seconds. + """ + self.current_position = self.current_position + self.speed * time + 0.5 * acc * time**2 + self.speed = self.speed + acc * time + logger.info(f"Accelerated to {self.speed} m/s.") + + def brake(self, dec: float, time: int) -> None: + """Calculate the new speed after braking. The speed can not be less than 0. + + :param dec: Rate at which the vehicle is accelerating, in m/s**2. + :param time: Time that the acceleration lasts, in seconds. + """ + self.current_position = self.current_position + self.speed * time + 0.5 * dec * time**2 + self.speed = max(0, self.speed - dec * time) + logger.info(f"Applied brakes. Current speed: {self.speed} m/s.") + + def repaint(self, new_color: str) -> None: + """Change the color of the vehicle. + + :param new_color: New color of the vehicle. + """ + self.color = new_color + logger.info(f"{self.brand} has been repainted to {new_color}.") + + def maintain_speed(self, time: int) -> None: + """Calculate the position after maintaining the same speed for n seconds. + + :param time: Time that the speed is maintained, in seconds. + """ + self.current_position = self.current_position + self.speed * time + logger.info(f"{self.brand} is maintaining speed of {self.speed} km/h for {time} hours.") + + +"""This is an example of how we could use the class +trambesos = Vehicle(brand = "Alstom", color = "Blue", max_capacity = 150, +speed = 0, power_source = "Electric", current_position = 0) + +trambesos.accelerate(15, 4) +trambesos.maintain_speed(54) +trambesos.brake(30, 2) + +distance_run = trambesos.current_position +""" + + +"""TASK 2""" + + +class Car(Vehicle): + """Class Car inheriting from Vehicle.""" + + def __init__( + self, + brand: str, + color: str, + max_capacity: int, + speed: float, + power_source: str, + current_position: float, + wheels: int, + license_plate: str, + max_fuel: float, + fuel_left: float, + max_trunk_space: float, + trunk_space_left: float, + consumption_per_km: float, + ) -> None: + super().__init__(brand, color, max_capacity, speed, power_source, current_position) + self.wheels = wheels + self.license_plate = license_plate + self.max_fuel = max_fuel + self.fuel_left = fuel_left + self.max_trunk_space = max_trunk_space + self.trunk_space_left = trunk_space_left + self.consumption_per_km = consumption_per_km + + def register_license(self, new_license: str) -> None: + """Change the current license to a new one. + + :param new_license: New license of the car. + """ + self.license_plate = new_license + logger.info(f"License plate registered: {new_license}") + + def refuel(self, liters: float) -> None: + """Refill the amount of fuel left. + + :param liters: Liters of fuel added. + """ + self.fuel_left = max(self.max_fuel, self.fuel_left + liters) + logger.info(f"Refueled {liters} liters. Total fuel now: {self.fuel_left} liters.") + + def fill_trunk(self, volume: float) -> None: + """Load objects into the trunk. + + :param volume: Volume of the objects added. + """ + self.trunk_space_left = max(0, self.trunk_space_left - volume) + logger.info(f"Filled trunk with {volume} m3. Space remaining: {self.trunk_space_left} m3.") + + def empty_trunk(self, volume: float) -> None: + """Remove objects from the trunk. + + :param volume: Volume of the objects removed. + """ + self.trunk_space_left = max(self.max_trunk_space, self.trunk_space_left + volume) + logger.info( + f"Emptied {volume} m3 from the trunk. Space remaining: {self.trunk_space_left} m3." + ) + + +"""This is an example of how we could use the class. +car_instance = Car(brand="Toyota", color="Blue", max_capacity=5, speed=60, + power_source="Gasoline", current_position=(0, 0), + wheels=4, license_plate="ABC123", fuel_left=20, + trunk_space_left=3, consumption_per_km=0.1) + +car_instance.register_license("XYZ789") +car_instance.refuel(10) +car_instance.accelerate(10, 2) +car_instance.fill_trunk(2) +car_instance.empty_trunk(1) +""" + + +class Boat(Vehicle): + """Class Boat inheriting from Vehicle.""" + + def __init__( + self, + brand: str, + color: str, + max_capacity: int, + speed: float, + power_source: str, + current_position: float, + engines: int, + food_supply_kg: float, + passengers: int, + destination: str, + crew: list, + ) -> None: + super().__init__(brand, color, max_capacity, speed, power_source, current_position) + self.engines = engines + self.food_supply_kg = food_supply_kg + self.passengers = passengers + self.destination = destination + self.crew = crew + + def board(self, num_passengers: int) -> None: + """Increase the number of passengers. + + :param num_passengers: The amount of passengers that board. + """ + self.passengers += num_passengers + logger.info(f"Boarded {num_passengers} passengers. Total passengers now: {self.passengers}") + + def resupply(self, kg_food: float) -> None: + """Resupply the food left on the boat. + + :param kg_food: Kilograms of food added to the supply. + """ + self.food_supply_kg += kg_food + logger.info( + f"Resupplied {kg_food} kg of food. Total food supply now: {self.food_supply_kg} kg." + ) + + def dock(self) -> None: + """Dock the boat.""" + logger.info("The boat has docked.") + + def update_destination(self, new_dest: str) -> None: + """Update the destination of the boat after docking. + + :param new_dest: The new destination of the boat. + """ + self.destination = new_dest + logger.info(f"Destination updated to {new_dest}.") + + +"""This is an example of how we could use the class. +boat_instance = Boat(brand="SeaMaster", color="White", max_capacity=50, speed=30, + power_source="Diesel", current_position=(0, 0), + engines=2, food_supply_kg=100, passengers=10, destination="Island A", + crew=["Captain John", "Chief Engineer Sarah", "Deckhand Mike", "Mate Emily"]) + +boat_instance.board(5) +boat_instance.accelerate(15, 1) +boat_instance.resupply(50) +boat_instance.update_destination("Island B") +boat_instance.dock() +""" + + +class Bike(Vehicle): + """Class Bike inheriting from Vehicle.""" + + def __init__( + self, + brand: str, + color: str, + max_capacity: int, + speed: float, + power_source: str, + current_position: float, + wheels: int, + owner: str, + size: int, + bike_type: str, + gear_transmission: str, + ) -> None: + super().__init__(brand, color, max_capacity, speed, power_source, current_position) + self.wheels = wheels + self.owner = owner + self.size = size + self.bike_type = bike_type + self.gear_transmission = gear_transmission + + def change_owner(self, new_owner: str) -> None: + """Change the owner of the bike. + + :param new_owner: The new owner of the bike. + """ + self.owner = new_owner + logger.info(f"Changed owner to {new_owner}.") + + def change_transmission(self, new_trans: str) -> None: + """Change the gear transmission of the bike. + + :param new_trans: The new transmission. + """ + self.gear_transmission = new_trans + logger.info(f"Changed transmission to {new_trans}.") + + +"""This is an example of how we could use the class. +bike_instance = Bike(brand="MountainBike", color="Red", max_capacity=1, speed=20, + power_source="Manual", current_position=(0, 0), + wheels=2, owner="Alice", size="Medium", + bike_type="Mountain", gear_transmission="7-speed") + +bike_instance.accelerate(10, 2) +bike_instance.change_owner("Bob") +bike_instance.change_transmission("5-speed") +""" diff --git a/scripts/python_training/solutions/m3_internal_libraries.py b/scripts/python_training/solutions/m3_internal_libraries.py new file mode 100755 index 0000000..53dee68 --- /dev/null +++ b/scripts/python_training/solutions/m3_internal_libraries.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +"""Solutions for the task of module 3: Internal libraries.""" +import pandas as pd +from nvs_sql import SqlRepository, SqlRepositoryError +from nvs_tk import Extractor +from nvs_tk import validation as val +from nvs_tk.errors import ExtractorError + +from python_training import M3_PATH + + +class TaskExtractor(Extractor): + """Extractor designed for the task of module 3.""" + + _QUERY_FILE = M3_PATH / "query.sql" + + COLUMNS = frozenset( + { + "drug_id", + "ims_id", + "country", + "molecule", + } + ) + + def __init__(self, repo: SqlRepository) -> None: + self._repo = repo + + def get(self) -> pd.DataFrame: + """Get the data.""" + try: + return self._repo.get( + self._QUERY_FILE, + ) + + except SqlRepositoryError as exc: + raise ExtractorError("Extractor was unable to retrieve data from DB.") from exc + + def validate(self, data: pd.DataFrame) -> None: + """Validate the data.""" + val.validate_not_empty(data) + val.validate_correct_columns(data, self.COLUMNS) + val.validate_no_duplicates(data) + + def clean(self, data: pd.DataFrame) -> pd.DataFrame: + """Clean the data.""" + return data diff --git a/scripts/python_training/solutions/m5_best_practices_a.py b/scripts/python_training/solutions/m5_best_practices_a.py new file mode 100755 index 0000000..08a08eb --- /dev/null +++ b/scripts/python_training/solutions/m5_best_practices_a.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +"""Solutions for the task of module 5: Best practices.""" + +def example_1_corrected(num_1, num_2): + """Given two numbers, return true if the first one is bigger than the second one. + + :param num_1: First number. + :param num_2: Second number. + """ + return num_1 > num_2 + + +def example_2_corrected(num): + """Convert a number from 1 to 5 into a vowel. + + :param num: Number to convert. + """ + mapper = { + 1: "a", + 2: "e", + 3: "i", + 4: "o", + 5: "u" + } + + try: + return mapper[num] + except KeyError: + print("Sorry, invalid number") + return 0 + + +def example_3_corrected(): + with open("file.txt") as f: + f.read() diff --git a/scripts/python_training/solutions/m5_best_practices_b.py b/scripts/python_training/solutions/m5_best_practices_b.py new file mode 100755 index 0000000..fc66c66 --- /dev/null +++ b/scripts/python_training/solutions/m5_best_practices_b.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +"""This code snippet is separated from the rest, but is still part of task 1.""" + +from typing import Union + + +class NumberContainer: + """Class NumberContainer.""" + + def __init__(self, value: Union[int, float]) -> None: + self.value = value + + def display_value(self): + """Display the value of the number.""" + print(f"Current value: {self.value}") diff --git a/scripts/python_training/solutions/m5_best_practices_c.py b/scripts/python_training/solutions/m5_best_practices_c.py new file mode 100755 index 0000000..824e525 --- /dev/null +++ b/scripts/python_training/solutions/m5_best_practices_c.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +"""This code snippet is separated from the rest, but is still part of task 1.""" + +from nvs_tk import Extractor + + +class TestExtractor(Extractor): + """Extractor used to retrieve data. + + :param repo: Repository used to retrieve data. + :param param1: Parameter 1. + :param param2: Parameter 2. + """ + + _QUERY_FILE = QUERIES_DIR / "file.sql" + + COLUMNS = frozenset( + { + "col_a", + "col_b", + "col_c", + "col_d", + } + ) + + def __init__(self, repo: SqlRepository, param1: None | int, param2: int) -> None: + self._repo = repo + self._param1 = param1 + self._param2 = param2 + + def get(self) -> pd.DataFrame: + try: + return self._repo.get( + self._QUERY_FILE, + params=utils.filter_version_id( + ("param1", self._param1), + ("param2", self._param2), + ), + ) + + except SqlRepository as exc: + raise ExtractorError("Extractor was unable to retrieve data from DB.") from exc + + def validate(self, data: pd.DataFrame) -> None: + val.validate_not_empty(data) + val.validate_correct_columns(data, self.COLUMNS) + val.validate_no_duplicates(data) + + def clean(self, data: pd.DataFrame) -> pd.DataFrame: + return data \ No newline at end of file diff --git a/scripts/python_training/solutions/m5_best_practices_d.py b/scripts/python_training/solutions/m5_best_practices_d.py new file mode 100755 index 0000000..5e8216a --- /dev/null +++ b/scripts/python_training/solutions/m5_best_practices_d.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +"""This code snippet is separated from the rest, but is still part of task 1.""" + +import sys + +import pandas as pd +from db_tools import start_connection, upload_to_db +from project import BASE_DIR + + +def upload_to_db(): + """Upload system information to database.""" + with open(BASE_DIR / "config.yaml") as file: + config = yaml.safe_load(file) + username = config["credentials"]["username"] + pwd = config["credentials"]["password"] + db = config["credentials"]["database"] + + version_info_dict = { + "major": sys.version_info.major, + "minor": sys.version_info.minor, + "micro": sys.version_info.micro, + "releaselevel": sys.version_info.releaselevel, + "serial": sys.version_info.serial, + } + + version_info_df = pd.DataFrame([version_info_dict]) + + conn = start_connection(db, username, pwd) + upload_to_db(conn, version_info_df) diff --git a/scripts/python_training/solutions/tests/conftest.py b/scripts/python_training/solutions/tests/conftest.py new file mode 100755 index 0000000..5625e56 --- /dev/null +++ b/scripts/python_training/solutions/tests/conftest.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +"""Configuration file for pytest.""" +import numpy as np +import pandas as pd +import pytest + + +@pytest.fixture +def df(): + return pd.DataFrame( + { + "Date": ["2020-01-01", "2020-01-02", "2020-01-03", "2020-01-04", "2020-01-05"], + "Open": [100, 101, 100, 103, 104], + "High": [101, 102, 101, 104, 105], + "Low": [99, 100, 99, 102, 103], + "Close": [100.5, 101.5, 100.5, 103.5, 104.5], + "Adj Close": [100, 101, 100, 103, 104], + "Volume": [1000, 1100, 1200, 1300, 1400], + } + ) + + +@pytest.fixture +def df_with_daily_returns(df): + df["Daily Returns"] = [np.nan, 0.01, -0.01, 0.03, 0.01] + return df + + +@pytest.fixture +def start_date(): + return "2020-01-02" + + +@pytest.fixture +def end_date(): + return "2020-01-04" + + +@pytest.fixture +def window(): + return 3 + + +@pytest.fixture +def lag_days(): + return 3 diff --git a/scripts/python_training/solutions/tests/integration/test_preprocess.py b/scripts/python_training/solutions/tests/integration/test_preprocess.py new file mode 100755 index 0000000..c45c751 --- /dev/null +++ b/scripts/python_training/solutions/tests/integration/test_preprocess.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from unittest.mock import patch + +import pandas as pd +import pytest + +from python_training.m2_external_libraries import preprocess + + +@pytest.fixture +def stocks_parquet(): + return pd.read_parquet("tests/assets/stocks_data.parquet") + + +@patch("python_training.m2_external_libraries._utils.db_connector.read_from_db") +def test_preprocess(mock_read_from_db, start_date, end_date, window, lag_days): + mock_read_from_db.return_value = pd.read_parquet("tests/assets/stocks_data.parquet") + processed_df = preprocess(start_date, end_date, window, lag_days) + + assert not processed_df.empty + assert f"MA_{window}" in processed_df.columns + assert "Volatility" in processed_df.columns + for i in range(1, lag_days + 1): + assert f"Close_lag_{i}" in processed_df.columns + + assert processed_df.index.min() >= pd.to_datetime(start_date) + assert processed_df.index.max() <= pd.to_datetime(end_date) diff --git a/scripts/python_training/solutions/tests/unit/test_add_lags.py b/scripts/python_training/solutions/tests/unit/test_add_lags.py new file mode 100755 index 0000000..c308a35 --- /dev/null +++ b/scripts/python_training/solutions/tests/unit/test_add_lags.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +"""Test for ``_add_lags`` function.""" + +import pandas as pd +import pytest + +from python_training.m2_external_libraries._preprocess import _add_lags + + +def test_add_lags(df_with_daily_returns, lag_days): + lagged_df = _add_lags(df_with_daily_returns, lag_days) + + for i in range(1, lag_days + 1): + assert f"Close_lag_{i}" in lagged_df.columns + + for i in range(1, lag_days + 1): + expected_lag = df_with_daily_returns["Close"].shift(i) + pd.testing.assert_series_equal(lagged_df[f"Close_lag_{i}"], expected_lag, check_names=False) + + +def test_add_lags_missing_close_column(df_with_daily_returns): + df_modified = df_with_daily_returns.drop(columns=["Close"]) + + with pytest.raises( + ValueError, match="DataFrame must contain a 'Close' column for lag feature calculations." + ): + _add_lags(df_modified, lag_days=5) + + +def test_add_lags_invalid_lag_days(df_with_daily_returns): + with pytest.raises(ValueError, match="'lag_days' must be a positive integer."): + _add_lags(df_with_daily_returns, lag_days=-1) diff --git a/scripts/python_training/solutions/tests/unit/test_enhance_with_technical_indicators.py b/scripts/python_training/solutions/tests/unit/test_enhance_with_technical_indicators.py new file mode 100755 index 0000000..77ab224 --- /dev/null +++ b/scripts/python_training/solutions/tests/unit/test_enhance_with_technical_indicators.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +"""Test for ``_enhance_with_technical_indicators`` function.""" +import pandas as pd +import pytest + +from python_training.m2_external_libraries._preprocess import _enhance_with_technical_indicators + + +def test_enhance_with_technical_indicators(df_with_daily_returns, window): + enhanced_df = _enhance_with_technical_indicators(df_with_daily_returns, window) + expected_ma = df_with_daily_returns["Close"].rolling(window=window).mean().round(3) + expected_volatility = ( + df_with_daily_returns["Daily Returns"].rolling(window=window).std().round(3) + ) + + assert f"MA_{window}" in enhanced_df.columns + assert "Volatility" in enhanced_df.columns + pd.testing.assert_series_equal( + enhanced_df[f"MA_{window}"].round(3), expected_ma, check_names=False + ) + pd.testing.assert_series_equal( + enhanced_df["Volatility"].round(3), expected_volatility, check_names=False + ) + + +def test_enhance_with_technical_indicators_missing_columns(df_with_daily_returns, window): + df_modified = df_with_daily_returns.drop(columns=["Close"]) + + with pytest.raises( + ValueError, + match="DataFrame is missing required columns for technical indicator calculations", + ): + _enhance_with_technical_indicators(df_modified, window) + + +def test_enhance_with_technical_indicators_invalid_window(df_with_daily_returns): + with pytest.raises(ValueError, match="'window' must be a positive integer."): + _enhance_with_technical_indicators(df_with_daily_returns, window=-1) diff --git a/scripts/python_training/solutions/tests/unit/test_prepare_and_normalize_data.py b/scripts/python_training/solutions/tests/unit/test_prepare_and_normalize_data.py new file mode 100755 index 0000000..1fb7689 --- /dev/null +++ b/scripts/python_training/solutions/tests/unit/test_prepare_and_normalize_data.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +"""Test for ``_prepare_and_normalize_data`` function.""" +import numpy as np +import pandas as pd +import pytest + +from python_training.m2_external_libraries._preprocess import _prepare_and_normalize_data + + +def test_prepare_and_normalize_data(df, start_date, end_date): + processed_df = _prepare_and_normalize_data(df, start_date, end_date) + returns = processed_df["Daily Returns"].round(3).to_list() + + assert processed_df.index[0] == pd.to_datetime(start_date) + assert processed_df.index[-1] == pd.to_datetime(end_date) + + assert "Month" in processed_df.columns + assert "DayOfWeek" in processed_df.columns + assert "Daily Returns" in processed_df.columns + assert np.isnan(returns[0]) + assert returns[1:] == [-0.01, 0.03] + + +def test_prepare_and_normalize_data_missing_date_column(df, start_date, end_date): + df = df.drop(columns=["Date"]) + + with pytest.raises(ValueError, match="DataFrame must contain a 'Date' column."): + _prepare_and_normalize_data(df, start_date, end_date) + + +def test_prepare_and_normalize_data_invalid_date_format(df, start_date, end_date): + df["Date"] = ["not a date", "neither this", "2020-01-03", "2020-01-04", "2020-01-05"] + + with pytest.raises(ValueError, match="Error converting 'Date' column to datetime."): + _prepare_and_normalize_data(df, start_date, end_date) From dba398ba160a5f82b14a576794726e4d7a477532 Mon Sep 17 00:00:00 2001 From: Alvaro Ortiz Date: Sat, 2 Mar 2024 14:20:13 +0000 Subject: [PATCH 17/20] Add folder for python training --- scripts/classes.py | 0 scripts/intermediate_oop/classes.py | 14 ++++++++++++-- scripts/intermediate_oop/dunder.py | 15 ++++++++++++++- scripts/intermediate_oop/main.py | 3 +++ scripts/intermediate_oop/main_dunder.py | 23 +++++++++++++++++++++++ 5 files changed, 52 insertions(+), 3 deletions(-) mode change 100644 => 100755 scripts/classes.py create mode 100644 scripts/intermediate_oop/main_dunder.py diff --git a/scripts/classes.py b/scripts/classes.py old mode 100644 new mode 100755 diff --git a/scripts/intermediate_oop/classes.py b/scripts/intermediate_oop/classes.py index 0ebbaa8..c28ada7 100755 --- a/scripts/intermediate_oop/classes.py +++ b/scripts/intermediate_oop/classes.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- """Class exercises.""" import math +from functools import total_ordering - +@total_ordering class BankAccount: """Bank account including an account balance.""" @@ -32,7 +33,16 @@ def __repr__(self): def __str__(self): return f"A Bank Account with balance={self._balance})" - + + def __eq__(self, other): + if not isinstance(other, BankAccount): + raise NotImplementedError + return self.balance == other.balance + + def __lt__(self, other): + if not isinstance(other, BankAccount): + raise NotImplementedError + return self.balance < other.balance class SuperMap: """Data structure for quickly finding objects based on their attributes.""" diff --git a/scripts/intermediate_oop/dunder.py b/scripts/intermediate_oop/dunder.py index 5085292..171b62d 100755 --- a/scripts/intermediate_oop/dunder.py +++ b/scripts/intermediate_oop/dunder.py @@ -4,7 +4,20 @@ class ReverseView: """Lazily operate on a sequence in reverse.""" - + def __init__(self, list): + self.original_list = list + self.reversed_list = list[::-1] + self.current = self.reversed_list[0] + self.high = self.reversed_list[len(self.reversed_list)-1] + + def __iter__(self): + return self + + def __next__(self): + self.current += 1 + if self.current < self.high: + return self.current + raise StopIteration class Comparator: """Object that is equal to a very small range of numbers.""" diff --git a/scripts/intermediate_oop/main.py b/scripts/intermediate_oop/main.py index ba32b3e..db1473f 100644 --- a/scripts/intermediate_oop/main.py +++ b/scripts/intermediate_oop/main.py @@ -63,3 +63,6 @@ print(circle.radius) print(circle.diameter) print(circle.area) + +print(mary_account > dana_account) + diff --git a/scripts/intermediate_oop/main_dunder.py b/scripts/intermediate_oop/main_dunder.py new file mode 100644 index 0000000..9eb8a44 --- /dev/null +++ b/scripts/intermediate_oop/main_dunder.py @@ -0,0 +1,23 @@ +from dunder import ReverseView + +numbers = [2, 1, 3, 4, 7, 11] +reverse_numbers = ReverseView(numbers) +print(list(reverse_numbers)) + +print(str(reverse_numbers)) +# '[11, 7, 4, 3, 1, 2]' + +print(reverse_numbers[0]) +# 11 + +print(reverse_numbers[-1]) +# 2 + +print(len(reverse_numbers)) +# 6 + + +numbers.append(18) + +print(list(reverse_numbers)) +# [18, 11, 7, 4, 3, 1, 2] \ No newline at end of file From 3d8de06f82df60172dd337da4cf5f1f418a47685 Mon Sep 17 00:00:00 2001 From: Alvaro Ortiz Date: Mon, 28 Oct 2024 21:06:32 +0100 Subject: [PATCH 18/20] feat(freecodecamp): Add Sudoku Solver --- scripts/freecodecamp/sudoku_oop/main.py | 78 +++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 scripts/freecodecamp/sudoku_oop/main.py diff --git a/scripts/freecodecamp/sudoku_oop/main.py b/scripts/freecodecamp/sudoku_oop/main.py new file mode 100644 index 0000000..ca04330 --- /dev/null +++ b/scripts/freecodecamp/sudoku_oop/main.py @@ -0,0 +1,78 @@ +class Board: + def __init__(self, board): + self.board = board + + def __str__(self): + board_str = '' + for row in self.board: + row_str = [str(i) if i else '*' for i in row] + board_str += ' '.join(row_str) + board_str += '\n' + return board_str + + def find_empty_cell(self): + for row, contents in enumerate(self.board): + try: + col = contents.index(0) + return row, col + except ValueError: + pass + return None + + def valid_in_row(self, row, num): + return num not in self.board[row] + + def valid_in_col(self, col, num): + return all(self.board[row][col] != num for row in range(9)) + + def valid_in_square(self, row, col, num): + row_start = (row // 3) * 3 + col_start = (col // 3) * 3 + for row_no in range(row_start, row_start + 3): + for col_no in range(col_start, col_start + 3): + if self.board[row_no][col_no] == num: + return False + return True + + def is_valid(self, empty, num): + row, col = empty + valid_in_row = self.valid_in_row(row, num) + valid_in_col = self.valid_in_col(col, num) + valid_in_square = self.valid_in_square(row, col, num) + return all([valid_in_row, valid_in_col, valid_in_square]) + + def solver(self): + if (next_empty := self.find_empty_cell()) is None: + return True + for guess in range(1, 10): + if self.is_valid(next_empty, guess): + row, col = next_empty + self.board[row][col] = guess + if self.solver(): + return True + self.board[row][col] = 0 + return False + +def solve_sudoku(board): + gameboard = Board(board) + print(f'Puzzle to solve:\n{gameboard}') + if gameboard.solver(): + print(f'Solved puzzle:\n{gameboard}') + else: + print('The provided puzzle is unsolvable.') + return gameboard + +puzzle = [ + [0, 0, 2, 0, 0, 8, 0, 0, 0], + [0, 0, 0, 0, 0, 3, 7, 6, 2], + [4, 3, 0, 0, 0, 0, 8, 0, 0], + [0, 5, 0, 0, 3, 0, 0, 9, 0], + [0, 4, 0, 0, 0, 0, 0, 2, 6], + [0, 0, 0, 4, 6, 7, 0, 0, 0], + [0, 8, 6, 7, 0, 4, 0, 0, 0], + [0, 0, 0, 5, 1, 9, 0, 0, 8], + [1, 7, 0, 0, 0, 6, 0, 0, 5] +] + +if __name__ == "__main__": + solve_sudoku(puzzle) \ No newline at end of file From bf88720024adbbe0ec714be8a69129ab36c3ba54 Mon Sep 17 00:00:00 2001 From: Alvaro Ortiz Date: Mon, 28 Oct 2024 21:07:05 +0100 Subject: [PATCH 19/20] feat(poetry): Update pyproject.toml --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7665a74..f9a3110 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ license = "Propietary" keywords = ["cookiecutter"] -repository = "" +repository = "https://github.com/alvaroof/python_learning" readme = ["README.md", "LICENSE", "RELEASE_NOTES.md"] @@ -31,7 +31,8 @@ exclude = ["tests/*"] [tool.poetry.dependencies] click = "~8" loguru = "~0.6" -python = "~3.9" +python = "~3.12" +tk = "^0.1.0" [tool.poetry.group.dev.dependencies] black = {extras = ["jupyter"], version = "22.6.0"} @@ -50,7 +51,6 @@ pylint = "2.15.10" pytest = "7.2.0" pytest-runner = "6.0.0" pytest-xdist = "3.1.0" -pytest-testmon = "1.4.2" python-dotenv = "0.20.0" radon = "5.1.0" semver = "2.13.0" From 563f953da857e20b3e7fb7a54c9e8db1bde85fff Mon Sep 17 00:00:00 2001 From: Alvaro Ortiz Date: Fri, 4 Apr 2025 08:25:46 +0200 Subject: [PATCH 20/20] Update --- scripts/exercism/robot_name.py | 22 +++++++++++++ .../solution_3.py | 33 +++++++++++++++++++ .../solution_chatgpt.py | 23 +++++++++++++ .../test_solution_3.py | 8 +++++ .../367-valid-perfect-square/solution_367.py | 30 +++++++++++++++++ .../solution_chatgpt.py | 31 +++++++++++++++++ .../test_solution_367.py | 15 +++++++++ 7 files changed, 162 insertions(+) create mode 100644 scripts/exercism/robot_name.py create mode 100644 scripts/leetcode/3-longest-substring-without-repeating-characters/solution_3.py create mode 100644 scripts/leetcode/3-longest-substring-without-repeating-characters/solution_chatgpt.py create mode 100644 scripts/leetcode/3-longest-substring-without-repeating-characters/test_solution_3.py create mode 100644 scripts/leetcode/367-valid-perfect-square/solution_367.py create mode 100644 scripts/leetcode/367-valid-perfect-square/solution_chatgpt.py create mode 100644 scripts/leetcode/367-valid-perfect-square/test_solution_367.py diff --git a/scripts/exercism/robot_name.py b/scripts/exercism/robot_name.py new file mode 100644 index 0000000..035f843 --- /dev/null +++ b/scripts/exercism/robot_name.py @@ -0,0 +1,22 @@ +import random +import string + +class Robot: + robot_names = [] + def __init__(self): + self.name = self.assign_random_name() + self.robot_names.append(self.name) + + def reset(self): + self.name = self.assign_random_name() + + def generate_random_name(self): + letters = ''.join(random.choices(string.ascii_uppercase, k=2)) + digits = ''.join(random.choices(string.digits, k=3)) + return letters + digits + + def assign_random_name(self): + candidate_name = self.generate_random_name() + while candidate_name in Robot.robot_names: + candidate_name = self.generate_random_name() + return candidate_name diff --git a/scripts/leetcode/3-longest-substring-without-repeating-characters/solution_3.py b/scripts/leetcode/3-longest-substring-without-repeating-characters/solution_3.py new file mode 100644 index 0000000..d521fe8 --- /dev/null +++ b/scripts/leetcode/3-longest-substring-without-repeating-characters/solution_3.py @@ -0,0 +1,33 @@ +class Solution: + def lengthOfLongestSubstring(self, s: str) -> int: + if len(s) == 0: return 0 + # starting index + longest_so_far = 1 + chars_in_substring = [] + prev_char = None + hash_map = {} + exit_condition = False + start_index = 0 + current_index = 0 + while not exit_condition: + for char in s[start_index:]: + if (char != prev_char) and (char not in chars_in_substring): + chars_in_substring.append(char) + prev_char = char + hash_map[char] = current_index + else: + if len(chars_in_substring) > longest_so_far: + longest_so_far = len(chars_in_substring) + chars_in_substring = [] + start_index = hash_map[char] + 1 + # char = s[hash_map[char] + 1] + # chars_in_substring.append(char) + prev_char = None + hash_map = {} + # start_index = hash_map[char] + 1 + continue + current_index += 1 + if len(chars_in_substring) > longest_so_far: + longest_so_far = len(chars_in_substring) + exit_condition = True + return longest_so_far \ No newline at end of file diff --git a/scripts/leetcode/3-longest-substring-without-repeating-characters/solution_chatgpt.py b/scripts/leetcode/3-longest-substring-without-repeating-characters/solution_chatgpt.py new file mode 100644 index 0000000..e356f78 --- /dev/null +++ b/scripts/leetcode/3-longest-substring-without-repeating-characters/solution_chatgpt.py @@ -0,0 +1,23 @@ +class Solution: + def lengthOfLongestSubstring(self, s: str) -> int: + if len(s) == 0: + return 0 + + longest_so_far = 0 + start_index = 0 + hash_map = {} + + for current_index, char in enumerate(s): + if char in hash_map and hash_map[char] >= start_index: + # Move the start to one position after the last occurrence + start_index = hash_map[char] + 1 + hash_map[char] = current_index + longest_so_far = max(longest_so_far, current_index - start_index + 1) + + return longest_so_far + +if __name__ == "__main__": + s = "abcabcbb" + solution = Solution() + answer = solution.lengthOfLongestSubstring(s) + print(answer) \ No newline at end of file diff --git a/scripts/leetcode/3-longest-substring-without-repeating-characters/test_solution_3.py b/scripts/leetcode/3-longest-substring-without-repeating-characters/test_solution_3.py new file mode 100644 index 0000000..a9732fe --- /dev/null +++ b/scripts/leetcode/3-longest-substring-without-repeating-characters/test_solution_3.py @@ -0,0 +1,8 @@ +# test_solution.py +from solution_3 import Solution + +def test_longest_substring(): + s = Solution() + assert s.lengthOfLongestSubstring("abcabcbb") == 3 + assert s.lengthOfLongestSubstring("bbbbb") == 1 + assert s.lengthOfLongestSubstring("pwwkew") == 3 \ No newline at end of file diff --git a/scripts/leetcode/367-valid-perfect-square/solution_367.py b/scripts/leetcode/367-valid-perfect-square/solution_367.py new file mode 100644 index 0000000..72632a0 --- /dev/null +++ b/scripts/leetcode/367-valid-perfect-square/solution_367.py @@ -0,0 +1,30 @@ +import pytest + +class Solution: + def isPerfectSquare(self, num: int) -> bool: + if num == 1: + return True + found_square = False + lower_limit = 10 ** ((len(str(num)) // 2) - 1) + if lower_limit < 1: lower_limit = 1 + n = lower_limit + while (n*n <= num): + if num % n == 0: + if n*n == num: + return True + n += 1 + return False + + +def test_perfect_square_true(): + s = Solution() + assert s.isPerfectSquare(9) == True + assert s.isPerfectSquare(16) == True + assert s.isPerfectSquare(1) == True + assert s.isPerfectSquare(10000) == True + +def test_perfect_square_false(): + s = Solution() + assert s.isPerfectSquare(14) == False + assert s.isPerfectSquare(2) == False + assert s.isPerfectSquare(9999) == False diff --git a/scripts/leetcode/367-valid-perfect-square/solution_chatgpt.py b/scripts/leetcode/367-valid-perfect-square/solution_chatgpt.py new file mode 100644 index 0000000..beba32c --- /dev/null +++ b/scripts/leetcode/367-valid-perfect-square/solution_chatgpt.py @@ -0,0 +1,31 @@ +class Solution: + def isPerfectSquare(self, num: int) -> bool: + left, right = 1, num + while left <= right: + mid = (left + right) // 2 + square = mid * mid + if square == num: + return True + elif square < num: + left = mid + 1 + else: + right = mid - 1 + return False + + +if __name__ == "__main__": + num = 9 + solution = Solution() + answer = solution.isPerfectSquare(num) + print(answer) + + num = 14 + solution = Solution() + answer = solution.isPerfectSquare(num) + print(answer) + + num = 16 + solution = Solution() + answer = solution.isPerfectSquare(num) + print(answer) + \ No newline at end of file diff --git a/scripts/leetcode/367-valid-perfect-square/test_solution_367.py b/scripts/leetcode/367-valid-perfect-square/test_solution_367.py new file mode 100644 index 0000000..c1f2e97 --- /dev/null +++ b/scripts/leetcode/367-valid-perfect-square/test_solution_367.py @@ -0,0 +1,15 @@ +# test_solution.py +from solution_367 import Solution + +def test_perfect_square_true(): + s = Solution() + assert s.isPerfectSquare(9) == True + assert s.isPerfectSquare(16) == True + assert s.isPerfectSquare(1) == True + assert s.isPerfectSquare(10000) == True + +def test_perfect_square_false(): + s = Solution() + assert s.isPerfectSquare(14) == False + assert s.isPerfectSquare(2) == False + assert s.isPerfectSquare(9999) == False