Repository: andreypomortsev/yndx-python-handbook

Issue:
Feature: Add 5 solutions for 4.3 F-J
Выложить решения для задач F,G,H,I,J параграфа 4.3
Покрыть тестами 100% решений, обновить README.md.

Hints from issue comments:

Solution patch:
diff --git a/README.md b/README.md
index 3680a851..d1228b83 100644
--- a/README.md
+++ b/README.md
@@ -540,11 +540,11 @@ def main():
 | C. [Многочлен N-ой степени](./solutions/4.3/43_c.py) | [✅](./tests/4.3/test_43_c.py) |
 | D. [Декор результата](./solutions/4.3/43_d.py) | [✅](./tests/4.3/test_43_d.py) |
 | E. [Накопление результата](./solutions/4.3/43_e.py) | [✅](./tests/4.3/test_43_e.py) |
-| F. [Сортировка слиянием](./solutions/4.3/43_f.py) | [❌](./tests/4.3/test_43_f.py) |
-| G. [Однотипность не порок](./solutions/4.3/43_g.py) | [❌](./tests/4.3/test_43_g.py) |
-| H. [Генератор Фибоначчи](./solutions/4.3/43_h.py) | [❌](./tests/4.3/test_43_h.py) |
-| I. [Циклический генератор](./solutions/4.3/43_i.py) | [❌](./tests/4.3/test_43_i.py) |
-| J. ["Выпрямление" списка](./solutions/4.3/43_j.py) | [❌](./tests/4.3/test_43_j.py) |
+| F. [Сортировка слиянием](./solutions/4.3/43_f.py) | [✅](./tests/4.3/test_43_f.py) |
+| G. [Однотипность не порок](./solutions/4.3/43_g.py) | [✅](./tests/4.3/test_43_g.py) |
+| H. [Генератор Фибоначчи](./solutions/4.3/43_h.py) | [✅](./tests/4.3/test_43_h.py) |
+| I. [Циклический генератор](./solutions/4.3/43_i.py) | [✅](./tests/4.3/test_43_i.py) |
+| J. ["Выпрямление" списка](./solutions/4.3/43_j.py) | [✅](./tests/4.3/test_43_j.py) |
 
 </details>
 
diff --git a/solutions/2.2/22_g.py b/solutions/2.2/22_g.py
index 858796f7..0b0cd7ff 100644
--- a/solutions/2.2/22_g.py
+++ b/solutions/2.2/22_g.py
@@ -1,5 +1,12 @@
-number = input()
-if number[:2] == number[2:][::-1]:
+str_number = input()
+
+number = int(str_number)
+ones = number % 10
+tens = number % 100 // 10
+hundreds = number % 1000 // 100
+thousands = number // 1000
+
+if thousands == ones and tens == hundreds:
     print("YES")
 else:
     print("NO")
diff --git a/solutions/4.3/43_d.py b/solutions/4.3/43_d.py
index a7cf47c8..a71e96e1 100644
--- a/solutions/4.3/43_d.py
+++ b/solutions/4.3/43_d.py
@@ -1,8 +1,5 @@
-from typing import Any, Callable
-
-
-def answer(func: Callable[..., Any]) -> str:
-    def wrapper(*args, **kwargs):
+def answer(func):
+    def wrapper(*args, **kwargs) -> str:
         return f"Результат функции: {func(*args, **kwargs)}"
 
     return wrapper
diff --git a/solutions/4.3/43_e.py b/solutions/4.3/43_e.py
index 75373b17..a0b10366 100644
--- a/solutions/4.3/43_e.py
+++ b/solutions/4.3/43_e.py
@@ -1,12 +1,5 @@
-from typing import Any, Callable, List, Optional
-
-
-def result_accumulator(
-    func: Callable[..., Any]
-) -> Callable[..., Optional[List[Any]]]:
-    def wrapper(
-        *args: Any, method: str = "accumulate", **kwargs: Any
-    ) -> Optional[List[Any]]:
+def result_accumulator(func):
+    def wrapper(*args, method: str = "accumulate", **kwargs):
         if not hasattr(wrapper, "results"):
             wrapper.results = []
 
