Предизвикателства > Безгрешен блок


Безгрешен блок
Краен срок: 09.11.2024 21:00
Точки: 1

Ние не сме безгрешни. Вероятно и вие не сте. Но сме истински! Искаме, обаче, да дефинираме контекстен мениджър, който да ни позволява да изпълняваме _(релативно)_ безгрешен блок от код. ### ProtectedSection Напишете context manager с името `ProtectedSection`, който да има два опционални параметъра - `log` и `suppress`. И двата параметъра приемат като стойност tuple от изключения. Искаме ако при изпълнение на кода в блока след отваряне на контекстният мениджър се възбуди грешка, която е в списъка на `log` - тя да бъде подтисната (не искаме да се прекъсва изпълнението на програмтата) и да бъде записана и да бъде достъпна по следния начин: ``` with ProtectedSection(log=(ZeroDivisionError, IndexError)) as err: x = 1 / 0 print(err.exception) # division by zero print(type(err.exception)) # <class 'ZeroDivisionError'> ``` Забележете, че обектът зад `err.exception` е **изключение** - `ZeroDivisionError`, а не просто някакъв стринг. Ако пък при изпълнение се хвърли грешка, която е в списъка на `suppress` - тя ще бъде подтисната, но няма да бъде записана: ``` with ProtectedSection(suppress=(ZeroDivisionError, IndexError)) as err: x = 1 / 0 print(err.exception) # None ``` Както е видно, в случай, че изключение, което е само "подтиснато", но не и "логнато" - в `err.exception` ще има `None`. **Същото важи и когато изключение не се е хвърлило.** И финално, двете могат да съществуват заедно, като приоритет имат грешките описани в `log`, пред тези в `suppress`: ``` with ProtectedSection(log=(ZeroDivisionError, IndexError), suppress=(TypeError, ZeroDivisionError, Exception)) as err: x = 1 / 0 print(err.exception) # division by zero print(type(err.exception)) # <class 'ZeroDivisionError'> ``` ### Уговорки - Няма да хвърляме изключения, които не са от тип `Exception` (например `KeyboardInterrupt`). - Не се притеснявайте за traceback-а, не ни вълнува за целите на домашното. - _Hint: Не е необходимо да правите нищо специално за да имате поведението `str(err.exception) == "division by zero"` (или както по-горе е написано - `print(err.exception)`, което всъщност имплицитно вика `str`), това е поведение по подразбиране на всичко, което наследява от `Exception`._
 1import unittest
 2
 3import solution
 4
 5
 6class TestSanity(unittest.TestCase):
 7    """Check if the CM is present."""
 8
 9    def test_protected_section(self):
