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 +``` + + diff --git a/src/controls/device.py b/src/controls/device.py new file mode 100644 index 0000000..fe85384 --- /dev/null +++ b/src/controls/device.py @@ -0,0 +1,96 @@ +from dataclasses import dataclass +from typing import Optional, Collection, Any +from abc import abstractmethod, ABC +import enum + + +class DeviceLifecycleState(enum.Enum): + INIT = "INIT" + OPEN = "OPEN" + CLOSE = "CLOSE" + + +class DeviceError(Exception): + pass + + +class NonReadableTrait(Exception): + pass + + +class NonWritableTrait(Exception): + pass + + +class NoSuchTrait(Exception): + pass + +class NoSuchAction(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(ABC): + _state = DeviceLifecycleState.INIT + + @property + def state(self) -> DeviceLifecycleState: + return self._state + + 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 + + @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..d1b4e14 --- /dev/null +++ b/src/equipment/turtle_device.py @@ -0,0 +1,103 @@ +from turtle import Turtle +from controls.device import ( + SynchronyDevice, + TraitDescriptor, + ActionDescriptor, + NoSuchTrait, + NoSuchAction, +) +from typing import Collection, Optional, Any +import inspect + + +class TurtleDevice(SynchronyDevice): + def __init__(self) -> None: + super().__init__() + self.open() + + def open(self) -> None: + self.turtle = Turtle() + + def close(self) -> None: + del self.turtle + self.close() + + def execute(self, action_name: str, *args, **kwargs) -> None: + actions = self.action_descriptors + for action in actions: + if action.name == action_name: + # 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]) + # 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: + return Turtle().__getattribute__(trait_name) + + 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) -> None: + self.write(trait_name=trait_name, value=Turtle().__getattribute__(trait_name)) + return + + def __getitem__(self, trait_name: str) -> Optional[Any]: + 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]: + 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) + parameters = inspect.signature(method).parameters + + arg_types = {} + 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 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/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..f63d336 --- /dev/null +++ b/tests/equipment/test_turtle_device.py @@ -0,0 +1,58 @@ +import pytest + +from equipment.turtle_device import TurtleDevice + + +def test_setup(): + td = TurtleDevice() # не падает + + +@pytest.mark.parametrize( + ("trait", "value"), [("DEFAULT_MODE", "standard"), ("DEFAULT_ANGLEOFFSET", 0.0)] +) +def test_read(trait, value): + td = TurtleDevice() + assert td.read(trait) == value + + +@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() + td.write(trait, value_to_pass) + assert td[trait] == value_to_pass + + +@pytest.mark.parametrize( + ("trait", "value_to_pass"), + [ + ("DEFAULT_ANGLEOFFSET", 10), + ("DEFAULT_MODE", "other mode"), + ], +) +def test_invalidate(trait, value_to_pass): + td = TurtleDevice() + td.write(trait, value_to_pass) + assert td[trait] != td.read(trait) + td.invalidate(trait) # сносит логическое значение + assert td[trait] == td.read(trait) + + +@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(action, **args) + assert td.turtle.pos() == expected