diff --git a/solutions/4.3/43_f.py b/solutions/4.3/43_f.py
new file mode 100644
index 00000000..8f190657
--- /dev/null
+++ b/solutions/4.3/43_f.py
@@ -0,0 +1,27 @@
+def merge_sort(numbers: list) -> list:
+    if len(numbers) <= 1:
+        return numbers
+    mid = len(numbers) // 2
+    left_part = numbers[:mid]
+    right_part = numbers[mid:]
+
+    return merge(merge_sort(left_part), merge_sort(right_part))
+
+
+def merge(left: list, right: list) -> list:
+    merged = []
+    left_index = right_index = 0
+    l_length, r_length = len(left), len(right)
+
+    while left_index < l_length and right_index < r_length:
+        if left[left_index] < right[right_index]:
+            merged.append(left[left_index])
+            left_index += 1
+        else:
+            merged.append(right[right_index])
+            right_index += 1
+
+    merged += left[left_index:]
+    merged += right[right_index:]
+
+    return merged
diff --git a/solutions/4.3/43_g.py b/solutions/4.3/43_g.py
new file mode 100644
index 00000000..8072bc7c
--- /dev/null
+++ b/solutions/4.3/43_g.py
@@ -0,0 +1,11 @@
+def same_type(func):
+    def wrapper(*args, **kwargs):
+        data_type = type(args[0])
+        for data in args[1:]:
+            if not isinstance(data, data_type):
+                print("Обнаружены различные типы данных")
+                return "Fail"
+        else:
+            return func(*args, **kwargs)
+
+    return wrapper
diff --git a/solutions/4.3/43_h.py b/solutions/4.3/43_h.py
new file mode 100644
index 00000000..555ca6e1
--- /dev/null
+++ b/solutions/4.3/43_h.py
@@ -0,0 +1,12 @@
+def fibonacci(num):
+    if not num:
+        return
+    if num < 2:
+        yield 0
+    else:
+        a, b = 0, 1
+        yield a
+        yield b
+        for _ in range(num - 2):
+            a, b = b, a + b
+            yield b
diff --git a/solutions/4.3/43_i.py b/solutions/4.3/43_i.py
new file mode 100644
index 00000000..1c4b255f
--- /dev/null
+++ b/solutions/4.3/43_i.py
@@ -0,0 +1,7 @@
+def cycle(iterable):
+    index = 0
+    while True:
+        yield iterable[index]
+        index += 1
+        if index == len(iterable):
+            index = 0
diff --git a/solutions/4.3/43_j.py b/solutions/4.3/43_j.py
new file mode 100644
index 00000000..03c6e013
--- /dev/null
+++ b/solutions/4.3/43_j.py
@@ -0,0 +1,10 @@
+def make_linear(iterable):
+    answer = []
+
+    for i in iterable:
+        if isinstance(i, list):
+            answer.extend(make_linear(i))
+        else:
+            answer.append(i)
+
+    return answer


Test patch:
diff --git a/tests/4.3/test_43_d.py b/tests/4.3/test_43_d.py
index 51e35820..7d5e8e23 100644
--- a/tests/4.3/test_43_d.py
+++ b/tests/4.3/test_43_d.py
@@ -3,6 +3,7 @@
 from typing import Callable, Tuple
 
 import pytest
