Предизвикателства > Abomination decorator


Abomination decorator
Краен срок: 24.10.2024 18:00
Точки: 2

### Въведение За да упражните малко декораторите и да изкарате някоя друга точка, искаме от вас да направите нещо, което абсолютно не трябва да съществува в контекста на Python - декоратор, който верифицира типовете на входните или изходните данни на една функция. Ужас! ### Задачата Напишете декоратор `type_check`, който да може да се използва по следния начин: ``` @type_check("in")(int, float, complex) def power(num, power): return num ** power @type_check("in")(str) @type_check("out")(str) def concatenate(*strings, separator=' beep boop '): return separator.join(strings) ``` При валидни типове, както в горните два примера - очакваме функцията да се изпълни безпроблемно, връщайки коректния резултат. При невалидни... #### type_check("in") При извикване на функция, декорирана с `type_check("in")(<типове>)`, следва да бъдат проверени типовете на **всички** аргументи. Ако някой от тях не е сред изброените такива, функцията трябва да **принтира** следния стринг: ``` "Invalid input arguments, expected <типове>!" ``` Където вместо <типове> имате стринговите репрезентаци на типовете, изброени при декорирането на функцията, съединени с ", " _(запетая и интервал)_. Например `"Invalid input arguments, expected <class 'dict'>, <class 'set'>!"`. Стрингът се принтира **само веднъж** за дадено извикване на декорираната функция, независимо от броят аргументи, които не отговарят на условието за тип. #### type_check("out") При извикване на функция, декорирана с `type_check("out")(<типове>)`, следва да бъде проверен резултатът от изпълнението ѝ. Ако той не е сред изброените такива, функцията трябва да **принтира** следния стринг: ``` "Invalid output value, expected <типове>!" ``` Съдържанието на <типове> е същото като по-горе, но за "out" типовете. #### Резултат от изпълнението Дори и двете условия да са нарушени _(или пък само едното)_, все пак искаме функцията да се изпълни - независимо дали това е да се "счупи" или да върне коректен резултат. Очевидно, ако функцията се счупи по средата на изпълнението си - няма как тя да върне резултат, така че ще принтираме само информация за входните аргументи. ### Уговорки - Принтирането става с `print`. Не ползвайте нищо езотерично, просто функцията `print`. - **Стрингова репрезентация** на типовете, за консистентност и простота ще рече резултатът от `str` извикан върху конкретния тип, напр. `str(dict)` -> `"<class 'dict'>"`. - **Спазвайте шаблона** на съобщението, упоменат по-горе, колкото и да ви се иска да принтирате нещо по-готино. Готиното можете да го сложите в коментар. - Трите уговорки са доста важни, иначе има немалък шанс да ви фейлнат тестовете. - Резултат от изпълнението на функция е само един обект. Дори когато тя връща "множество" стойности (`return a, b, c`) - функцията отново връща само един обект и той е `tuple`. Проверката за тип се извършва само върху този обект. - Този път **задължително** не оставяйте хвърчащи `print`-ове из решението си, тъй като могат да афектират резултата от тестовете. - Ако бяхме една идея по-напред с материала - щяхме да предложим по-шукаритетен вариант да менажираме съобщенията, но за момента ще трябва да се задоволим с `print`. #### Спазвайте шаблона на съобщението, упоменат по-горе. Не `"Invalid in args, expected <типове>!"`, не `"Invalid args, mate, sorry!"`, не `"Баце, тия аргументи не ста'ат!"`. Имаме и по-големи букви, не ни карайте да ги ползваме. ### Пример ``` @type_check("in")(int, float) @type_check("out")(int, float) def power(num, power): return num ** power print(power(6j, 2)) # Invalid input arguments, expected <class 'int'>, <class 'float'>! # Invalid output value, expected <class 'int'>, <class 'float'>! # (-36+0j) @type_check("in")(str) def concatenate(*strings, separator=' beep boop '): return separator.join(strings) print(concatenate(5, '6')) # Invalid input arguments, expected <class 'str'>! # Traceback (most recent call last): # ... # return separator.join(strings) # TypeError: sequence item 0: expected str instance, int found ```
 1import unittest
 2
 3import solution
 4
 5
 6class TestSanity(unittest.TestCase):
 7    """Check if the function is present."""
 8
 9    def test_interfaces(self):
