1import importlib
2import re
3
4
5class BridgeKeeper:
6 def __init__(self, module_name):
7 self.module_name = module_name
8
9 def __enter__(self):
10 module = importlib.import_module(self.module_name)
11 allowed = {}
12
13 for name in dir(module):
14 obj = getattr(module, name)
15
16 if (
17 self._passes_name(obj)
18 and self._passes_callable(obj)
19 and self._passes_third_question(obj)
20 ):
21 allowed[name] = obj
22
23 return FilteredModule(allowed)
24
25 def __exit__(self, exc_type, exc, tb):
26 return False
27
28 def _passes_name(self, obj):
29 if not hasattr(obj, "__name__"):
30 return False
31 name = obj.__name__
32 return isinstance(name, str) and name[:1].isupper()
33
34 def _passes_callable(self, obj):
35 if not callable(obj):
36 return False
37
38 params = self._parse_docstring(obj)
39
40 try:
41 args = [self._dummy_value(t) for t in params]
42 obj(*args)
43 return True
44 except Exception:
45 return False
46
47 def _parse_docstring(self, obj):
48 doc = obj.__doc__
49 if not doc:
50 return []
51
52 match = re.search(r"Parameters\s*-+\s*((?:.+\n)+?)(?:\n\s*\n|$)", doc)
53 if not match:
54 return []
55
56 params_block = match.group(1)
57 types = re.findall(r"\w+\s*:\s*([^\n]+)", params_block)
58
59 return [t.strip() for t in types]
60
61 def _dummy_value(self, type_str):
62 # union
63 if "|" in type_str:
64 type_str = type_str.split("|")[0].strip()
65
66 if type_str.startswith("list["):
67 inner = type_str[5:-1]
68 return [self._dummy_value(inner)]
69
70 if type_str.startswith("tuple["):
71 inner = type_str[6:-1]
72 return (self._dummy_value(inner),)
73
74 if type_str.startswith("set["):
75 inner = type_str[4:-1]
76 return {self._dummy_value(inner)}
77
78 if type_str.startswith("dict["):
79 inner = type_str[5:-1]
80 k, v = map(str.strip, inner.split(","))
81 return {self._dummy_value(k): self._dummy_value(v)}
82
83 mapping = {
84 "int": 1,
85 "float": 1.0,
86 "str": "a",
87 "bool": True,
88 }
89
90 return mapping.get(type_str, None)
91
92 def _passes_third_question(self, obj):
93 for attr in dir(obj):
94 if attr.startswith("__") and attr.endswith("__"):
95 continue
96
97 if self._valid_attribute_name(attr):
98 return True
99
100 return False
101
102 def _valid_attribute_name(self, name):
103 if re.search(r"[aeiou]{4,}", name):
104 return False
105
106 letters = [c for c in name if c.isalpha()]
107 if not letters:
108 return False
109
110 return letters[-1].isupper()
111
112
113class FilteredModule:
114 def __init__(self, allowed):
115 self._allowed = allowed
116
117 def __getattr__(self, name):
118 if name in self._allowed:
119 return self._allowed[name]
120 raise AttributeError(f"{name} is not allowed")
121
122 def __dir__(self):
123 return list(self._allowed.keys())
...........EF.........F.F
======================================================================
ERROR: test_ignores_parameter_like_lines_outside_parameters_block (test.TestBridgeKeeper.test_ignores_parameter_like_lines_outside_parameters_block)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 37, in test_ignores_parameter_like_lines_outside_parameters_block
self.assertEqual(filtered.tricky_doc(2, "xo"), "xoxo")
^^^^^^^^^^^^^^^^^^^
File "/tmp/solution.py", line 120, in __getattr__
raise AttributeError(f"{name} is not allowed")
AttributeError: tricky_doc is not allowed
======================================================================
FAIL: test_rejects_callable_when_parameter_names_do_not_match (test.TestBridgeKeeper.test_rejects_callable_when_parameter_names_do_not_match)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 59, in test_rejects_callable_when_parameter_names_do_not_match
with self.assertRaises(AttributeError):
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
AssertionError: AttributeError not raised
======================================================================
FAIL: test_rejects_third_answer_with_four_consecutive_uppercase_vowels (test.TestBridgeKeeper.test_rejects_third_answer_with_four_consecutive_uppercase_vowels)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 121, in test_rejects_third_answer_with_four_consecutive_uppercase_vowels
with self.assertRaises(AttributeError):
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
AssertionError: AttributeError not raised
======================================================================
FAIL: test_rejects_union_when_callable_does_not_support_all_union_branches (test.TestBridgeKeeper.test_rejects_union_when_callable_does_not_support_all_union_branches)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/tmp/test.py", line 72, in test_rejects_union_when_callable_does_not_support_all_union_branches
with self.assertRaises(AttributeError):
~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
AssertionError: AttributeError not raised
----------------------------------------------------------------------
Ran 25 tests in 0.008s
FAILED (failures=3, errors=1)
Виктор Бечев
28.04.2026 17:07Отвъд забележките - хубава атомарност на функциите.
|
| f | 1 | import importlib | f | 1 | import importlib |
| n | 2 | import inspect | n | ||
| 3 | import re | 2 | import re | ||
| 4 | 3 | ||||
| 5 | 4 | ||||
| 6 | class BridgeKeeper: | 5 | class BridgeKeeper: | ||
| 7 | def __init__(self, module_name): | 6 | def __init__(self, module_name): | ||
| 8 | self.module_name = module_name | 7 | self.module_name = module_name | ||
| 9 | 8 | ||||
| 10 | def __enter__(self): | 9 | def __enter__(self): | ||
| n | 11 | self.module = importlib.import_module(self.module_name) | n | 10 | module = importlib.import_module(self.module_name) |
| 12 | self.allowed = {} | 11 | allowed = {} | ||
| 13 | 12 | ||||
| n | 14 | for name in dir(self.module): | n | 13 | for name in dir(module): |
| 15 | obj = getattr(self.module, name) | 14 | obj = getattr(module, name) | ||
| 16 | 15 | ||||
| n | n | 16 | if ( | ||
| 17 | if self._passes_name(obj) and \ | 17 | self._passes_name(obj) | ||
| 18 | self._passes_callable(obj) and \ | 18 | and self._passes_callable(obj) | ||
| 19 | self._passes_third_question(obj): | 19 | and self._passes_third_question(obj) | ||
| 20 | ): | ||||
| 21 | allowed[name] = obj | ||||
| 20 | 22 | ||||
| n | 21 | self.allowed[name] = obj | n | ||
| 22 | |||||
| 23 | return FilteredModule(self.allowed) | 23 | return FilteredModule(allowed) | ||
| 24 | 24 | ||||
| 25 | def __exit__(self, exc_type, exc, tb): | 25 | def __exit__(self, exc_type, exc, tb): | ||
| 26 | return False | 26 | return False | ||
| n | 27 | n | |||
| 28 | 27 | ||||
| 29 | def _passes_name(self, obj): | 28 | def _passes_name(self, obj): | ||
| 30 | if not hasattr(obj, "__name__"): | 29 | if not hasattr(obj, "__name__"): | ||
| 31 | return False | 30 | return False | ||
| 32 | name = obj.__name__ | 31 | name = obj.__name__ | ||
| 33 | return isinstance(name, str) and name[:1].isupper() | 32 | return isinstance(name, str) and name[:1].isupper() | ||
| n | 34 | n | |||
| 35 | 33 | ||||
| 36 | def _passes_callable(self, obj): | 34 | def _passes_callable(self, obj): | ||
| 37 | if not callable(obj): | 35 | if not callable(obj): | ||
| 38 | return False | 36 | return False | ||
| 39 | 37 | ||||
| 40 | params = self._parse_docstring(obj) | 38 | params = self._parse_docstring(obj) | ||
| 41 | 39 | ||||
| 42 | try: | 40 | try: | ||
| 43 | args = [self._dummy_value(t) for t in params] | 41 | args = [self._dummy_value(t) for t in params] | ||
| 44 | obj(*args) | 42 | obj(*args) | ||
| 45 | return True | 43 | return True | ||
| 46 | except Exception: | 44 | except Exception: | ||
| 47 | return False | 45 | return False | ||
| 48 | 46 | ||||
| 49 | def _parse_docstring(self, obj): | 47 | def _parse_docstring(self, obj): | ||
| 50 | doc = obj.__doc__ | 48 | doc = obj.__doc__ | ||
| 51 | if not doc: | 49 | if not doc: | ||
| 52 | return [] | 50 | return [] | ||
| 53 | 51 | ||||
| 54 | match = re.search(r"Parameters\s*-+\s*((?:.+\n)+?)(?:\n\s*\n|$)", doc) | 52 | match = re.search(r"Parameters\s*-+\s*((?:.+\n)+?)(?:\n\s*\n|$)", doc) | ||
| 55 | if not match: | 53 | if not match: | ||
| 56 | return [] | 54 | return [] | ||
| 57 | 55 | ||||
| 58 | params_block = match.group(1) | 56 | params_block = match.group(1) | ||
| n | 59 | n | |||
| 60 | types = re.findall(r"\w+\s*:\s*([^\n]+)", params_block) | 57 | types = re.findall(r"\w+\s*:\s*([^\n]+)", params_block) | ||
| 61 | 58 | ||||
| 62 | return [t.strip() for t in types] | 59 | return [t.strip() for t in types] | ||
| n | 63 | n | |||
| 64 | 60 | ||||
| 65 | def _dummy_value(self, type_str): | 61 | def _dummy_value(self, type_str): | ||
| 66 | # union | 62 | # union | ||
| 67 | if "|" in type_str: | 63 | if "|" in type_str: | ||
| 68 | type_str = type_str.split("|")[0].strip() | 64 | type_str = type_str.split("|")[0].strip() | ||
| 69 | 65 | ||||
| 70 | if type_str.startswith("list["): | 66 | if type_str.startswith("list["): | ||
| 71 | inner = type_str[5:-1] | 67 | inner = type_str[5:-1] | ||
| 72 | return [self._dummy_value(inner)] | 68 | return [self._dummy_value(inner)] | ||
| 73 | 69 | ||||
| 74 | if type_str.startswith("tuple["): | 70 | if type_str.startswith("tuple["): | ||
| 75 | inner = type_str[6:-1] | 71 | inner = type_str[6:-1] | ||
| 76 | return (self._dummy_value(inner),) | 72 | return (self._dummy_value(inner),) | ||
| 77 | 73 | ||||
| 78 | if type_str.startswith("set["): | 74 | if type_str.startswith("set["): | ||
| 79 | inner = type_str[4:-1] | 75 | inner = type_str[4:-1] | ||
| 80 | return {self._dummy_value(inner)} | 76 | return {self._dummy_value(inner)} | ||
| 81 | 77 | ||||
| 82 | if type_str.startswith("dict["): | 78 | if type_str.startswith("dict["): | ||
| 83 | inner = type_str[5:-1] | 79 | inner = type_str[5:-1] | ||
| 84 | k, v = map(str.strip, inner.split(",")) | 80 | k, v = map(str.strip, inner.split(",")) | ||
| 85 | return {self._dummy_value(k): self._dummy_value(v)} | 81 | return {self._dummy_value(k): self._dummy_value(v)} | ||
| 86 | 82 | ||||
| 87 | mapping = { | 83 | mapping = { | ||
| 88 | "int": 1, | 84 | "int": 1, | ||
| 89 | "float": 1.0, | 85 | "float": 1.0, | ||
| 90 | "str": "a", | 86 | "str": "a", | ||
| 91 | "bool": True, | 87 | "bool": True, | ||
| 92 | } | 88 | } | ||
| 93 | 89 | ||||
| 94 | return mapping.get(type_str, None) | 90 | return mapping.get(type_str, None) | ||
| 95 | 91 | ||||
| n | 96 | n | |||
| 97 | def _passes_third_question(self, obj): | 92 | def _passes_third_question(self, obj): | ||
| 98 | for attr in dir(obj): | 93 | for attr in dir(obj): | ||
| 99 | if attr.startswith("__") and attr.endswith("__"): | 94 | if attr.startswith("__") and attr.endswith("__"): | ||
| 100 | continue | 95 | continue | ||
| 101 | 96 | ||||
| 102 | if self._valid_attribute_name(attr): | 97 | if self._valid_attribute_name(attr): | ||
| 103 | return True | 98 | return True | ||
| 104 | 99 | ||||
| 105 | return False | 100 | return False | ||
| t | 106 | t | |||
| 107 | 101 | ||||
| 108 | def _valid_attribute_name(self, name): | 102 | def _valid_attribute_name(self, name): | ||
| 109 | if re.search(r"[aeiou]{4,}", name): | 103 | if re.search(r"[aeiou]{4,}", name): | ||
| 110 | return False | 104 | return False | ||
| 111 | 105 | ||||
| 112 | letters = [c for c in name if c.isalpha()] | 106 | letters = [c for c in name if c.isalpha()] | ||
| 113 | if not letters: | 107 | if not letters: | ||
| 114 | return False | 108 | return False | ||
| 115 | 109 | ||||
| 116 | return letters[-1].isupper() | 110 | return letters[-1].isupper() | ||
| 117 | 111 | ||||
| 118 | 112 | ||||
| 119 | class FilteredModule: | 113 | class FilteredModule: | ||
| 120 | def __init__(self, allowed): | 114 | def __init__(self, allowed): | ||
| 121 | self._allowed = allowed | 115 | self._allowed = allowed | ||
| 122 | 116 | ||||
| 123 | def __getattr__(self, name): | 117 | def __getattr__(self, name): | ||
| 124 | if name in self._allowed: | 118 | if name in self._allowed: | ||
| 125 | return self._allowed[name] | 119 | return self._allowed[name] | ||
| 126 | raise AttributeError(f"{name} is not allowed") | 120 | raise AttributeError(f"{name} is not allowed") | ||
| 127 | 121 | ||||
| 128 | def __dir__(self): | 122 | def __dir__(self): | ||
| 129 | return list(self._allowed.keys()) | 123 | return list(self._allowed.keys()) |
| Legends | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
| |||||||||
| f | 1 | import importlib | f | 1 | import importlib |
| 2 | import inspect | 2 | import inspect | ||
| 3 | import re | 3 | import re | ||
| 4 | 4 | ||||
| 5 | 5 | ||||
| 6 | class BridgeKeeper: | 6 | class BridgeKeeper: | ||
| 7 | def __init__(self, module_name): | 7 | def __init__(self, module_name): | ||
| 8 | self.module_name = module_name | 8 | self.module_name = module_name | ||
| 9 | 9 | ||||
| 10 | def __enter__(self): | 10 | def __enter__(self): | ||
| 11 | self.module = importlib.import_module(self.module_name) | 11 | self.module = importlib.import_module(self.module_name) | ||
| 12 | self.allowed = {} | 12 | self.allowed = {} | ||
| 13 | 13 | ||||
| 14 | for name in dir(self.module): | 14 | for name in dir(self.module): | ||
| 15 | obj = getattr(self.module, name) | 15 | obj = getattr(self.module, name) | ||
| 16 | 16 | ||||
| 17 | if self._passes_name(obj) and \ | 17 | if self._passes_name(obj) and \ | ||
| 18 | self._passes_callable(obj) and \ | 18 | self._passes_callable(obj) and \ | ||
| 19 | self._passes_third_question(obj): | 19 | self._passes_third_question(obj): | ||
| 20 | 20 | ||||
| 21 | self.allowed[name] = obj | 21 | self.allowed[name] = obj | ||
| 22 | 22 | ||||
| 23 | return FilteredModule(self.allowed) | 23 | return FilteredModule(self.allowed) | ||
| 24 | 24 | ||||
| 25 | def __exit__(self, exc_type, exc, tb): | 25 | def __exit__(self, exc_type, exc, tb): | ||
| 26 | return False | 26 | return False | ||
| 27 | 27 | ||||
| 28 | 28 | ||||
| 29 | def _passes_name(self, obj): | 29 | def _passes_name(self, obj): | ||
| 30 | if not hasattr(obj, "__name__"): | 30 | if not hasattr(obj, "__name__"): | ||
| 31 | return False | 31 | return False | ||
| 32 | name = obj.__name__ | 32 | name = obj.__name__ | ||
| 33 | return isinstance(name, str) and name[:1].isupper() | 33 | return isinstance(name, str) and name[:1].isupper() | ||
| 34 | 34 | ||||
| 35 | 35 | ||||
| 36 | def _passes_callable(self, obj): | 36 | def _passes_callable(self, obj): | ||
| 37 | if not callable(obj): | 37 | if not callable(obj): | ||
| 38 | return False | 38 | return False | ||
| 39 | 39 | ||||
| 40 | params = self._parse_docstring(obj) | 40 | params = self._parse_docstring(obj) | ||
| 41 | 41 | ||||
| 42 | try: | 42 | try: | ||
| 43 | args = [self._dummy_value(t) for t in params] | 43 | args = [self._dummy_value(t) for t in params] | ||
| 44 | obj(*args) | 44 | obj(*args) | ||
| 45 | return True | 45 | return True | ||
| 46 | except Exception: | 46 | except Exception: | ||
| 47 | return False | 47 | return False | ||
| 48 | 48 | ||||
| n | 49 | n | |||
| 50 | def _parse_docstring(self, obj): | 49 | def _parse_docstring(self, obj): | ||
| 51 | doc = obj.__doc__ | 50 | doc = obj.__doc__ | ||
| 52 | if not doc: | 51 | if not doc: | ||
| 53 | return [] | 52 | return [] | ||
| 54 | 53 | ||||
| n | 55 | lines = doc.splitlines() | n | 54 | match = re.search(r"Parameters\s*-+\s*((?:.+\n)+?)(?:\n\s*\n|$)", doc) |
| 56 | 55 | if not match: | |||
| 57 | start = None | ||||
| 58 | for i, line in enumerate(lines): | ||||
| 59 | if line.strip() == "Parameters": | ||||
| 60 | start = i | ||||
| 61 | break | ||||
| 62 | |||||
| 63 | if start is None: | ||||
| 64 | return [] | 56 | return [] | ||
| 65 | 57 | ||||
| n | 66 | i = start + 2 | n | 58 | params_block = match.group(1) |
| 67 | 59 | ||||
| n | 68 | params = [] | n | 60 | types = re.findall(r"\w+\s*:\s*([^\n]+)", params_block) |
| 69 | 61 | ||||
| t | 70 | while i < len(lines): | t | 62 | return [t.strip() for t in types] |
| 71 | line = lines[i].strip() | ||||
| 72 | if not line: | ||||
| 73 | break | ||||
| 74 | |||||
| 75 | match = re.match(r"(\w+)\s*:\s*(.+)", line) | ||||
| 76 | if match: | ||||
| 77 | _, type_str = match.groups() | ||||
| 78 | params.append(type_str.strip()) | ||||
| 79 | |||||
| 80 | i += 1 | ||||
| 81 | |||||
| 82 | return params | ||||
| 83 | 63 | ||||
| 84 | 64 | ||||
| 85 | def _dummy_value(self, type_str): | 65 | def _dummy_value(self, type_str): | ||
| 86 | # union | 66 | # union | ||
| 87 | if "|" in type_str: | 67 | if "|" in type_str: | ||
| 88 | type_str = type_str.split("|")[0].strip() | 68 | type_str = type_str.split("|")[0].strip() | ||
| 89 | 69 | ||||
| 90 | if type_str.startswith("list["): | 70 | if type_str.startswith("list["): | ||
| 91 | inner = type_str[5:-1] | 71 | inner = type_str[5:-1] | ||
| 92 | return [self._dummy_value(inner)] | 72 | return [self._dummy_value(inner)] | ||
| 93 | 73 | ||||
| 94 | if type_str.startswith("tuple["): | 74 | if type_str.startswith("tuple["): | ||
| 95 | inner = type_str[6:-1] | 75 | inner = type_str[6:-1] | ||
| 96 | return (self._dummy_value(inner),) | 76 | return (self._dummy_value(inner),) | ||
| 97 | 77 | ||||
| 98 | if type_str.startswith("set["): | 78 | if type_str.startswith("set["): | ||
| 99 | inner = type_str[4:-1] | 79 | inner = type_str[4:-1] | ||
| 100 | return {self._dummy_value(inner)} | 80 | return {self._dummy_value(inner)} | ||
| 101 | 81 | ||||
| 102 | if type_str.startswith("dict["): | 82 | if type_str.startswith("dict["): | ||
| 103 | inner = type_str[5:-1] | 83 | inner = type_str[5:-1] | ||
| 104 | k, v = map(str.strip, inner.split(",")) | 84 | k, v = map(str.strip, inner.split(",")) | ||
| 105 | return {self._dummy_value(k): self._dummy_value(v)} | 85 | return {self._dummy_value(k): self._dummy_value(v)} | ||
| 106 | 86 | ||||
| 107 | mapping = { | 87 | mapping = { | ||
| 108 | "int": 1, | 88 | "int": 1, | ||
| 109 | "float": 1.0, | 89 | "float": 1.0, | ||
| 110 | "str": "a", | 90 | "str": "a", | ||
| 111 | "bool": True, | 91 | "bool": True, | ||
| 112 | } | 92 | } | ||
| 113 | 93 | ||||
| 114 | return mapping.get(type_str, None) | 94 | return mapping.get(type_str, None) | ||
| 115 | 95 | ||||
| 116 | 96 | ||||
| 117 | def _passes_third_question(self, obj): | 97 | def _passes_third_question(self, obj): | ||
| 118 | for attr in dir(obj): | 98 | for attr in dir(obj): | ||
| 119 | if attr.startswith("__") and attr.endswith("__"): | 99 | if attr.startswith("__") and attr.endswith("__"): | ||
| 120 | continue | 100 | continue | ||
| 121 | 101 | ||||
| 122 | if self._valid_attribute_name(attr): | 102 | if self._valid_attribute_name(attr): | ||
| 123 | return True | 103 | return True | ||
| 124 | 104 | ||||
| 125 | return False | 105 | return False | ||
| 126 | 106 | ||||
| 127 | 107 | ||||
| 128 | def _valid_attribute_name(self, name): | 108 | def _valid_attribute_name(self, name): | ||
| 129 | if re.search(r"[aeiou]{4,}", name): | 109 | if re.search(r"[aeiou]{4,}", name): | ||
| 130 | return False | 110 | return False | ||
| 131 | 111 | ||||
| 132 | letters = [c for c in name if c.isalpha()] | 112 | letters = [c for c in name if c.isalpha()] | ||
| 133 | if not letters: | 113 | if not letters: | ||
| 134 | return False | 114 | return False | ||
| 135 | 115 | ||||
| 136 | return letters[-1].isupper() | 116 | return letters[-1].isupper() | ||
| 137 | 117 | ||||
| 138 | 118 | ||||
| 139 | class FilteredModule: | 119 | class FilteredModule: | ||
| 140 | def __init__(self, allowed): | 120 | def __init__(self, allowed): | ||
| 141 | self._allowed = allowed | 121 | self._allowed = allowed | ||
| 142 | 122 | ||||
| 143 | def __getattr__(self, name): | 123 | def __getattr__(self, name): | ||
| 144 | if name in self._allowed: | 124 | if name in self._allowed: | ||
| 145 | return self._allowed[name] | 125 | return self._allowed[name] | ||
| 146 | raise AttributeError(f"{name} is not allowed") | 126 | raise AttributeError(f"{name} is not allowed") | ||
| 147 | 127 | ||||
| 148 | def __dir__(self): | 128 | def __dir__(self): | ||
| 149 | return list(self._allowed.keys()) | 129 | return list(self._allowed.keys()) |
| Legends | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
| |||||||||