From 661e983ab670c140a8cac1767b4a44e76aa626c9 Mon Sep 17 00:00:00 2001 From: Dmitriy Bazanov Date: Mon, 16 Oct 2023 19:03:12 +0300 Subject: [PATCH 1/8] Init hw3 files --- src/controls/device.py | 82 +++++++++++++++++++++++ src/controls/smth.py | 4 -- src/equipment/__init__.py | 0 src/equipment/turtle_device.py | 8 +++ src/other_module/__init__.py | 3 - src/other_module/snth2.py | 4 -- src/tests/controls/test_device.py | 12 ++++ src/tests/equipment/test_turtle_device.py | 13 ++++ 8 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 src/controls/device.py delete mode 100644 src/controls/smth.py create mode 100644 src/equipment/__init__.py create mode 100644 src/equipment/turtle_device.py delete mode 100644 src/other_module/__init__.py delete mode 100644 src/other_module/snth2.py create mode 100644 src/tests/controls/test_device.py create mode 100644 src/tests/equipment/test_turtle_device.py diff --git a/src/controls/device.py b/src/controls/device.py new file mode 100644 index 0000000..d126692 --- /dev/null +++ b/src/controls/device.py @@ -0,0 +1,82 @@ +from dataclasses import dataclass +from typing import Optional, Collection, Any +from abc import abstractmethod + +class DeviceLifecycleState: + pass # TODO(Homework #3) + + +class DevaceError(Exception): + pass + + +class NonReadableTrait(Exception): + pass + + +class NonWritableTrait(Exception): + pass + + +@dataclass +class TraitDescriptor: + name: str + info: Optional[str] = None + readable: bool = True + writable: bool = False + + +@dataclass +class ActionDescriptor: + name: str + arguments: dict[str, type] + info: Optional[str] = None + + +class Device: + # TODO(Homework #3) + _state = DeviceLifecycleState.INIT + + @property + def state(self) -> DeviceLifecycleState: + return self._state + + def close(self): + self._state = DeviceLifecycleState.CLOSE + + def trait_descriptors(self) -> Collection[TraitDescriptor]: + pass + + def action_descriptors(self) -> Collection[ActionDescriptor]: + pass + + @abstractmethod + def __getitem__(self, trait_name: str) -> Optional[Any]: + """Return logical state of trait `trait_name`.""" + pass + + +class SynchronyDevice(Device): + + def open(self): + self._state = DeviceLifecycleState.OPEN + + @abstractmethod + def execute(self, action_name: str, *args, **kwargs): + """Execute action `action_name`, using `args` and `kwargs` as action argument.""" + pass + + @abstractmethod + def read(self, trait_name: str) -> Any: + """Read physical state of trait `trait_name` from device.""" + raise NonReadableTrait + + @abstractmethod + def write(self, trait_name: str, value: Any) -> bool: + """Pass `value` to trait `trait_name` of device.""" + raise NonWritableTrait + + @abstractmethod + def invalidate(self, trait_name: str): + """Invalidate logical state of trait `trait_name`""" + pass \ No newline at end of file diff --git a/src/controls/smth.py b/src/controls/smth.py deleted file mode 100644 index 18d956c..0000000 --- a/src/controls/smth.py +++ /dev/null @@ -1,4 +0,0 @@ - -def multiply(a:int, b:int) -> int: - """Multiply two ints""" - return a*b \ No newline at end of file diff --git a/src/equipment/__init__.py b/src/equipment/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/equipment/turtle_device.py b/src/equipment/turtle_device.py new file mode 100644 index 0000000..3ba251f --- /dev/null +++ b/src/equipment/turtle_device.py @@ -0,0 +1,8 @@ +from turtle import Turtle +from controls.device import SynchronyDevice + +class TurtleDevice(SynchronyDevice): + pass # TODO(Homework #3) + + + diff --git a/src/other_module/__init__.py b/src/other_module/__init__.py deleted file mode 100644 index 874b243..0000000 --- a/src/other_module/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Just other sample module -""" \ No newline at end of file diff --git a/src/other_module/snth2.py b/src/other_module/snth2.py deleted file mode 100644 index b48e8e0..0000000 --- a/src/other_module/snth2.py +++ /dev/null @@ -1,4 +0,0 @@ - -def add(a:int, b:int) -> int: - """Add two ints""" - return a + b \ No newline at end of file diff --git a/src/tests/controls/test_device.py b/src/tests/controls/test_device.py new file mode 100644 index 0000000..51249ad --- /dev/null +++ b/src/tests/controls/test_device.py @@ -0,0 +1,12 @@ +from unittest import TestCase + +from controls.device import DeviceLifecycleState + + +class DeviceLifecycleStateTest(TestCase): + + def setUp(self) -> None: + pass + + def test_enum(self): + self.assertEqual(DeviceLifecycleStateTest["INIT"], DeviceLifecycleStateTest.INIT) diff --git a/src/tests/equipment/test_turtle_device.py b/src/tests/equipment/test_turtle_device.py new file mode 100644 index 0000000..a12912b --- /dev/null +++ b/src/tests/equipment/test_turtle_device.py @@ -0,0 +1,13 @@ +from unittest import TestCase + +from equipment.turtle_device import TurtleDevice + + +class TurtleDeviceTest(TestCase): + + def setUp(self) -> None: + self.device = TurtleDevice() + + def test_open(self): + self.device.open() + self.device.close() \ No newline at end of file From 4363ac2dfd0a5a0159457079a44f726c6237ea1e Mon Sep 17 00:00:00 2001 From: Dmitriy Bazanov Date: Mon, 16 Oct 2023 22:40:59 +0300 Subject: [PATCH 2/8] Make first tasks and some final methods --- src/controls/device.py | 28 ++++++++--- src/equipment/turtle_device.py | 81 ++++++++++++++++++++++++++++++- src/tests/controls/test_device.py | 4 +- 3 files changed, 103 insertions(+), 10 deletions(-) diff --git a/src/controls/device.py b/src/controls/device.py index d126692..fe85384 100644 --- a/src/controls/device.py +++ b/src/controls/device.py @@ -1,12 +1,16 @@ from dataclasses import dataclass from typing import Optional, Collection, Any -from abc import abstractmethod - -class DeviceLifecycleState: - pass # TODO(Homework #3) +from abc import abstractmethod, ABC +import enum -class DevaceError(Exception): +class DeviceLifecycleState(enum.Enum): + INIT = "INIT" + OPEN = "OPEN" + CLOSE = "CLOSE" + + +class DeviceError(Exception): pass @@ -18,6 +22,12 @@ class NonWritableTrait(Exception): pass +class NoSuchTrait(Exception): + pass + +class NoSuchAction(Exception): + pass + @dataclass class TraitDescriptor: name: str @@ -33,8 +43,7 @@ class ActionDescriptor: info: Optional[str] = None -class Device: - # TODO(Homework #3) +class Device(ABC): _state = DeviceLifecycleState.INIT @property @@ -44,9 +53,14 @@ class Device: def close(self): self._state = DeviceLifecycleState.CLOSE + + @property + @abstractmethod def trait_descriptors(self) -> Collection[TraitDescriptor]: pass + @property + @abstractmethod def action_descriptors(self) -> Collection[ActionDescriptor]: pass diff --git a/src/equipment/turtle_device.py b/src/equipment/turtle_device.py index 3ba251f..f433dd8 100644 --- a/src/equipment/turtle_device.py +++ b/src/equipment/turtle_device.py @@ -1,8 +1,85 @@ from turtle import Turtle -from controls.device import SynchronyDevice +from controls.device import ( + SynchronyDevice, + TraitDescriptor, + ActionDescriptor, + NoSuchTrait, + NoSuchAction, +) +from typing import Collection, Optional, Any +import inspect + class TurtleDevice(SynchronyDevice): - pass # TODO(Homework #3) + def __init__(self) -> None: + super().__init__() + self.open() + def open(self): + self.turtle = Turtle() + def close(self): + del self.turtle + def execute(self, action_name: str, *args, **kwargs): + pass + + def read(self, trait_name: str) -> Any: + traits = self.trait_descriptors + for trait in traits: + if trait.name == trait_name: + return self.turtle.__getattribute__(trait_name) + raise NoSuchTrait + + def write(self, trait_name: str, value: Any) -> bool: + traits = self.trait_descriptors + for trait in traits: + if trait.name == trait_name: + attribute_type = type(self.turtle.__getattribute__(trait_name)) + assert ( + type(value) == attribute_type + ), f"Wrong value type. Type should be {attribute_type}, but you pass {type(value)}" + self.turtle.__setattr__(trait_name, value) + return True + raise NoSuchTrait + + def invalidate(self, trait_name: str): + pass + + def __getitem__(self, trait_name: str) -> Optional[Any]: + pass + + @property + def trait_descriptors(self) -> Collection[TraitDescriptor]: + return [ + TraitDescriptor(name=attr, info=getattr(self.turtle, attr).__doc__) + for attr in dir(self.turtle) + if attr[0] != "_" + and attr[-1] != "_" + and not callable(getattr(self.turtle, attr)) + ] + + @property + def action_descriptors(self) -> Collection[ActionDescriptor]: + methods = [ + attr + for attr in dir(self.turtle) + if attr[0] != "_" + and attr[-1] != "_" + and callable(getattr(self.turtle, attr)) + ] + + result = [] + for method_name in methods: + method = getattr(self.turtle, method_name) + args = inspect.signature(method).parameters + arg_types = {} + for arg_name, arg in args.items(): + arg_types[arg_name] = arg.annotation + result.append( + ActionDescriptor( + name=method_name, info=method.__doc__, arguments=arg_types + ) + ) + + return result diff --git a/src/tests/controls/test_device.py b/src/tests/controls/test_device.py index 51249ad..7ae2435 100644 --- a/src/tests/controls/test_device.py +++ b/src/tests/controls/test_device.py @@ -9,4 +9,6 @@ class DeviceLifecycleStateTest(TestCase): pass def test_enum(self): - self.assertEqual(DeviceLifecycleStateTest["INIT"], DeviceLifecycleStateTest.INIT) + # NOTE: это имеется в виду? + # self.assertEqual(DeviceLifecycleStateTest["INIT"], DeviceLifecycleStateTest.INIT) + self.assertEqual(DeviceLifecycleState["INIT"], DeviceLifecycleState.INIT) From e9a0b2832e708c938eec8391efa113c4a3b53d40 Mon Sep 17 00:00:00 2001 From: Dmitriy Bazanov Date: Thu, 26 Oct 2023 22:57:53 +0300 Subject: [PATCH 3/8] Add all methods realized --- src/equipment/turtle_device.py | 40 ++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/src/equipment/turtle_device.py b/src/equipment/turtle_device.py index f433dd8..6dc3001 100644 --- a/src/equipment/turtle_device.py +++ b/src/equipment/turtle_device.py @@ -15,21 +15,31 @@ class TurtleDevice(SynchronyDevice): super().__init__() self.open() - def open(self): + def open(self) -> None: self.turtle = Turtle() - def close(self): + def close(self)-> None: del self.turtle + self.close() - def execute(self, action_name: str, *args, **kwargs): - pass + def execute(self, action_name: str, *args, **kwargs) -> None: + actions = self.action_descriptors + for action in actions: + if action.name == action_name: + # NOTE: судя по заданию, должен быть такой чекер, но почему-то у меня inspect плохо спарсил типы + # везде empty_time, хотя судя по доке в модуле typos проставлены + # for argument in kwargs: + # action_tmp_argument_type = action.arguments[argument] + # given_method_tmp_argument = type(kwargs[argument]) + # assert ( + # action_tmp_argument_type == given_method_tmp_argument + # ), f"Переданный аргумент должен быть типа {action_tmp_argument_type}, вы передали {given_method_tmp_argument}" + getattr(self.turtle, action_name)(*args, **kwargs) + return + raise NoSuchAction def read(self, trait_name: str) -> Any: - traits = self.trait_descriptors - for trait in traits: - if trait.name == trait_name: - return self.turtle.__getattribute__(trait_name) - raise NoSuchTrait + return Turtle().__getattribute__(trait_name) def write(self, trait_name: str, value: Any) -> bool: traits = self.trait_descriptors @@ -43,11 +53,17 @@ class TurtleDevice(SynchronyDevice): return True raise NoSuchTrait - def invalidate(self, trait_name: str): - pass + def invalidate(self, trait_name: str) -> None: + # NOTE: если я правильно понял, что должен делать метод + self.write(trait_name=trait_name, value=Turtle().__getattribute__(trait_name)) + return def __getitem__(self, trait_name: str) -> Optional[Any]: - pass + traits = self.trait_descriptors + for trait in traits: + if trait.name == trait_name: + return self.turtle.__getattribute__(trait_name) + raise NoSuchTrait @property def trait_descriptors(self) -> Collection[TraitDescriptor]: From 0ce2aa5941fd83850cb1bfaf245880ee1173e8cf Mon Sep 17 00:00:00 2001 From: Dmitriy Bazanov Date: Thu, 26 Oct 2023 23:32:17 +0300 Subject: [PATCH 4/8] Upd --- src/equipment/turtle_device.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/equipment/turtle_device.py b/src/equipment/turtle_device.py index 6dc3001..787b3ad 100644 --- a/src/equipment/turtle_device.py +++ b/src/equipment/turtle_device.py @@ -54,7 +54,6 @@ class TurtleDevice(SynchronyDevice): raise NoSuchTrait def invalidate(self, trait_name: str) -> None: - # NOTE: если я правильно понял, что должен делать метод self.write(trait_name=trait_name, value=Turtle().__getattribute__(trait_name)) return @@ -88,10 +87,13 @@ class TurtleDevice(SynchronyDevice): result = [] for method_name in methods: method = getattr(self.turtle, method_name) - args = inspect.signature(method).parameters + parameters = inspect.signature(method).parameters + arg_types = {} - for arg_name, arg in args.items(): - arg_types[arg_name] = arg.annotation + for arg_name, arg in parameters.items(): + arg_types[arg_name] = parameters[arg_name].annotation + + result.append( ActionDescriptor( name=method_name, info=method.__doc__, arguments=arg_types From 7f1db9a6270e7cb31d03170d5b8c861591901d41 Mon Sep 17 00:00:00 2001 From: Dmitriy Bazanov Date: Thu, 26 Oct 2023 23:55:48 +0300 Subject: [PATCH 5/8] black formatting --- src/equipment/turtle_device.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/equipment/turtle_device.py b/src/equipment/turtle_device.py index 787b3ad..d1b4e14 100644 --- a/src/equipment/turtle_device.py +++ b/src/equipment/turtle_device.py @@ -18,7 +18,7 @@ class TurtleDevice(SynchronyDevice): def open(self) -> None: self.turtle = Turtle() - def close(self)-> None: + def close(self) -> None: del self.turtle self.close() @@ -26,8 +26,10 @@ class TurtleDevice(SynchronyDevice): actions = self.action_descriptors for action in actions: if action.name == action_name: - # NOTE: судя по заданию, должен быть такой чекер, но почему-то у меня inspect плохо спарсил типы + # NOTE: судя по заданию, должен быть такой assert, + # но почему-то у меня inspect плохо спарсил типы именно для Turtle # везде empty_time, хотя судя по доке в модуле typos проставлены + # при чем тестировал на pd.DataFrame, там все прекрасно парсит # for argument in kwargs: # action_tmp_argument_type = action.arguments[argument] # given_method_tmp_argument = type(kwargs[argument]) @@ -93,11 +95,9 @@ class TurtleDevice(SynchronyDevice): for arg_name, arg in parameters.items(): arg_types[arg_name] = parameters[arg_name].annotation - result.append( ActionDescriptor( name=method_name, info=method.__doc__, arguments=arg_types ) ) - return result From 353505239e133b2531bcca7d943ff8df3074ef44 Mon Sep 17 00:00:00 2001 From: Dmitriy Bazanov Date: Fri, 27 Oct 2023 00:17:40 +0300 Subject: [PATCH 6/8] Add tests on pytest and change dir structure --- src/tests/controls/test_device.py | 14 ---------- src/tests/equipment/test_turtle_device.py | 13 ---------- tests/controls/test_device.py | 7 +++++ tests/equipment/test_turtle_device.py | 31 +++++++++++++++++++++++ 4 files changed, 38 insertions(+), 27 deletions(-) delete mode 100644 src/tests/controls/test_device.py delete mode 100644 src/tests/equipment/test_turtle_device.py create mode 100644 tests/controls/test_device.py create mode 100644 tests/equipment/test_turtle_device.py diff --git a/src/tests/controls/test_device.py b/src/tests/controls/test_device.py deleted file mode 100644 index 7ae2435..0000000 --- a/src/tests/controls/test_device.py +++ /dev/null @@ -1,14 +0,0 @@ -from unittest import TestCase - -from controls.device import DeviceLifecycleState - - -class DeviceLifecycleStateTest(TestCase): - - def setUp(self) -> None: - pass - - def test_enum(self): - # NOTE: это имеется в виду? - # self.assertEqual(DeviceLifecycleStateTest["INIT"], DeviceLifecycleStateTest.INIT) - self.assertEqual(DeviceLifecycleState["INIT"], DeviceLifecycleState.INIT) diff --git a/src/tests/equipment/test_turtle_device.py b/src/tests/equipment/test_turtle_device.py deleted file mode 100644 index a12912b..0000000 --- a/src/tests/equipment/test_turtle_device.py +++ /dev/null @@ -1,13 +0,0 @@ -from unittest import TestCase - -from equipment.turtle_device import TurtleDevice - - -class TurtleDeviceTest(TestCase): - - def setUp(self) -> None: - self.device = TurtleDevice() - - def test_open(self): - self.device.open() - self.device.close() \ No newline at end of file diff --git a/tests/controls/test_device.py b/tests/controls/test_device.py new file mode 100644 index 0000000..3e8bf7d --- /dev/null +++ b/tests/controls/test_device.py @@ -0,0 +1,7 @@ +import pytest + +from controls.device import DeviceLifecycleState + + +def test_enum(): + assert DeviceLifecycleState["INIT"] == DeviceLifecycleState.INIT diff --git a/tests/equipment/test_turtle_device.py b/tests/equipment/test_turtle_device.py new file mode 100644 index 0000000..043f948 --- /dev/null +++ b/tests/equipment/test_turtle_device.py @@ -0,0 +1,31 @@ +import pytest +import time + +from equipment.turtle_device import TurtleDevice + +def test_setup(): + td = TurtleDevice() # не падает + +def test_read(): + td = TurtleDevice() + assert td.read("DEFAULT_ANGLEOFFSET") == 0 + +def test_write(): + td = TurtleDevice() + value_to_pass = 10 + td.write("DEFAULT_ANGLEOFFSET", value_to_pass) + assert td["DEFAULT_ANGLEOFFSET"] == value_to_pass + +def test_invalidate(): + td = TurtleDevice() + value_to_pass = 10 + td.write("DEFAULT_ANGLEOFFSET", value_to_pass) + td.invalidate("DEFAULT_ANGLEOFFSET") # сносит логическое значение + assert td["DEFAULT_ANGLEOFFSET"] == 0 + +def test_execute(): + td = TurtleDevice() + td.execute("back", **{"distance": 5.0}) + assert td.turtle.pos() == (-5.0 ,0.0) + + \ No newline at end of file From 3cd5169c3c5d641e121e317c3a284a82500e874b Mon Sep 17 00:00:00 2001 From: Dmitriy Bazanov Date: Fri, 27 Oct 2023 11:00:16 +0300 Subject: [PATCH 7/8] Upd readme with pytest --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 03e6ced..4f3ebbe 100644 --- a/README.md +++ b/README.md @@ -15,4 +15,12 @@ $ cd src/docs $ make html ``` +# How to run Pytest tests + +``` +$ export PYTHONPATH=src +$ pytest +``` + + From 559288a790b975faf49f0d4bdd58ba730392503c Mon Sep 17 00:00:00 2001 From: Dmitriy Bazanov Date: Fri, 27 Oct 2023 11:31:58 +0300 Subject: [PATCH 8/8] Update tests with many params --- tests/equipment/test_turtle_device.py | 63 +++++++++++++++++++-------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/tests/equipment/test_turtle_device.py b/tests/equipment/test_turtle_device.py index 043f948..f63d336 100644 --- a/tests/equipment/test_turtle_device.py +++ b/tests/equipment/test_turtle_device.py @@ -1,31 +1,58 @@ import pytest -import time from equipment.turtle_device import TurtleDevice + def test_setup(): - td = TurtleDevice() # не падает + td = TurtleDevice() # не падает -def test_read(): + +@pytest.mark.parametrize( + ("trait", "value"), [("DEFAULT_MODE", "standard"), ("DEFAULT_ANGLEOFFSET", 0.0)] +) +def test_read(trait, value): td = TurtleDevice() - assert td.read("DEFAULT_ANGLEOFFSET") == 0 + assert td.read(trait) == value -def test_write(): + +@pytest.mark.parametrize( + ("trait", "value_to_pass"), + [ + ("DEFAULT_ANGLEOFFSET", 1), + ("DEFAULT_ANGLEOFFSET", 2), + pytest.param("DEFAULT_ANGLEOFFSET", 3.0, marks=pytest.mark.xfail), + ("DEFAULT_MODE", "other_mode"), + ], +) +def test_write(trait, value_to_pass): td = TurtleDevice() - value_to_pass = 10 - td.write("DEFAULT_ANGLEOFFSET", value_to_pass) - assert td["DEFAULT_ANGLEOFFSET"] == value_to_pass + td.write(trait, value_to_pass) + assert td[trait] == value_to_pass -def test_invalidate(): + +@pytest.mark.parametrize( + ("trait", "value_to_pass"), + [ + ("DEFAULT_ANGLEOFFSET", 10), + ("DEFAULT_MODE", "other mode"), + ], +) +def test_invalidate(trait, value_to_pass): td = TurtleDevice() - value_to_pass = 10 - td.write("DEFAULT_ANGLEOFFSET", value_to_pass) - td.invalidate("DEFAULT_ANGLEOFFSET") # сносит логическое значение - assert td["DEFAULT_ANGLEOFFSET"] == 0 + td.write(trait, value_to_pass) + assert td[trait] != td.read(trait) + td.invalidate(trait) # сносит логическое значение + assert td[trait] == td.read(trait) -def test_execute(): + +@pytest.mark.parametrize( + ("action", "args", "expected"), + [ + ("back", {"distance": 5.0}, (-5.0, 0.0)), + pytest.param("back", {"distance": 5.0}, (0.0, -5.0), marks=pytest.mark.xfail), + ], +) +def test_execute(action, args, expected): td = TurtleDevice() - td.execute("back", **{"distance": 5.0}) - assert td.turtle.pos() == (-5.0 ,0.0) - - \ No newline at end of file + td.execute(action, **args) + assert td.turtle.pos() == expected