10        self.assertIn('type_check', dir(solution), 'Убеди се, че функцията "type_check" е налична с точно това име.')
11        self.assertTrue(callable(solution.type_check), 'Убеди се, че "type_check" е функция.')
12
13
14if __name__ == '__main__':
15    unittest.main()
  1import itertools
  2import unittest
  3from unittest.mock import call, patch
  4
  5from solution import type_check
  6
  7
  8def join_collections(*collections):
  9    result = []
 10    for collection in collections:
 11        result.extend(collection)
 12    return result
 13
 14def nothing():
 15    pass
 16
 17def lonely_island(main_character, supporting_character, **kwargs):
 18    return f"This is the tale of {main_character} and {supporting_character}!"
 19
 20def divide_by_zero(numbers):
 21    return [number / 0 for number in numbers]
 22
 23
 24class TestTypeCheck(unittest.TestCase):
 25    """Test the function of the decorator."""
 26
 27    BASE_STRING_IN = "Invalid input arguments, expected {}!"
 28    BASE_STRING_OUT = "Invalid output value, expected {}!"
 29
 30    def assert_in_permutations(self, call, types, inout_string):
 31        """Check if call is in a list of all permutations of the types."""
 32        types_permutations = itertools.permutations(types)
 33        types_strings = [", ".join(str(_type) for _type in perm) for perm in types_permutations]
 34        call_permutations = [call(inout_string.format(types_string)) for types_string in types_strings]
 35        self.assertIn(call, call_permutations)
 36
 37    @patch("builtins.print")
 38    def test_check_in(self, mock_print):
 39        """The decorator should report invalid "in" arguments."""
 40        # Single type
 41        decorated = type_check("in")(list)(join_collections)
 42        result = decorated('asdf', 'movie')
 43        self.assertEqual(mock_print.call_count, 1)
 44        mock_print.assert_has_calls(
 45            [call(self.BASE_STRING_IN.format(str(list)))])
 46        self.assertEqual(result, ['a', 's', 'd', 'f', 'm', 'o', 'v', 'i', 'e'])
 47
 48        # Multiple types
 49        mock_print.reset_mock()
 50        types = (list, tuple, set, type(_ for _ in []))
 51        decorated = type_check("in")(*types)(join_collections)
 52        result = decorated('asdf', 'movie')
 53        self.assertEqual(mock_print.call_count, 1)
 54        self.assert_in_permutations(mock_print.mock_calls[0], types, self.BASE_STRING_IN)
 55        self.assertEqual(result, ['a', 's', 'd', 'f', 'm', 'o', 'v', 'i', 'e'])
 56
 57        # Valid input
 58        mock_print.reset_mock()
 59        decorated = type_check("in")(list, tuple, set, type(_ for _ in []))(join_collections)
 60        result = decorated([1, 2], (3,), {4})
 61        mock_print.assert_not_called()
 62        self.assertEqual(result, [1, 2, 3, 4])
 63
 64    @patch("builtins.print")
 65    def test_check_out(self, mock_print):
 66        """The decorator should report an invalid "out" value."""
 67        # Single type
 68        decorated = type_check("out")(
 69            type("Epyt", (type,),{"__repr__": lambda self: f"{self.__name__[::-1].lower()} {self.__class__.__name__[::-1].lower()}"})("Ym", (), {}))(nothing)
 70        # Why do I do these things?
 71        result = decorated()
 72        mock_print.assert_has_calls([call(self.BASE_STRING_OUT.format("my type"))])
 73        self.assertEqual(mock_print.call_count, 1)
 74        self.assertEqual(result, None)
 75
 76        # Multiple types
 77        mock_print.reset_mock()
 78        types = (str, bool, Exception)
 79        decorated = type_check("out")(*types)(nothing)
 80        result = decorated()
 81        self.assertEqual(mock_print.call_count, 1)
 82        self.assert_in_permutations(mock_print.mock_calls[0], types, self.BASE_STRING_OUT)
 83        self.assertEqual(result, None)
 84
 85        # Valid output
 86        mock_print.reset_mock()
 87        decorated = type_check("out")(type(None))(nothing)
 88        result = decorated()
 89        mock_print.assert_not_called()
 90        self.assertEqual(result, None)
 91
 92    @patch("builtins.print")
 93    def test_check_both(self, mock_print):
 94        """The decorator should report invalid "in" and "out" together."""
 95        decorated = type_check("in")(float)(lonely_island)
 96        decorated = type_check("out")(int)(decorated)
 97        result = decorated("Captain Jack Sparrow", "Bill", pirates=True)
 98        mock_print.assert_has_calls(
 99            [call(self.BASE_STRING_IN.format(str(float))),
100             call(self.BASE_STRING_OUT.format(str(int)))])
101        self.assertEqual(mock_print.call_count, 2)
102        self.assertEqual(result, "This is the tale of Captain Jack Sparrow and Bill!")
103
104    @patch("builtins.print")
105    def test_check_decorated_exception(self, mock_print):
106        """The decorator should not supress any exceptions raised."""
107        types = (list, tuple)
108        decorated = type_check("in")(*types)(divide_by_zero)
109        decorated = type_check("out")(*types)(decorated)
110        with self.assertRaises(ZeroDivisionError):
111            decorated({1, 2, 3, 4, 5, 6})
112        self.assert_in_permutations(mock_print.mock_calls[0], types, self.BASE_STRING_IN)
113
114if __name__ == '__main__':
115    unittest.main()
Дискусия
Виктор Бечев
25.10.2024 11:40