10        self.assertIn('ProtectedSection', dir(solution), 'Убеди се, че класът "ProtectedSection" е наличен с точно това име.')
11        self.assertTrue(isinstance(solution.ProtectedSection, type), 'Убеди се, че "ProtectedSection" е клас.')
12
13
14if __name__ == '__main__':
15    unittest.main()
  1import unittest
  2
  3import solution
  4
  5
  6class TestSolution(unittest.TestCase):
  7    """Test ProtectedSection."""
  8
  9    ERROR_TEXT = 'Ако ползваш AI генератори, ще влезеш в черния списък!'
 10
 11    @staticmethod
 12    def assertEqualException(found, expected):
 13        assert type(expected) is type(found), ('Wrong exception type. '
 14                                               f'{type(expected)} expected, '
 15                                               f'{type(found)} found.')
 16        assert str(expected) == str(found), ('Wrong exception text. '
 17                                             f'"{str(expected)}" expected, '
 18                                             f'"{str(found)}" found.')
 19
 20    def test_solution(self):
 21        """Test everything in a single test case. Only 100% gives a point."""
 22
 23        # Empty input with no exception
 24        with solution.ProtectedSection() as cm:
 25            pass
 26        self.assertIsNone(cm.exception)
 27
 28        # Empty input with exception
 29        with self.assertRaises(KeyError):
 30            with solution.ProtectedSection() as cm:
 31                raise KeyError(self.ERROR_TEXT)
 32        self.assertIsNone(cm.exception)
 33
 34        # Only logged input with one parameter and no exception
 35        with solution.ProtectedSection(log=(ValueError,)) as cm:
 36            pass
 37        self.assertIsNone(cm.exception)
 38
 39        # Only logged input with one parameter and exception
 40        with solution.ProtectedSection(log=(Exception,)) as cm:
 41            raise Exception(self.ERROR_TEXT)
 42        self.assertEqualException(cm.exception, Exception(self.ERROR_TEXT))
 43
 44        # Only suppressed input with one parameter and no exception
 45        with solution.ProtectedSection(suppress=(KeyError,)) as cm:
 46            pass
 47        self.assertIsNone(cm.exception)
 48
 49        # Only suppressed input with one parameter and exception
 50        with solution.ProtectedSection(suppress=(Exception,)) as cm:
 51            raise Exception(self.ERROR_TEXT)
 52        self.assertIsNone(cm.exception)
 53
 54        # Only logged input with multiple parameters and no exception
 55        with solution.ProtectedSection(log=(ValueError, IndexError)) as cm:
 56            pass
 57        self.assertIsNone(cm.exception)
 58
 59        # Only logged input with multiple parameters and exception
 60        with solution.ProtectedSection(log=(TypeError, IndexError)) as cm:
 61            raise IndexError(self.ERROR_TEXT)
 62        self.assertEqualException(cm.exception, IndexError(self.ERROR_TEXT))
 63
 64        # Only suppressed input with multiple parameters and no exception
 65        with solution.ProtectedSection(suppress=(ValueError, IndexError)) as cm:
 66            pass
 67        self.assertIsNone(cm.exception)
 68
 69        # Only suppressed input with multiple parameters and exception
 70        with solution.ProtectedSection(suppress=(TypeError, IndexError)) as cm:
 71            raise IndexError(self.ERROR_TEXT)
 72        self.assertIsNone(cm.exception)
 73
 74        # Only logged input with multiple parameters and different exception
 75        with self.assertRaises(ValueError):
 76            with solution.ProtectedSection(log=(TypeError, IndexError)) as cm:
 77                raise ValueError(self.ERROR_TEXT)
 78        self.assertIsNone(cm.exception)
 79
 80        # Only supressed input with multiple parameters and different exception
 81        with self.assertRaises(ZeroDivisionError):
 82            with solution.ProtectedSection(suppress=(TypeError, IndexError)) as cm:
 83                raise ZeroDivisionError(self.ERROR_TEXT)
 84        self.assertIsNone(cm.exception)
 85
 86        # Mixed input with multiple parameters and no exception
 87        with solution.ProtectedSection(log=(ZeroDivisionError, ValueError), suppress=(TypeError, IndexError)) as cm:
 88            pass
 89        self.assertIsNone(cm.exception)
 90
 91        # Mixed input with multiple parameters and exception from log
 92        with solution.ProtectedSection(log=(ZeroDivisionError, ValueError), suppress=(TypeError, IndexError)) as cm:
 93            raise ZeroDivisionError(self.ERROR_TEXT)
 94        self.assertEqualException(cm.exception, ZeroDivisionError(self.ERROR_TEXT))
 95
 96        # Mixed input with multiple parameters and exception from suppress
 97        with solution.ProtectedSection(log=(ZeroDivisionError, ValueError), suppress=(TypeError, IndexError)) as cm:
 98            raise TypeError(self.ERROR_TEXT)
 99        self.assertIsNone(cm.exception)
100
101        # Text log priority over suppress
102        with solution.ProtectedSection(log=(ImportError, ), suppress=(ImportError, )) as cm:
103            raise ImportError(self.ERROR_TEXT)
104        self.assertEqualException(cm.exception, ImportError(self.ERROR_TEXT))
105
106        # Test nested expressions with inner salenting
107        with solution.ProtectedSection(log=(MemoryError, ), suppress=(MemoryError, )) as cm1:
108            with solution.ProtectedSection(log=(MemoryError, ), suppress=(MemoryError, )) as cm2:
109                raise MemoryError(self.ERROR_TEXT)
110        self.assertIsNone(cm1.exception)
111        self.assertEqualException(cm2.exception, MemoryError(self.ERROR_TEXT))
112
113        # Test nested expressions with outer salenting
114        with solution.ProtectedSection(log=(MemoryError, ), suppress=(MemoryError, )) as cm1:
115            with solution.ProtectedSection(log=(IndexError, ), suppress=(IndexError, )) as cm2:
116                raise MemoryError(self.ERROR_TEXT)
117        self.assertEqualException(cm1.exception, MemoryError(self.ERROR_TEXT))
118        self.assertIsNone(cm2.exception)
119
120    def test_special_cases(self):
121        """Test special cases to show you that you missed something."""
122        # Ensure an isntance can be reused
123        protected_section = solution.ProtectedSection(log=(ZeroDivisionError, IndexError))
124        with protected_section as cm:
125            x = 1 / 0
126        with protected_section as cm:
127            x = 1
128        self.assertIsNone(cm.exception)
129
130        # Ensure the exceptions' types match exactly, not by inheritance
131        with solution.ProtectedSection(log=(Exception, ), suppress=(ZeroDivisionError, )) as cm:
132            raise ZeroDivisionError(self.ERROR_TEXT)
133        self.assertIsNone(cm.exception)
134
135if __name__ == '__main__':
136    unittest.main()
Дискусия
Виктор Бечев
09.11.2024 18:42

