Предизвикателства > 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()
Дискусия
Василена Станойска
23.10.2024 10:04

Здравейте. В `@type_check("in")(int, float)` трябва да подадем като възможни типове толкова на брой типове, колкото са и аргументите на функцията, която декорираме или може да подадем само един тип, например int и трябва всички аргументи на power (примерно) да проверим дали са int?