From c4acbacdc0f9bfdd5b6a288eef606c0fb1ad76d0 Mon Sep 17 00:00:00 2001 From: SoldatovDaniil <92687673+SoldatovDaniil@users.noreply.github.com> Date: Tue, 27 Sep 2022 23:04:16 +0300 Subject: [PATCH 1/6] Homework --- homeworks/HomeWork2.py | 46 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 homeworks/HomeWork2.py diff --git a/homeworks/HomeWork2.py b/homeworks/HomeWork2.py new file mode 100644 index 0000000..e2ccad7 --- /dev/null +++ b/homeworks/HomeWork2.py @@ -0,0 +1,46 @@ +class Array(object): + + def __init__(self, *args): + self._data = tuple(args) + + def append(self, elem): + self._data = (*self._data, elem) + + def __add__(self, other): + return Array(*(self._data + other._data)) + + def __radd__(self, other): + return Array(*(other._data + self._data)) + + def show(self): + print(self._data) + + def __len__(self): + return len(self._data) + + def index(self, elem): + if elem in self._data: + return self._data.index(elem) + else: + return -1 + + def __getitem__(self, index): + return self._data[index] + + +a = Array(1, 2, 123, 56) +b = Array(1, 1000) + +a.show() +b.show() + +a += b +a = a + b +b = a + Array(0) + +a.show() +b.show() +print(a[3]) + +for i in a: + print(i) From 701ab0df2f89c9646fd40ca2489fda262376d64d Mon Sep 17 00:00:00 2001 From: SoldatovDaniil <92687673+SoldatovDaniil@users.noreply.github.com> Date: Tue, 27 Sep 2022 23:17:25 +0300 Subject: [PATCH 2/6] Homework --- homeworks/{HomeWork2.py => SoldatovDaniil/hw2.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename homeworks/{HomeWork2.py => SoldatovDaniil/hw2.py} (100%) diff --git a/homeworks/HomeWork2.py b/homeworks/SoldatovDaniil/hw2.py similarity index 100% rename from homeworks/HomeWork2.py rename to homeworks/SoldatovDaniil/hw2.py From 0385c0ec08f1c165434df7bedbaeb47c9f4c0ff3 Mon Sep 17 00:00:00 2001 From: SoldatovDaniil <92687673+SoldatovDaniil@users.noreply.github.com> Date: Wed, 28 Sep 2022 20:17:14 +0300 Subject: [PATCH 3/6] add game.py --- homeworks/SoldatovDaniil/{ => 2}/hw2.py | 0 practise/SoldatovDaniil/1/game.py | 79 +++++++++++++++++++++++++ 2 files changed, 79 insertions(+) rename homeworks/SoldatovDaniil/{ => 2}/hw2.py (100%) create mode 100644 practise/SoldatovDaniil/1/game.py diff --git a/homeworks/SoldatovDaniil/hw2.py b/homeworks/SoldatovDaniil/2/hw2.py similarity index 100% rename from homeworks/SoldatovDaniil/hw2.py rename to homeworks/SoldatovDaniil/2/hw2.py diff --git a/practise/SoldatovDaniil/1/game.py b/practise/SoldatovDaniil/1/game.py new file mode 100644 index 0000000..c33513c --- /dev/null +++ b/practise/SoldatovDaniil/1/game.py @@ -0,0 +1,79 @@ +# `random` module is used to shuffle field, see: +# https://docs.python.org/3/library/random.html#random.shuffle +import random + +# Empty tile, there's only one empty cell on a field: +EMPTY_MARK = 'x' + +# Dictionary of possible moves if a form of: +# key -> delta to move the empty tile on a field. +MOVES = { + 'w': -4, + 's': 4, + 'a': -1, + 'd': 1, +} + + +def shuffle_field(): + field = [] + for i in range(1, 16): + field.append(i) + field.append(EMPTY_MARK) + for i in range(100): + random.shuffle(field) + return field + + +def print_field(field): + for i in range(0, 16): + if field[i] == EMPTY_MARK or field[i] < 10: + print(' ', end='') + print(field[i], end=' | ') + if i % 4 == 3: + print() + + +def is_game_finished(field): + for i in range(0, 15): + if field[i] != i + 1: + return False + return True + + +def perform_move(field, key): + for i in range(0, 16): + if field[i] == EMPTY_MARK: + emptyIndex = i + + if emptyIndex % 4 == 3 and key == "d": + return None + if emptyIndex % 4 == 0 and key == "a": + return None + if MOVES[key] + emptyIndex < 0 and 15 < MOVES[key] + emptyIndex: + return None + field[emptyIndex] = field[emptyIndex + MOVES[key]] + field[emptyIndex + MOVES[key]] = EMPTY_MARK + return field + + +def handle_user_input(): + userMove = input() + if userMove not in ['w', 's', 'a', 'd']: + return None + return userMove + + +def main(): + myField = shuffle_field() + + while not(is_game_finished(myField)): + print_field(myField) + key = handle_user_input() + perform_move(myField, key) + + +if __name__ == '__main__': + # See what this means: + # http://stackoverflow.com/questions/419163/what-does-if-name-main-do + main() From 85a74440d1386235bdea49c8bca5c04abc7a4565 Mon Sep 17 00:00:00 2001 From: SoldatovDaniil <92687673+SoldatovDaniil@users.noreply.github.com> Date: Mon, 3 Oct 2022 21:57:30 +0300 Subject: [PATCH 4/6] HW3 --- homeworks/SoldatovDaniil/3/__init__.py | 0 homeworks/SoldatovDaniil/3/contract.py | 37 +++++++++++ homeworks/SoldatovDaniil/3/hw4.md | 85 ++++++++++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 homeworks/SoldatovDaniil/3/__init__.py create mode 100644 homeworks/SoldatovDaniil/3/contract.py create mode 100644 homeworks/SoldatovDaniil/3/hw4.md diff --git a/homeworks/SoldatovDaniil/3/__init__.py b/homeworks/SoldatovDaniil/3/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/homeworks/SoldatovDaniil/3/contract.py b/homeworks/SoldatovDaniil/3/contract.py new file mode 100644 index 0000000..475f5fb --- /dev/null +++ b/homeworks/SoldatovDaniil/3/contract.py @@ -0,0 +1,37 @@ +class ContractError(Exception): + """We use this error when someone breaks our contract.""" + + +#: Special value, that indicates that validation for this type is not required. +Any = object() + + +def contract(arg_types=None, return_type=None, raises=None): + def decorated_contract(func): + def wrapped(*args, **kwargs): + if arg_types is not None: + allArgs = tuple(args + tuple(kwargs.values())) + for index in range(len(arg_types)): + if arg_types[index] != Any: + if not isinstance(allArgs[index], arg_types[index]): + raise ContractError("ArgsType Error") + try: + result = func(*args, **kwargs) + except (Exception if raises is None or Any in raises else raises) as ex: + raise ex + except Exception as ex: + raise ContractError from ex + if return_type is not None: + if not isinstance(result, return_type): + raise ContractError("ReturnType Error") + return result + return wrapped + return decorated_contract + + +@contract(arg_types=(int, int), return_type=float, raises=(ZeroDivisionError,)) +def div(one, two): + return one / two + + +print(div(1, 234.5)) diff --git a/homeworks/SoldatovDaniil/3/hw4.md b/homeworks/SoldatovDaniil/3/hw4.md new file mode 100644 index 0000000..b0a3fc2 --- /dev/null +++ b/homeworks/SoldatovDaniil/3/hw4.md @@ -0,0 +1,85 @@ +## Теория + +### Exceptions + +- `try/except/else/finally`: https://pythonz.net/references/named/try-except-finally/ +- `raise`: https://pythonz.net/references/named/raise/ +- Иерархия Exception: https://docs.python.org/3/library/exceptions.html#exception-hierarchy +- Когда не нужно использовать Exception? https://sobolevn.me/2019/02/python-exceptions-considered-an-antipattern + +### Decorators + +- Декораторы шаг за шагом: https://pythonworld.ru/osnovy/dekoratory.html + +### Generators + +- Iterable vs Iterator vs Generator: https://nvie.com/posts/iterators-vs-generators/ + +### Comprehensions + +- Comprehensions: https://python-3-patterns-idioms-test.readthedocs.io/en/latest/Comprehensions.html + +### Context managers + +- Менеджеры контекста: https://pythonz.net/references/named/contextmanager/ +- `with`: https://pythonz.net/references/named/with/ + + +## Практика + +Задача: реализовать декоратор `@contract`. Смотри файл `contract.py` + +Требования: + +1. Необходимо проверять типы аргументов и тип выходного значения функции. Указываем кортеж типов для `arg_types`. Каждый тип в кортеже - соответсвует типу аргумента. Для типа выходного значения - указываем `return_type` + +```python +@contract(arg_types=(int, int), return_type=int) +def add_two_numbers(first, second): + return first + second + +add_two_numbers(1, 2) # ok +``` + +2. Если передан неправильный тип, вызываем ошибку `ContractError`: + +```python +add_two_numbers('a', 'b') # raises ContractError +``` + +3. Параметр `raises` отвечает за типы исключений, которые функция может кидать. Если выкинутое исключение отсутсвует в списке разрешенных, то мы добавляем `ContractError` (смотри `raise from`). Пример: + +```python +@contract(arg_types=(int, int), return_type=float, raises=(ZeroDivisionError,)) +def div(first, second): + return first / second + +div(1, 2) # ok +div(1, 0) # raises ZeroDisionError +div(1, None) # raises ContractError from TypeError +``` + +4. Можно не передавать какое-то значение из `arg_types` или `return_type`. Или передать значение `None`: тогда ничего не будет происходить. Пример: + +```python +# validates only return type, args and raises are ignored: +@contract(return_type=int) + +# validation is completely disabled: +@contract(return_type=None, arg_types=None, raises=None) + +# return type and raises checks are disabled: +@contract(arg_types=(str, str)) +``` + +5. Можно передать специальное значение `Any` для того, чтобы игнорировать какой-то один тип внутри `arg_types` или `raises`. Например: + +```python +@contract(arg_types=(int, Any)) +def add_two_numbers(first, second): + return first + second + +add_two_numbers(1, 2) # ok +add_two_numbers(1, 3.4) # ok +add_two_numbers(2.1, 1) # raises ContractError +``` From c21e5328e72533dcb94ac03313d74fd6c27570c8 Mon Sep 17 00:00:00 2001 From: SoldatovDaniil <92687673+SoldatovDaniil@users.noreply.github.com> Date: Mon, 3 Oct 2022 22:01:10 +0300 Subject: [PATCH 5/6] HW3 --- homeworks/SoldatovDaniil/3/__init__.py | 0 .../SoldatovDaniil/3/{contract.py => hw3.py} | 0 homeworks/SoldatovDaniil/3/hw4.md | 85 ------------------- 3 files changed, 85 deletions(-) delete mode 100644 homeworks/SoldatovDaniil/3/__init__.py rename homeworks/SoldatovDaniil/3/{contract.py => hw3.py} (100%) delete mode 100644 homeworks/SoldatovDaniil/3/hw4.md diff --git a/homeworks/SoldatovDaniil/3/__init__.py b/homeworks/SoldatovDaniil/3/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/homeworks/SoldatovDaniil/3/contract.py b/homeworks/SoldatovDaniil/3/hw3.py similarity index 100% rename from homeworks/SoldatovDaniil/3/contract.py rename to homeworks/SoldatovDaniil/3/hw3.py diff --git a/homeworks/SoldatovDaniil/3/hw4.md b/homeworks/SoldatovDaniil/3/hw4.md deleted file mode 100644 index b0a3fc2..0000000 --- a/homeworks/SoldatovDaniil/3/hw4.md +++ /dev/null @@ -1,85 +0,0 @@ -## Теория - -### Exceptions - -- `try/except/else/finally`: https://pythonz.net/references/named/try-except-finally/ -- `raise`: https://pythonz.net/references/named/raise/ -- Иерархия Exception: https://docs.python.org/3/library/exceptions.html#exception-hierarchy -- Когда не нужно использовать Exception? https://sobolevn.me/2019/02/python-exceptions-considered-an-antipattern - -### Decorators - -- Декораторы шаг за шагом: https://pythonworld.ru/osnovy/dekoratory.html - -### Generators - -- Iterable vs Iterator vs Generator: https://nvie.com/posts/iterators-vs-generators/ - -### Comprehensions - -- Comprehensions: https://python-3-patterns-idioms-test.readthedocs.io/en/latest/Comprehensions.html - -### Context managers - -- Менеджеры контекста: https://pythonz.net/references/named/contextmanager/ -- `with`: https://pythonz.net/references/named/with/ - - -## Практика - -Задача: реализовать декоратор `@contract`. Смотри файл `contract.py` - -Требования: - -1. Необходимо проверять типы аргументов и тип выходного значения функции. Указываем кортеж типов для `arg_types`. Каждый тип в кортеже - соответсвует типу аргумента. Для типа выходного значения - указываем `return_type` - -```python -@contract(arg_types=(int, int), return_type=int) -def add_two_numbers(first, second): - return first + second - -add_two_numbers(1, 2) # ok -``` - -2. Если передан неправильный тип, вызываем ошибку `ContractError`: - -```python -add_two_numbers('a', 'b') # raises ContractError -``` - -3. Параметр `raises` отвечает за типы исключений, которые функция может кидать. Если выкинутое исключение отсутсвует в списке разрешенных, то мы добавляем `ContractError` (смотри `raise from`). Пример: - -```python -@contract(arg_types=(int, int), return_type=float, raises=(ZeroDivisionError,)) -def div(first, second): - return first / second - -div(1, 2) # ok -div(1, 0) # raises ZeroDisionError -div(1, None) # raises ContractError from TypeError -``` - -4. Можно не передавать какое-то значение из `arg_types` или `return_type`. Или передать значение `None`: тогда ничего не будет происходить. Пример: - -```python -# validates only return type, args and raises are ignored: -@contract(return_type=int) - -# validation is completely disabled: -@contract(return_type=None, arg_types=None, raises=None) - -# return type and raises checks are disabled: -@contract(arg_types=(str, str)) -``` - -5. Можно передать специальное значение `Any` для того, чтобы игнорировать какой-то один тип внутри `arg_types` или `raises`. Например: - -```python -@contract(arg_types=(int, Any)) -def add_two_numbers(first, second): - return first + second - -add_two_numbers(1, 2) # ok -add_two_numbers(1, 3.4) # ok -add_two_numbers(2.1, 1) # raises ContractError -``` From 0eb64c2952099e57ea5b1e1590bafb961b8deddd Mon Sep 17 00:00:00 2001 From: SoldatovDaniil <92687673+SoldatovDaniil@users.noreply.github.com> Date: Tue, 18 Oct 2022 21:35:51 +0300 Subject: [PATCH 6/6] hw4 --- homeworks/SoldatovDaniil/4/tests/conftest.py | 5 + .../4/tests/test_done_command.py | 30 ++++ .../4/tests/test_representations.py | 18 +++ .../4/tests/test_to_read_item.py | 43 ++++++ .../4/tests/test_undone_command.py | 30 ++++ homeworks/SoldatovDaniil/4/todo/__init__.py | 0 homeworks/SoldatovDaniil/4/todo/__main__.py | 28 ++++ homeworks/SoldatovDaniil/4/todo/commands.py | 128 ++++++++++++++++++ .../4/todo/custom_exceptions.py | 2 + homeworks/SoldatovDaniil/4/todo/models.py | 52 +++++++ homeworks/SoldatovDaniil/4/todo/reflection.py | 34 +++++ homeworks/SoldatovDaniil/4/todo/runtime.py | 52 +++++++ homeworks/SoldatovDaniil/4/todo/storage.py | 21 +++ 13 files changed, 443 insertions(+) create mode 100644 homeworks/SoldatovDaniil/4/tests/conftest.py create mode 100644 homeworks/SoldatovDaniil/4/tests/test_done_command.py create mode 100644 homeworks/SoldatovDaniil/4/tests/test_representations.py create mode 100644 homeworks/SoldatovDaniil/4/tests/test_to_read_item.py create mode 100644 homeworks/SoldatovDaniil/4/tests/test_undone_command.py create mode 100644 homeworks/SoldatovDaniil/4/todo/__init__.py create mode 100644 homeworks/SoldatovDaniil/4/todo/__main__.py create mode 100644 homeworks/SoldatovDaniil/4/todo/commands.py create mode 100644 homeworks/SoldatovDaniil/4/todo/custom_exceptions.py create mode 100644 homeworks/SoldatovDaniil/4/todo/models.py create mode 100644 homeworks/SoldatovDaniil/4/todo/reflection.py create mode 100644 homeworks/SoldatovDaniil/4/todo/runtime.py create mode 100644 homeworks/SoldatovDaniil/4/todo/storage.py diff --git a/homeworks/SoldatovDaniil/4/tests/conftest.py b/homeworks/SoldatovDaniil/4/tests/conftest.py new file mode 100644 index 0000000..e846edf --- /dev/null +++ b/homeworks/SoldatovDaniil/4/tests/conftest.py @@ -0,0 +1,5 @@ +import os +import sys + +BASE_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) +sys.path.append(BASE_DIR) diff --git a/homeworks/SoldatovDaniil/4/tests/test_done_command.py b/homeworks/SoldatovDaniil/4/tests/test_done_command.py new file mode 100644 index 0000000..83a6927 --- /dev/null +++ b/homeworks/SoldatovDaniil/4/tests/test_done_command.py @@ -0,0 +1,30 @@ +def test_class_exists(): + from todo.commands import DoneCommand + + assert isinstance(DoneCommand, type) + + +def test_command_label_in_list(): + from todo.commands import DoneCommand + from todo.runtime import get_routes + + assert DoneCommand().label in get_routes() + + +def test_command_execution(monkeypatch): + from todo.commands import DoneCommand + from todo.runtime import perform_command + from todo.models import Storage, ToDoItem + + monkeypatch.setitem(__builtins__, 'input', lambda _: 0) + + item = ToDoItem('test') + item.done = False + + s = Storage() + s.items.clear() + s.items.append(item) + + perform_command(DoneCommand().label) + + assert item.done is True diff --git a/homeworks/SoldatovDaniil/4/tests/test_representations.py b/homeworks/SoldatovDaniil/4/tests/test_representations.py new file mode 100644 index 0000000..07043c8 --- /dev/null +++ b/homeworks/SoldatovDaniil/4/tests/test_representations.py @@ -0,0 +1,18 @@ +def test_to_buy_item(): + from todo.models import ToBuyItem + + item = ToBuyItem('header', 'price') + assert str(item).startswith('- ToBuy: ') + + item.done = True + assert str(item).startswith('+ ToBuy: ') + + +def test_to_do_item(): + from todo.models import ToDoItem + + item = ToDoItem('subject') + assert str(item).startswith('- ToDo: ') + + item.done = True + assert str(item).startswith('+ ToDo: ') diff --git a/homeworks/SoldatovDaniil/4/tests/test_to_read_item.py b/homeworks/SoldatovDaniil/4/tests/test_to_read_item.py new file mode 100644 index 0000000..b1ea1e3 --- /dev/null +++ b/homeworks/SoldatovDaniil/4/tests/test_to_read_item.py @@ -0,0 +1,43 @@ +def test_class_exists(): + from todo.models import ToReadItem + + assert isinstance(ToReadItem, type) + + +def test_there_are_attributes(): + from todo.models import ToReadItem + + heading = 'test' + url = 'http://ya.ru' + item = ToReadItem(heading, url) + + assert item.heading == heading + assert item.url == url + assert item.done is False + + +def test_fabric_method(monkeypatch): + from todo.models import ToReadItem + + value = 'some input' + + monkeypatch.setitem(__builtins__, 'input', lambda _: value) + item = ToReadItem.construct() + + assert item.heading == value + assert item.url == value + assert item.done is False + + +def test_representation(): + from todo.models import ToReadItem + + heading = 'test' + url = 'http://ya.ru' + item = ToReadItem(heading, url) + + assert str(item) == '- ToRead: {} {}'.format(heading, url) + + item.done = True + + assert str(item) == '+ ToRead: {} {}'.format(heading, url) diff --git a/homeworks/SoldatovDaniil/4/tests/test_undone_command.py b/homeworks/SoldatovDaniil/4/tests/test_undone_command.py new file mode 100644 index 0000000..b25bedf --- /dev/null +++ b/homeworks/SoldatovDaniil/4/tests/test_undone_command.py @@ -0,0 +1,30 @@ +def test_class_exists(): + from todo.commands import UndoneCommand + + assert isinstance(UndoneCommand, type) + + +def test_command_label_in_list(): + from todo.commands import UndoneCommand + from todo.runtime import get_routes + + assert UndoneCommand().label in get_routes() + + +def test_command_execution(monkeypatch): + from todo.commands import UndoneCommand + from todo.runtime import perform_command + from todo.models import Storage, ToDoItem + + monkeypatch.setitem(__builtins__, 'input', lambda _: 0) + + item = ToDoItem('test') + item.done = True + + s = Storage() + s.items.clear() + s.items.append(item) + + perform_command(UndoneCommand().label) + + assert item.done is False diff --git a/homeworks/SoldatovDaniil/4/todo/__init__.py b/homeworks/SoldatovDaniil/4/todo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/homeworks/SoldatovDaniil/4/todo/__main__.py b/homeworks/SoldatovDaniil/4/todo/__main__.py new file mode 100644 index 0000000..55f9f2c --- /dev/null +++ b/homeworks/SoldatovDaniil/4/todo/__main__.py @@ -0,0 +1,28 @@ +""" +Main file. Contains program execution logic. +""" + +from todo.custom_exceptions import UserExitException +from todo.runtime import parse_user_input, perform_command + + +def main(): + """ + Main method, works infinitely until user runs `exit` command. + Or hits `Ctrl+C` in the console. + """ + while True: + try: + perform_command(parse_user_input()) + except UserExitException: + break + except Exception as ex: + print('You have done something wrong!', ex) + + +if __name__ == '__main__': + try: + main() + except KeyboardInterrupt: + print() + print('Shutting down, bye!') diff --git a/homeworks/SoldatovDaniil/4/todo/commands.py b/homeworks/SoldatovDaniil/4/todo/commands.py new file mode 100644 index 0000000..e331c06 --- /dev/null +++ b/homeworks/SoldatovDaniil/4/todo/commands.py @@ -0,0 +1,128 @@ + +from todo.custom_exceptions import UserExitException +from todo.models import BaseItem +from todo.reflection import find_classes + + +class BaseCommand(object): + label: str + + def perform(self, store): + raise NotImplementedError() + + +class ListCommand(BaseCommand): + label = 'list' + + def perform(self, store): + if len(store.items) == 0: + print('There are no items in the storage.') + return + + for index, obj in enumerate(store.items): + print('{0}: {1}'.format(index, str(obj))) + + +class NewCommand(BaseCommand): + label = 'new' + + def perform(self, store): + classes = self._load_item_classes() + + print('Select item type:') + for index, name in enumerate(classes.keys()): + print('{0}: {1}'.format(index, name)) + + selection = None + selected_key = None + + while True: + try: + selected_key = self._select_item(classes) + except ValueError: + print('Bad input, try again.') + except IndexError: + print('Wrong index, try again.') + else: + break + + selected_class = classes[selected_key] + print('Selected: {0}'.format(selected_class.__name__)) + print() + + new_object = selected_class.construct() + + store.items.append(new_object) + print('Added {0}'.format(str(new_object))) + print() + return new_object + + def _load_item_classes(self) -> dict: + # Dynamic load: + return dict(find_classes(BaseItem)) + + def _select_item(self, classes): + selection = int(input('Input number: ')) + if selection < 0: + raise IndexError('Index needs to be >0') + return list(classes.keys())[selection] + + +class MarkCommand(object): + + def perform(self, store): + x = 0 + + for obj in store.items: + if str(obj)[0] == self.char_command: + x += 1 + if x == 0: + print("There are no {0} items in the storage.".format(self.char_command)) + return + + for index, obj in enumerate(store.items): + if str(obj)[0] == self.char_command: + print('{0}: {1}'.format(index, str(obj))) + + selected_item = self.select_item(store) + + print("{0} has been {1}\n".format(selected_item, self.operation)) + + operation = getattr(selected_item, str(self.operation), None) + assert operation is not None + operation() + + def select_item(self, store): + while True: + try: + user_selection = int(input("Input number of item: ")) + if user_selection < 0 or str(store.items[user_selection])[0] != self.char_command: + raise IndexError() + except ValueError: + print("Bad input") + except IndexError: + print("Bad index") + else: + break + if store.items is None: + print("There are no items in storage") + return store.items[user_selection] + + +class DoneCommand(MarkCommand, BaseCommand): + label = "done" + char_command = '-' + operation = "mark_done" + + +class UndoneCommand(MarkCommand, BaseCommand): + label = "undone" + char_command = '+' + operation = "mark_undone" + + +class ExitCommand(BaseCommand): + label = 'exit' + + def perform(self, _store): + raise UserExitException('See you next time!') diff --git a/homeworks/SoldatovDaniil/4/todo/custom_exceptions.py b/homeworks/SoldatovDaniil/4/todo/custom_exceptions.py new file mode 100644 index 0000000..5e8963c --- /dev/null +++ b/homeworks/SoldatovDaniil/4/todo/custom_exceptions.py @@ -0,0 +1,2 @@ +class UserExitException(Exception): + """We use this exception when user wants to exit.""" diff --git a/homeworks/SoldatovDaniil/4/todo/models.py b/homeworks/SoldatovDaniil/4/todo/models.py new file mode 100644 index 0000000..8a4f40b --- /dev/null +++ b/homeworks/SoldatovDaniil/4/todo/models.py @@ -0,0 +1,52 @@ +class BaseItem(object): + def __init__(self, heading): + self.heading = heading + self.done = False # TODO: make sure we can use it... + + def __repr__(self): + return self.__class__.__name__ + + def mark_done(self): + self.done = True + + def mark_undone(self): + self.done = False + + def __str__(self): + return "{0} {1}".format('+' if self.done else '-', self.__repr__()) + + @classmethod + def construct(cls): + raise NotImplementedError() + + +class ToDoItem(BaseItem): + def __str__(self): + return '{0}: {1}'.format( + super().__str__(self), + self.heading, + ) + + @classmethod + def construct(cls): + heading = input('Input heading: ') + return cls(heading) + + +class ToBuyItem(BaseItem): + def __init__(self, heading, price): + super().__init__(heading) + self.price = price + + def __str__(self): + return '{0}: {1} for {2}'.format( + super().__str__(self), + self.heading, + self.price, + ) + + @classmethod + def construct(cls): + heading = input('Input heading: ') + price = input('Input price: ') + return cls(heading, price) diff --git a/homeworks/SoldatovDaniil/4/todo/reflection.py b/homeworks/SoldatovDaniil/4/todo/reflection.py new file mode 100644 index 0000000..dee9f85 --- /dev/null +++ b/homeworks/SoldatovDaniil/4/todo/reflection.py @@ -0,0 +1,34 @@ +import inspect +import sys + + +def find_classes(base_class) -> tuple: + """ + Finds all subclasses of a class inside module. + + :param base_class: Base class to search children. + :return: tuple of subclasses + """ + return inspect.getmembers( + sys.modules[base_class.__module__], + _reflect_filter(base_class), + ) + + +def _reflect_filter(base_class): + """ + Reflection is used to load modules dynamically. + + This method is complex. It does some dark magic. + How is it even possible to understand it? + + :param base_class: Target base class + :return: function to filter only subclasses of `base_class` + """ + def class_filter(klass): + return inspect.isclass( + klass, + ) and klass.__module__ == base_class.__module__ and issubclass( + klass, base_class, + ) and klass is not base_class + return class_filter diff --git a/homeworks/SoldatovDaniil/4/todo/runtime.py b/homeworks/SoldatovDaniil/4/todo/runtime.py new file mode 100644 index 0000000..c955fa3 --- /dev/null +++ b/homeworks/SoldatovDaniil/4/todo/runtime.py @@ -0,0 +1,52 @@ +from todo.commands import BaseCommand +from todo.custom_exceptions import UserExitException +from todo.reflection import find_classes +from todo.storage import Storage + + +def get_routes() -> dict: + """ + This function contains the dictionary of possible commands. + :return: `dict` of possible commands, with the format: `name -> class` + """ + routes = find_classes(BaseCommand) + return { + route().label: route + for _, route in routes + } + + +def perform_command(command: str) -> None: + """ + Performs the command by name. + Stores the result in `Storage()`. + :param command: command name, selected by user. + """ + command = command.lower() + routes = get_routes() + + try: + command_class = routes[command] + except KeyError: + print('Bad command, try again.') + return + + command_inst = command_class() + storage = Storage() + + try: + command_inst.perform(storage) + except UserExitException as ex: + # Handling `exit` command. + print(ex) + raise + + +def parse_user_input(): + """ + Gets the user input. + :return: `str` with the user input. + """ + commands = get_routes().keys() + message = 'Input your command: ({0}): '.format('|'.join(commands)) + return input(message) diff --git a/homeworks/SoldatovDaniil/4/todo/storage.py b/homeworks/SoldatovDaniil/4/todo/storage.py new file mode 100644 index 0000000..ef58690 --- /dev/null +++ b/homeworks/SoldatovDaniil/4/todo/storage.py @@ -0,0 +1,21 @@ +class Storage(object): # storage = Storge() + """ + Singleton storage. + + Read more about singleton design pattern: + https://stackoverflow.com/questions/6760685/creating-a-singleton-in-python + https://en.wikipedia.org/wiki/Singleton_pattern + + It is used to emulate in-memory storage. + It should be replaced with a database in a real application. + """ + + obj = None + items = None + + @classmethod + def __new__(cls, *args): + if cls.obj is None: + cls.obj = object.__new__(cls) + cls.items = [] + return cls.obj