Вярно, не гледам внимателно. Трябва да отпечата `None`. Но понеже не сме го уточнили и дискусията се развива доста късно - вероятно няма да ви вземем точката ако само това ви липсва.
Илиан Запрянов
09.11.2024 17:54

@Виктор_Бечев, идеята ми е, че TypeError в случая е в suppress, тоест нямаме err.exception. Тогава при отпечатване на err.exception трябва да отпечата None или тази в предишния with
Георги Кунчев
09.11.2024 17:34

@Йоан_Байчев, при текущата дефиниция, тъй като инстанцията може да пази само един атрибут `exception`, очаквано е вторият ти блок да презапише изключението и резултатът да е "Text 2". Да, това е леко неочаквано поведение, но причината е, че дефиницята, която сме направили, не очаква такова използване. В реалния случай би дефинирал две отделни инстанции за отделните блокове. Ако пък искаме да пазим информация дори с една инстанция, би трябвало да променим изискванията за класа си, защото сега това е случай, който те не могат да поддържат адекватно. По принцип бих дал звезда на подобен въпрос, защото е граничен случай, който дефиницята ни не поддържа, но тъй като вече получи звезда в собственото си решение, ще оставя това "неозвездено". Ще ти кажа, обаче, "Браво" за въпроса. Наистина граничен случай. TLDR - Няма да тестваме с този случай, защото е неочаквано използване на класа ни. ПП: Позволих си да редактирам въпроса ти, за да се визуализира като код. Можеш да използваш бектикове, за да дефинираш блок от код в коментарите си. Още глезотии [тук](https://py-fmi.org/info/showdown)
Йоан Байчев
09.11.2024 17:26

``` protected_section = ProtectedSection(log=(ValueError, )) with protected_section as cm1: raise ValueError('Text 1') print(cm1.exception) with protected_section as cm2: raise ValueError('Text 2') print(cm2.exception) print(cm1.exception) ``` Какво се очаква да бъде поведението в такъв случай?
Виктор Бечев
09.11.2024 14:38

Желателно е да работи правилно за всеки индивидуален контекст. С други думи, в долният пример се очаква да има `TypeError` втория път.
Илиан Запрянов
09.11.2024 14:10

Какво трябва да е поведението при преизползване на инстанция? https://pastebin.com/HCRqcELb
Георги Кунчев
08.11.2024 16:05

Само при точно съвпадение.
Илиан Запрянов
08.11.2024 15:31

```python with ProtectedSection(suppress=(Exception,)) as err: x = 1 / 0 print(err.exception) # Тук трябва ли да се потисне грешката, тъй като ZeroDivisionError е подклас на Exception # или трябва да са точните грешки, т.е. да се потисне само при: with ProtectedSection(suppress=(ZeroDivisionError,)) as err: x = 1 / 0 print(err.exception) ```
Георги Кунчев
07.11.2024 22:29

Да. Очаква се грешката да се хвърли.
Никола Георгиев
07.11.2024 22:11

Какво става ако възбудената грешка не принадлежи на нито единия тюпъл... Пример ``` with ProtectedSection(log=(), suppress=()) as err: x = 1/0 print(err.exception) ``` Вярно ли е че това трябва да се отпечата? ``` Traceback (most recent call last): File "...", line ..., in ... x = 1/0 ZeroDivisionError: division by zero ```