@Димитър_Фенерски - Жорката ти отговори на коментара към твоето домашно, аз ще ти отговоря и тук, защото темата е валидна. Приели сме, че не е било очевидно и сме направили необходимите корекции в тестовете - 3-ма души имат разлика в точките (с 1 нагоре). В същото време, за в бъдеще ще разчитаме на вас да питате когато има неясни неща по условието. Няма как да сме експлицитни за всеки един дребен детайл и разчитаме ако примерите не са ви достатъчни и особено ако добавяте "бонус" функционалност - да питате. Ясно е, че когато сме допуснали грешка - ще действаме подобаващо. Просто следващия път няма да третираме ситуации като тази _(приемайки, че са налични примери, разбира се)_ като грешка, така че смело питайте смело!
Димитър Фенерски
24.10.2024 19:13

> Където вместо <типове> имате стринговите репрезентаци на типовете, изброени при декорирането на функцията, съединени с ", " (запетая и интервал). Например "Invalid input arguments, expected <class 'dict'>, <class 'set'>!". > Стрингът се принтира само веднъж за дадено извикване на декорираната функция, независимо от броят аргументи, които не отговарят на условието за тип. не е указано изрично, че подредбата трябва да бъде една и съща в подаването и принтирането, но са взимани точки затова
Георги Кунчев
24.10.2024 16:53

Да, моля. Твоето явно си качила точно по време на сериозния ни проблем.
Камелия Тодорова
24.10.2024 16:52

Ако все още не е налично решението ни , трябва ли да го качим наново?
Георги Кунчев
24.10.2024 16:45

Стана малко проблем. Довечера ще разясним, както и с новина. Всички решения, които бяха изчезнали, вече трябва да са налични.
Стефан Шиваров
24.10.2024 16:34

Само на мен ли ми се изтри изпратеното решение, понеже ми излиза, че имам коментар ама не мога да си видя решението вече?
Йоан Байчев
23.10.2024 23:42

Трябва ли декораторът да запазва метаданните на оригиналната функция , или не е необходимо?
Виктор Бечев
23.10.2024 15:45

Каквото ти е кеф, няма да тестваме с невалидни опции.
Стефан Шиваров
23.10.2024 15:35

Ако като първи аргумент на декоратора подадем нещо различно от "in" или "out", какво поведение се очаква от функцията?
Георги Кунчев
23.10.2024 10:33

Броят типове, които подаваш, не е обвързан с броя параметри, които очаква декорираната функция. Разглеждай подадените типовете като списък от валидни типове, които важат за всички очаквани параметри. Всички аргументи, подадени на функцията, трябва да се сравнят със списъка. Ако типът на който и да било аргумент не е дефиниран в списъка с валидни типове, имаш проблем и принтиш.