+from _pytest.monkeypatch import MonkeyPatch
 
 from tests import utils
 from tests.data.test_data_43 import (test_data_d_a_plus_b,
@@ -16,7 +17,7 @@
     ids=[data[2] for data in test_data_d_a_plus_b],
 )
 def test_answer_a_plus_b(
-    monkeypatch: pytest.MonkeyPatch,
+    monkeypatch: MonkeyPatch,
     request: pytest.FixtureRequest,
     load_module: Callable[[str], ModuleType],
     args: Tuple[int],
@@ -44,7 +45,7 @@ def a_plus_b(a, b):
     ids=[data[2] for data in test_data_d_get_letters],
 )
 def test_answer_get_letters(
-    monkeypatch: pytest.MonkeyPatch,
+    monkeypatch: MonkeyPatch,
     request: pytest.FixtureRequest,
     load_module: Callable[[str], ModuleType],
     args: Tuple[str],
@@ -72,7 +73,7 @@ def get_letters(text: str) -> str:
     ids=[data[2] for data in test_data_d_get_sum_of_ords],
 )
 def test_answer_get_sum_of_ords(
-    monkeypatch: pytest.MonkeyPatch,
+    monkeypatch: MonkeyPatch,
     request: pytest.FixtureRequest,
     load_module: Callable[[str], ModuleType],
     args: Tuple[str],
diff --git a/tests/4.3/test_43_f.py b/tests/4.3/test_43_f.py
new file mode 100644
index 00000000..d82e6a67
--- /dev/null
+++ b/tests/4.3/test_43_f.py
@@ -0,0 +1,34 @@
+from types import ModuleType
+from typing import List, Tuple
+
+import pytest
+
+from tests.data.test_data_43 import f_test_data
+
+
+@pytest.mark.parametrize(
+    "unsorted_list, expected_output, _",
+    f_test_data,
+    ids=[data[-1] for data in f_test_data],
+)
+def test_merge_sort(
+    setup_environment: Tuple[ModuleType, str],
+    unsorted_list: List[int],
+    expected_output: List[int],
+    _: str,
+) -> None:
+    wrapped_module, _ = setup_environment
+
+    returned_output = wrapped_module.merge_sort(unsorted_list)
+    error_msg = (
+        "Функция `merge_sort` должна быть реализована " "при помощи рекурсии."
+    )
+    try:
+        assert (
+            wrapped_module.merge_sort.call_count >= len(unsorted_list) // 2
+        ), error_msg
+        assert returned_output == expected_output
+
+    finally:
+        # Сбрасываем счетчик вызова функции, независимо от результата теста
+        wrapped_module.merge_sort.call_count = 0
diff --git a/tests/4.3/test_43_g.py b/tests/4.3/test_43_g.py
new file mode 100644
index 00000000..ac906f8b
--- /dev/null
+++ b/tests/4.3/test_43_g.py
@@ -0,0 +1,98 @@
+from io import StringIO
+from types import ModuleType
+from typing import Any, Callable, Tuple
+
+import pytest
+from _pytest.monkeypatch import MonkeyPatch
+
+from tests import utils
+from tests.data.test_data_43 import (test_data_g_a_plus_b,
+                                     test_data_g_combine_text,
+                                     test_data_g_get_sum_of_ords)
+
+
+@pytest.mark.parametrize(
+    "args, expected_output, _",
+    test_data_g_a_plus_b,
+    ids=[data[-1] for data in test_data_g_a_plus_b],
+)
+def test_same_type_a_plus_b(
+    monkeypatch: MonkeyPatch,
+    request: pytest.FixtureRequest,
+    load_module: Callable[[str], ModuleType],
+    args: Tuple[int | float, ...],
+    expected_output: str,
+    _: str,  # Название теста
+) -> None:
+    file_path, _ = utils.get_tested_file_details(request)
+    solution_module = load_module(file_path)
+
+    @solution_module.same_type
+    def a_plus_b(a: int | float, b: int | float) -> int | float:
+        return a + b
+
+    mock_print = StringIO()
+    monkeypatch.setattr("sys.stdout", mock_print)
+
+    print(a_plus_b(*args) or "Fail")
+    printed_output = mock_print.getvalue()
+    assert printed_output == expected_output
+
+
+@pytest.mark.parametrize(
+    "args, expected_output, _",
+    test_data_g_combine_text,
+    ids=[data[-1] for data in test_data_g_combine_text],
+)
+def test_same_type_combine_text(
+    monkeypatch: MonkeyPatch,
+    request: pytest.FixtureRequest,
+    load_module: Callable[[str], ModuleType],
+    args: Tuple[Any],
+    expected_output: str,
+    _: str,  # Название теста
+) -> None:
+    file_path, _ = utils.get_tested_file_details(request)
+    solution_module = load_module(file_path)
+
+    @solution_module.same_type
+    def combine_text(*words) -> str:
+        return " ".join(words)
+
+    mock_print = StringIO()
+    monkeypatch.setattr("sys.stdout", mock_print)
+
+    print(combine_text(*args) or "Fail")
+    printed_output = mock_print.getvalue()
+    assert printed_output == expected_output
+
+
+@pytest.mark.parametrize(
+    "args, expected_output, _",
+    test_data_g_get_sum_of_ords,
+    ids=[data[2] for data in test_data_g_get_sum_of_ords],
+)
+def test_same_type_get_sum_of_ords(
+    monkeypatch: MonkeyPatch,
+    request: pytest.FixtureRequest,
+    load_module: Callable[[str], ModuleType],
+    args: Tuple[Any],
+    expected_output: str,
+    _: str,  # Название теста
+) -> None:
+    file_path, _ = utils.get_tested_file_details(request)
+    solution_module = load_module(file_path)
+
+    @solution_module.same_type
+    def get_sum_of_ords(*text) -> int:
+        result = 0
+        for item in text:
+            result += sum(map(ord, item))
+        return result
+
+    mock_print = StringIO()
+    monkeypatch.setattr("sys.stdout", mock_print)
+
+    print(get_sum_of_ords(*args))
+    printed_output = mock_print.getvalue()
+    assert printed_output == expected_output
diff --git a/tests/4.3/test_43_h.py b/tests/4.3/test_43_h.py
new file mode 100644
index 00000000..c4371a14
--- /dev/null
+++ b/tests/4.3/test_43_h.py
@@ -0,0 +1,34 @@
+from io import StringIO
+from types import ModuleType
+from typing import Tuple
+
+import pytest
+from _pytest.monkeypatch import MonkeyPatch
+
+from tests.data.test_data_43 import h_test_data
+
+
+@pytest.mark.parametrize(
+    "number, separator, expected_output, _",
+    h_test_data,
+    ids=[i[-1] for i in h_test_data],
+)
+def test_fibonacci(
+    monkeypatch: MonkeyPatch,
+    setup_environment: Tuple[ModuleType, str],
+    number: int,
+    separator: str,
+    expected_output: str,
+    _: str,
+) -> None:
+    wrapped_module, _ = setup_environment
+
+    returned_output = wrapped_module.fibonacci(number)
+
+    mock_print = StringIO()
+    monkeypatch.setattr("sys.stdout", mock_print)
+
+    print(*returned_output, sep=separator)
+    printed_output = mock_print.getvalue()
+
+    assert printed_output == expected_output
diff --git a/tests/4.3/test_43_i.py b/tests/4.3/test_43_i.py
new file mode 100644
index 00000000..2fc5f3a4
--- /dev/null
+++ b/tests/4.3/test_43_i.py
@@ -0,0 +1,32 @@
+from io import StringIO
+from types import ModuleType
+from typing import Tuple
+
+import pytest
+from _pytest.monkeypatch import MonkeyPatch
+
+from tests.data.test_data_43 import i_test_data
+
+
+@pytest.mark.parametrize(
+    "iterable, n, expected_output, _",
+    i_test_data,
+    ids=[i[-1] for i in i_test_data],
+)
+def test_cycle(
+    monkeypatch: MonkeyPatch,
+    setup_environment: Tuple[ModuleType, str],
+    iterable: int,
+    n: str,
+    expected_output: str,
+    _: str,
+) -> None:
+    module, _ = setup_environment
+
+    mock_print = StringIO()
+    monkeypatch.setattr("sys.stdout", mock_print)
+
+    print(*(x for _, x in zip(range(n), module.cycle(iterable))))
+    printed_output = mock_print.getvalue()
+
+    assert printed_output == expected_output
diff --git a/tests/4.3/test_43_j.py b/tests/4.3/test_43_j.py
new file mode 100644
index 00000000..07f8d1ad
--- /dev/null
+++ b/tests/4.3/test_43_j.py
@@ -0,0 +1,26 @@
+from types import ModuleType
+from typing import List, Tuple, TypeVar
+
+import pytest
+
+from tests.data.test_data_43 import j_test_data
+
+T = TypeVar("T")
+
+
+@pytest.mark.parametrize(
+    "deep_list, expected_output, _",
+    j_test_data,
+    ids=[i[-1] for i in j_test_data],
+)
+def test_make_linear(
+    setup_environment: Tuple[ModuleType, str],
+    deep_list: List[T],
+    expected_output: List[T],
+    _: str,
+) -> None:
+    wrapped_module, _ = setup_environment
+
+    returned_output = wrapped_module.make_linear(deep_list)
+
+    assert returned_output == expected_output
diff --git a/tests/conftest.py b/tests/conftest.py
index 89402f18..63ec3885 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -20,7 +20,7 @@
 
 
 @pytest.fixture(scope="module")
-def wrap_answer() -> Generator[Callable[[str, str], str], None, None]:
+def wrap_answer() -> Callable[[str, str, str | None], str]:
     def wrapper(file_path: str, file_name: str, args: Union[str, None]) -> str:
         """
         Считывает решение из файла и оборачивает его в main функцию
@@ -157,7 +157,7 @@ def setup_environment(
 @pytest.fixture(scope="module")
 def make_test_files(
     request: SubRequest,
-    setup_environment: pytest.fixture,
+    setup_environment: Generator[Tuple[ModuleType, str], None, None],
 ) -> Generator[
     Callable[[Union[Tuple[str], str], Union[Tuple[str], str]], ModuleType],
     None,
@@ -173,8 +173,8 @@ def make_test_files(
 
     Аргументы:
         request (SubRequest): объект, дающий информацию о тестовом запросе.
-        setup_environment (pytest.fixture): фикстура для настройки тестовой
-            среды.
+        setup_environment (Generator[Tuple[ModuleType, str], None, None]):
+            фикстура для настройки тестовой среды.
 
     Возвращает:
         Generator[
@@ -189,8 +189,8 @@ def make_test_files(
     """
 
     def _create_test_file(
-        file_names: Union[Tuple[str], str],
-        mock_input_texts: Union[Tuple[str], str],
+        file_names: Tuple[str] | str,
+        mock_input_texts: Tuple[str] | str,
         mode: str = "w",
     ) -> ModuleType:
         """
diff --git a/tests/constants.py b/tests/constants.py
index 326277dc..1b7f8ed9 100644
--- a/tests/constants.py
+++ b/tests/constants.py
@@ -37,5 +37,6 @@
         "a": "recursive_sum",
         "b": "recursive_digit_sum",
         "c": "make_equation",
+        "j": "make_linear",
     },
 }
diff --git a/tests/data/test_data_43.py b/tests/data/test_data_43.py
index faf93fd6..c906230d 100644
--- a/tests/data/test_data_43.py
+++ b/tests/data/test_data_43.py
@@ -129,3 +129,110 @@
     ((" ",), {}, None, "whitespace"),
     (("Very interesting!",), {"method": "drop"}, [32, 1683], "mixed"),
 ]
+
+f_test_data = [
+    ([3, 2, 1], [1, 2, 3], "first open test"),
+    ([3, 1, 5, 3], [1, 3, 3, 5], "second open test"),
+    ([100, 200, 400, 600, 101], [100, 101, 200, 400, 600], "odd length"),
+    ([13], [13], "one number"),
+    ([6, 7, 8, 9, 10], [6, 7, 8, 9, 10], "already sorted"),
+    ([10, 9, 8, 7, 6], [6, 7, 8, 9, 10], "reverse sorted"),
+]
+
+test_data_g_a_plus_b = [
+    (
+        (3, 5.2),
+        "Обнаружены различные типы данных\nFail\n",
+        "first open test 1",
+    ),
+    (
+        (7, "9"),
+        "Обнаружены различные типы данных\nFail\n",
+        "first open test 2",
+    ),
+    ((-3, 5), "2\n", "first open test 3"),
+]
+
+test_data_g_combine_text = [
+    (("Hello,", "world!"), "Hello, world!\n", "first open test 1"),
+    (
+        (2, "+", 2, "=", 4),
+        "Обнаружены различные типы данных\nFail\n",
+        "first open test 2",
+    ),
+    (
+        ("Список из 30", 0, "можно получить так", [0] * 30),
+        "Обнаружены различные типы данных\nFail\n",
+        "first open test 3",
+    ),
+]
+
+test_data_g_get_sum_of_ords = [
+    (("Hello, world!",), "1161\n", "hello world"),
+    (
+        ("Декораторы это круто =)", True),
+        "Обнаружены различные типы данных\nFail\n",
+        "different types",
+    ),
+    ((" ", 123.1), "Обнаружены различные типы данных\nFail\n", "whitespace"),
+    (
+        ("Very interesting!", 12 + 13j),
+        "Обнаружены различные типы данных\nFail\n",
+        "complex",
+    ),
+]
+
+h_test_data = [
+    (5, " ", "0 1 1 2 3\n", "first open test"),
+    (10, ", ", "0, 1, 1, 2, 3, 5, 8, 13, 21, 34\n", "second open test"),
+    (6, "", "011235\n", "no separation"),
+    (1, " ", "0\n", "one"),
+    (2, "\t", "0\t1\n", "two"),
+    (3, "\n", "0\n1\n1\n", "three"),
+    (0, "\n", "\n", "zero -> None"),
+]
+
+i_test_data = [
+    ([1, 2, 3], 5, "1 2 3 1 2\n", "first open test"),
+    ([1, 2, 3, 4], 15, "1 2 3 4 1 2 3 4 1 2 3 4 1 2 3\n", "second open test"),
+    ("iterable", 8, "i t e r a b l e\n", "string"),
+    (
+        (
+            "Who",
+            "loves",
+            23,
+            4 + 7j,
+        ),
+        7,
+        "Who loves 23 (4+7j) Who loves 23\n",
+        "mixed types",
+    ),
+    (
+        (
+            "Who",
+            "loves",
+            23,
+            4 + 7j,
+        ),
+        1,
+        "Who\n",
+        "short mixed types",
+    ),
+    (range(5), 5, "0 1 2 3 4\n", "range"),
+    (range(-5, 0, 1), 5, "-5 -4 -3 -2 -1\n", "negative range"),
+    (range(-5, 0, 1), 3, "-5 -4 -3\n", "negative range short"),
+    (range(-3, 0, 1), 6, "-3 -2 -1 -3 -2 -1\n", "long negative range"),
+]
+
+j_test_data = [
+    ([1, 2, [3]], [1, 2, 3], "first open test"),
+    ([1, [2, [3, 4]], 5, 6], [1, 2, 3, 4, 5, 6], "second open test"),
+    ([[[[[[[1]]]]]]], [1], "deep"),
+    (
+        [[3, 2], [[[[5], 6]], 12], 31, [[[21], 3]]],
+        [3, 2, 5, 6, 12, 31, 21, 3],
+        "everything and between",
+    ),
+    ([1], [1], "one element"),
+    ([[[[[[[[], 2], 1]]]]]], [2, 1], "with empty list"),
+]
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 24e2b1ce..f407df4d 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -116,7 +116,7 @@ def test_generate_error_msg(
 )
 def test_compare_output_passes(
     printed: str,
-    expected: Union[str, Set[str], Tuple[str], List[str]],
+    expected: Union[str, Set[str], Tuple[str], List[str], str],
     _: str,
 ) -> None:
     result = utils.compare_output(printed, expected)
diff --git a/tests/utils.py b/tests/utils.py
index b4b8979d..a1861a5e 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -104,15 +104,16 @@ def generate_error_msg(
 
 def compare_output(
     printed_output: str,
-    expected_output: Union[Set[str], Tuple[str], List[str]],
+    expected_output: Union[Set[str], Tuple[str], List[str], str],
 ) -> None:
     """
     Сравнивает напечатанный вывод с ожидаемым выводом.
 
     Аргументы:
         printed_output (str): Ответ выданные модулем.
-        expected_output (Union[Set[str], Tuple[str], List[str]],): Ожидаемые
-            данные вывода, могут быть: строкой, списком, set или кортежем.
+        expected_output (Union[Set[str], Tuple[str], List[str]], str):
+            Ожидаемые данные вывода, могут быть: строкой, списком, set
+            или кортежем.
     Исключения:
         AssertionError: Если фактический вывод не совпадает с ожидаемым.
     """