From e4d360c990caea6a5bf9a2b05cb2e0842cc5409c Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Fri, 22 Apr 2022 04:58:05 -0500 Subject: [PATCH 01/27] Add coroutine command support --- commands2/__init__.py | 11 +++- commands2/button/__init__.py | 6 ++- commands2/button/button.py | 8 +++ commands2/button/joystickbutton.py | 7 +++ commands2/button/networkbutton.py | 28 +++++++++++ commands2/button/povbutton.py | 7 +++ commands2/coroutinecommand.py | 81 ++++++++++++++++++++++++++++++ commands2/src/Command.cpp.inl | 7 +++ commands2/src/helpers.cpp | 18 ++++++- commands2/src/helpers.h | 15 +++++- commands2/trigger.py | 76 ++++++++++++++++++++++++++++ pyproject.toml | 8 +-- 12 files changed, 262 insertions(+), 10 deletions(-) create mode 100644 commands2/button/button.py create mode 100644 commands2/button/joystickbutton.py create mode 100644 commands2/button/networkbutton.py create mode 100644 commands2/button/povbutton.py create mode 100644 commands2/coroutinecommand.py create mode 100644 commands2/trigger.py diff --git a/commands2/__init__.py b/commands2/__init__.py index 18c5630d..07fe0de2 100644 --- a/commands2/__init__.py +++ b/commands2/__init__.py @@ -1,4 +1,6 @@ from . import _init_impl +from .trigger import Trigger +from .coroutinecommand import CoroutineCommand, commandify from .version import version as __version__ @@ -40,7 +42,7 @@ TrapezoidProfileCommandRadians, TrapezoidProfileSubsystem, TrapezoidProfileSubsystemRadians, - Trigger, + # Trigger, WaitCommand, WaitUntilCommand, # button, @@ -84,9 +86,14 @@ "TrapezoidProfileCommandRadians", "TrapezoidProfileSubsystem", "TrapezoidProfileSubsystemRadians", - "Trigger", + # "Trigger", "WaitCommand", "WaitUntilCommand", # "button", "requirementsDisjoint", + + # py side + "commandify", + "CoroutineCommand", + "Trigger", ] diff --git a/commands2/button/__init__.py b/commands2/button/__init__.py index 69a9f835..b0817cdd 100644 --- a/commands2/button/__init__.py +++ b/commands2/button/__init__.py @@ -1,4 +1,6 @@ -# autogenerated by 'robotpy-build create-imports commands2.button commands2._impl.button' -from .._impl.button import Button, JoystickButton, NetworkButton, POVButton +from .button import Button +from .joystickbutton import JoystickButton +from .networkbutton import NetworkButton +from .povbutton import POVButton __all__ = ["Button", "JoystickButton", "NetworkButton", "POVButton"] diff --git a/commands2/button/button.py b/commands2/button/button.py new file mode 100644 index 00000000..a0b85553 --- /dev/null +++ b/commands2/button/button.py @@ -0,0 +1,8 @@ +from ..trigger import Trigger + +class Button(Trigger): + whenPressed = Trigger.whenActive + whenReleased = Trigger.whenInactive + + + diff --git a/commands2/button/joystickbutton.py b/commands2/button/joystickbutton.py new file mode 100644 index 00000000..c47ec2f6 --- /dev/null +++ b/commands2/button/joystickbutton.py @@ -0,0 +1,7 @@ +from wpilib import Joystick + +from .button import Button + +class JoystickButton(Button): + def __init__(self, joystick: Joystick, button: int) -> None: + super().__init__(lambda: joystick.getRawButton(button)) diff --git a/commands2/button/networkbutton.py b/commands2/button/networkbutton.py new file mode 100644 index 00000000..87ee66ff --- /dev/null +++ b/commands2/button/networkbutton.py @@ -0,0 +1,28 @@ +from networktables import NetworkTable, NetworkTables, NetworkTableEntry + +from typing import Union, overload + +from .button import Button + +class NetworkButton(Button): + + @overload + def __init__(self, entry: NetworkTableEntry) -> None: ... + + @overload + def __init__(self, table: Union[NetworkTable, str], field: str) -> None: ... + + def __init__(self, *args, **kwargs) -> None: + num_args = (len(args) + len(kwargs)) + if num_args == 1: + entry: NetworkTableEntry = kwargs.get('entry', args[0]) + super().__init__(lambda: NetworkTables.isConnected and entry.getBoolean(False)) + else: + table = kwargs.get('table', args[0]) + field = kwargs.get('field', args[-1]) + + if isinstance(table, str): + table = NetworkTables.getTable(table) + + entry = table.getEntry(field) + self.__init__(entry) diff --git a/commands2/button/povbutton.py b/commands2/button/povbutton.py new file mode 100644 index 00000000..a35a866a --- /dev/null +++ b/commands2/button/povbutton.py @@ -0,0 +1,7 @@ +from wpilib import Joystick + +from .button import Button + +class POVButton(Button): + def __init__(self, joystick: Joystick, angle: int, povNumber: int = 0) -> None: + super().__init__(lambda: joystick.getPOV(povNumber) == angle) \ No newline at end of file diff --git a/commands2/coroutinecommand.py b/commands2/coroutinecommand.py new file mode 100644 index 00000000..474f0d20 --- /dev/null +++ b/commands2/coroutinecommand.py @@ -0,0 +1,81 @@ +from functools import wraps +from typing import Any, Callable, Generator, List, Union, Optional +from ._impl import CommandBase, Subsystem +import inspect +from typing_extensions import TypeGuard + +Coroutine = Generator[None, None, None] +CoroutineFunction = Callable[[], Generator[None, None, None]] +Coroutineable = Union[Callable[[], None], CoroutineFunction] + +def is_coroutine(func: Any) -> TypeGuard[Coroutine]: + return inspect.isgenerator(func) + +def is_coroutine_function(func: Any) -> TypeGuard[CoroutineFunction]: + return inspect.isgeneratorfunction(func) + +def is_coroutineable(func: Any) -> TypeGuard[Coroutineable]: + return is_coroutine_function(func) or callable(func) + +def ensure_generator_function(func: Coroutineable) -> Callable[..., Coroutine]: + if is_coroutine_function(func): + return func + + @wraps(func) + def wrapper(*args, **kwargs): + func(*args, **kwargs) + yield + + return wrapper + +class CoroutineCommand(CommandBase): + coroutine: Optional[Coroutine] + coroutine_function: Optional[Coroutineable] + is_finished: bool + + def __init__(self, coroutine: Union[Coroutine, Coroutineable], requirements: Optional[List[Subsystem]] = None) -> None: + self.coroutine = None + self.coroutine_function = None + + if is_coroutine(coroutine): + self.coroutine = coroutine + elif is_coroutine_function(coroutine): + self.coroutine_function = coroutine + else: + raise TypeError("The coroutine must be a coroutine or a coroutine function") + + if requirements is not None: + self.addRequirements(requirements) + + self.is_finished = False + + def initialize(self) -> None: + if self.coroutine_function: + self.coroutine = ensure_generator_function(self.coroutine_function)() + elif self.coroutine and self.is_finished: + RuntimeError("Generator objects cannot be reused.") + + self.is_finished = False + + + def execute(self): + try: + if not self.is_finished: + if not self.coroutine: + raise TypeError("This command was not properly initialized") + next(self.coroutine) + except StopIteration: + self.is_finished = True + + def isFinished(self): + return self.is_finished + +class commandify: + def __init__(self, requirements: Optional[List[Subsystem]] = None) -> None: + self.requirements = requirements + + def __call__(self, func: Coroutineable): + @wraps(func) + def arg_accepter(*args, **kwargs) -> CoroutineCommand: + return CoroutineCommand(lambda: ensure_generator_function(func)(*args, **kwargs), self.requirements) + return arg_accepter diff --git a/commands2/src/Command.cpp.inl b/commands2/src/Command.cpp.inl index 0316adda..71da0f24 100644 --- a/commands2/src/Command.cpp.inl +++ b/commands2/src/Command.cpp.inl @@ -159,5 +159,12 @@ cls_Command "\n" ":returns: the command with the timeout added\n" DECORATOR_NOTE) + .def("__iter__", + [](std::shared_ptr self) { + return py::make_iterator(CommandIterator(self), CommandIteratorSentinel()); + }, + py::keep_alive<0, 1>(), + "qweqwe\n" + ) ; diff --git a/commands2/src/helpers.cpp b/commands2/src/helpers.cpp index e5d8319e..6b337e3f 100644 --- a/commands2/src/helpers.cpp +++ b/commands2/src/helpers.cpp @@ -23,4 +23,20 @@ std::vector pyargs2SubsystemList(py::args subs) { subsystems.emplace_back(py::cast(sub)); } return subsystems; -} \ No newline at end of file +} + +CommandIterator::CommandIterator(std::shared_ptr cmd) : cmd(cmd) {} +std::shared_ptr CommandIterator::operator*() const { return cmd; } +CommandIterator& CommandIterator::operator++() { + if (!called_initialize) { + cmd->Initialize(); + called_initialize = true; + return *this; + } + cmd->Execute(); + return *this; +} + +bool operator==(const CommandIterator& it, const CommandIteratorSentinel&) { + return it.called_initialize && it.cmd->IsFinished(); +} diff --git a/commands2/src/helpers.h b/commands2/src/helpers.h index c3e3649a..e5c42199 100644 --- a/commands2/src/helpers.h +++ b/commands2/src/helpers.h @@ -16,4 +16,17 @@ std::shared_ptr convertToSharedPtrHack(T *orig) { py::object pyo = py::cast(orig); return py::cast>(pyo); -} \ No newline at end of file +} + +class CommandIterator { + public: + std::shared_ptr cmd; + bool called_initialize = false; + explicit CommandIterator(std::shared_ptr cmd); + std::shared_ptr operator*() const; + CommandIterator& operator++(); +}; + +class CommandIteratorSentinel {}; + +bool operator==(const CommandIterator& it, const CommandIteratorSentinel&); diff --git a/commands2/trigger.py b/commands2/trigger.py new file mode 100644 index 00000000..ffcc13f5 --- /dev/null +++ b/commands2/trigger.py @@ -0,0 +1,76 @@ +from typing import Callable, Optional, overload, List, Union + +from ._impl import Command, Subsystem +from ._impl import Trigger as _Trigger + +from .coroutinecommand import CoroutineCommand, Coroutineable, Coroutine + +class Trigger: + """ + A button that can be pressed or released. + """ + + def __init__(self, is_active: Callable[[], bool] = lambda: False) -> None: + self._trigger = _Trigger(is_active) + + def __bool__(self) -> bool: + return bool(self._trigger) + + def get(self) -> bool: + return bool(self) + + def __call__(self) -> bool: + return bool(self) + + def __and__(self, other: 'Trigger') -> 'Trigger': + return Trigger(lambda: self() and other()) + + def __or__(self, other: 'Trigger') -> 'Trigger': + return Trigger(lambda: self() or other()) + + def __not__(self) -> 'Trigger': + return Trigger(lambda: not self()) + + @overload + def whenActive(self, command: Command, /, interruptible: bool = True) -> None: ... + + @overload + def whenActive(self, coroutine: Union[Coroutine, Coroutineable], /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None) -> None: ... + + @overload + def whenActive(self, coroutine: None, /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None) -> Callable[[Coroutineable], None]: ... + + def whenActive(self, command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]], /, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None) -> Union[None, Callable[[Coroutineable], None]]: + if command_or_coroutine is None: + def wrapper(coroutine: Coroutineable) -> None: + self.whenActive(coroutine, interruptible = interruptible, requirements = requirements) + return wrapper + + if isinstance(command_or_coroutine, Command): + self._trigger.whenActive(command_or_coroutine, interruptible) + return + + self._trigger.whenActive(CoroutineCommand(command_or_coroutine, requirements), interruptible) + return + + @overload + def whenInactive(self, command: Command, /, interruptible: bool = True) -> None: ... + + @overload + def whenInactive(self, coroutine: Union[Coroutine, Coroutineable], /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None) -> None: ... + + @overload + def whenInactive(self, coroutine: None, /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None) -> Callable[[Coroutineable], None]: ... + + def whenInactive(self, command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]], /, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None) -> Union[None, Callable[[Coroutineable], None]]: + if command_or_coroutine is None: + def wrapper(coroutine: Coroutineable) -> None: + self.whenInactive(coroutine, interruptible = interruptible, requirements = requirements) + return wrapper + + if isinstance(command_or_coroutine, Command): + self._trigger.whenInactive(command_or_coroutine, interruptible) + return + + self._trigger.whenInactive(CoroutineCommand(command_or_coroutine, requirements), interruptible) + return \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f298af04..f0805bd8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,10 +58,10 @@ extra_includes = [ ] generate = [ - { Button = "frc2/command/button/Button.h" }, - { JoystickButton = "frc2/command/button/JoystickButton.h" }, - { NetworkButton = "frc2/command/button/NetworkButton.h" }, - { POVButton = "frc2/command/button/POVButton.h" }, + # { Button = "frc2/command/button/Button.h" }, + # { JoystickButton = "frc2/command/button/JoystickButton.h" }, + # { NetworkButton = "frc2/command/button/NetworkButton.h" }, + # { POVButton = "frc2/command/button/POVButton.h" }, { Trigger = "frc2/command/button/Trigger.h" }, { CommandBase = "frc2/command/CommandBase.h" }, From 8488fbce22dba9f2b9e789447e3df0b4fc6323a2 Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Fri, 22 Apr 2022 06:35:01 -0500 Subject: [PATCH 02/27] Allow setting runs_when_disabled for coroutines --- commands2/coroutinecommand.py | 3 ++- commands2/trigger.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/commands2/coroutinecommand.py b/commands2/coroutinecommand.py index 474f0d20..c485beab 100644 --- a/commands2/coroutinecommand.py +++ b/commands2/coroutinecommand.py @@ -33,9 +33,10 @@ class CoroutineCommand(CommandBase): coroutine_function: Optional[Coroutineable] is_finished: bool - def __init__(self, coroutine: Union[Coroutine, Coroutineable], requirements: Optional[List[Subsystem]] = None) -> None: + def __init__(self, coroutine: Union[Coroutine, Coroutineable], requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False) -> None: self.coroutine = None self.coroutine_function = None + self.runsWhenDisabled = lambda: runs_when_disabled if is_coroutine(coroutine): self.coroutine = coroutine diff --git a/commands2/trigger.py b/commands2/trigger.py index ffcc13f5..504917b5 100644 --- a/commands2/trigger.py +++ b/commands2/trigger.py @@ -35,42 +35,42 @@ def __not__(self) -> 'Trigger': def whenActive(self, command: Command, /, interruptible: bool = True) -> None: ... @overload - def whenActive(self, coroutine: Union[Coroutine, Coroutineable], /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None) -> None: ... + def whenActive(self, coroutine: Union[Coroutine, Coroutineable], /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False) -> None: ... @overload - def whenActive(self, coroutine: None, /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None) -> Callable[[Coroutineable], None]: ... + def whenActive(self, coroutine: None, /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False) -> Callable[[Coroutineable], None]: ... - def whenActive(self, command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]], /, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None) -> Union[None, Callable[[Coroutineable], None]]: + def whenActive(self, command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]], /, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False) -> Union[None, Callable[[Coroutineable], None]]: if command_or_coroutine is None: def wrapper(coroutine: Coroutineable) -> None: - self.whenActive(coroutine, interruptible = interruptible, requirements = requirements) + self.whenActive(coroutine, interruptible = interruptible, requirements = requirements, runs_when_disabled = runs_when_disabled) return wrapper if isinstance(command_or_coroutine, Command): self._trigger.whenActive(command_or_coroutine, interruptible) return - self._trigger.whenActive(CoroutineCommand(command_or_coroutine, requirements), interruptible) + self._trigger.whenActive(CoroutineCommand(command_or_coroutine, requirements, runs_when_disabled), interruptible) return @overload def whenInactive(self, command: Command, /, interruptible: bool = True) -> None: ... @overload - def whenInactive(self, coroutine: Union[Coroutine, Coroutineable], /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None) -> None: ... + def whenInactive(self, coroutine: Union[Coroutine, Coroutineable], /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False) -> None: ... @overload - def whenInactive(self, coroutine: None, /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None) -> Callable[[Coroutineable], None]: ... + def whenInactive(self, coroutine: None, /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False) -> Callable[[Coroutineable], None]: ... - def whenInactive(self, command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]], /, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None) -> Union[None, Callable[[Coroutineable], None]]: + def whenInactive(self, command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]], /, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False) -> Union[None, Callable[[Coroutineable], None]]: if command_or_coroutine is None: def wrapper(coroutine: Coroutineable) -> None: - self.whenInactive(coroutine, interruptible = interruptible, requirements = requirements) + self.whenInactive(coroutine, interruptible = interruptible, requirements = requirements, runs_when_disabled = runs_when_disabled) return wrapper if isinstance(command_or_coroutine, Command): self._trigger.whenInactive(command_or_coroutine, interruptible) return - self._trigger.whenInactive(CoroutineCommand(command_or_coroutine, requirements), interruptible) + self._trigger.whenInactive(CoroutineCommand(command_or_coroutine, requirements, runs_when_disabled), interruptible) return \ No newline at end of file From 5de62e0a70adeb0b42445f260e38a3cdcee2ebff Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Fri, 22 Apr 2022 06:37:24 -0500 Subject: [PATCH 03/27] black --- commands2/__init__.py | 1 - commands2/button/button.py | 8 +- commands2/button/joystickbutton.py | 5 +- commands2/button/networkbutton.py | 46 ++++--- commands2/button/povbutton.py | 5 +- commands2/coroutinecommand.py | 132 ++++++++++-------- commands2/trigger.py | 209 +++++++++++++++++++---------- 7 files changed, 248 insertions(+), 158 deletions(-) diff --git a/commands2/__init__.py b/commands2/__init__.py index 07fe0de2..466952a0 100644 --- a/commands2/__init__.py +++ b/commands2/__init__.py @@ -91,7 +91,6 @@ "WaitUntilCommand", # "button", "requirementsDisjoint", - # py side "commandify", "CoroutineCommand", diff --git a/commands2/button/button.py b/commands2/button/button.py index a0b85553..ef21c2b4 100644 --- a/commands2/button/button.py +++ b/commands2/button/button.py @@ -1,8 +1,6 @@ from ..trigger import Trigger -class Button(Trigger): - whenPressed = Trigger.whenActive - whenReleased = Trigger.whenInactive - - +class Button(Trigger): + whenPressed = Trigger.whenActive + whenReleased = Trigger.whenInactive diff --git a/commands2/button/joystickbutton.py b/commands2/button/joystickbutton.py index c47ec2f6..8e42d871 100644 --- a/commands2/button/joystickbutton.py +++ b/commands2/button/joystickbutton.py @@ -2,6 +2,7 @@ from .button import Button + class JoystickButton(Button): - def __init__(self, joystick: Joystick, button: int) -> None: - super().__init__(lambda: joystick.getRawButton(button)) + def __init__(self, joystick: Joystick, button: int) -> None: + super().__init__(lambda: joystick.getRawButton(button)) diff --git a/commands2/button/networkbutton.py b/commands2/button/networkbutton.py index 87ee66ff..53f65f31 100644 --- a/commands2/button/networkbutton.py +++ b/commands2/button/networkbutton.py @@ -4,25 +4,29 @@ from .button import Button -class NetworkButton(Button): - - @overload - def __init__(self, entry: NetworkTableEntry) -> None: ... - - @overload - def __init__(self, table: Union[NetworkTable, str], field: str) -> None: ... - def __init__(self, *args, **kwargs) -> None: - num_args = (len(args) + len(kwargs)) - if num_args == 1: - entry: NetworkTableEntry = kwargs.get('entry', args[0]) - super().__init__(lambda: NetworkTables.isConnected and entry.getBoolean(False)) - else: - table = kwargs.get('table', args[0]) - field = kwargs.get('field', args[-1]) - - if isinstance(table, str): - table = NetworkTables.getTable(table) - - entry = table.getEntry(field) - self.__init__(entry) +class NetworkButton(Button): + @overload + def __init__(self, entry: NetworkTableEntry) -> None: + ... + + @overload + def __init__(self, table: Union[NetworkTable, str], field: str) -> None: + ... + + def __init__(self, *args, **kwargs) -> None: + num_args = len(args) + len(kwargs) + if num_args == 1: + entry: NetworkTableEntry = kwargs.get("entry", args[0]) + super().__init__( + lambda: NetworkTables.isConnected and entry.getBoolean(False) + ) + else: + table = kwargs.get("table", args[0]) + field = kwargs.get("field", args[-1]) + + if isinstance(table, str): + table = NetworkTables.getTable(table) + + entry = table.getEntry(field) + self.__init__(entry) diff --git a/commands2/button/povbutton.py b/commands2/button/povbutton.py index a35a866a..aab05b07 100644 --- a/commands2/button/povbutton.py +++ b/commands2/button/povbutton.py @@ -2,6 +2,7 @@ from .button import Button + class POVButton(Button): - def __init__(self, joystick: Joystick, angle: int, povNumber: int = 0) -> None: - super().__init__(lambda: joystick.getPOV(povNumber) == angle) \ No newline at end of file + def __init__(self, joystick: Joystick, angle: int, povNumber: int = 0) -> None: + super().__init__(lambda: joystick.getPOV(povNumber) == angle) diff --git a/commands2/coroutinecommand.py b/commands2/coroutinecommand.py index c485beab..daaeb179 100644 --- a/commands2/coroutinecommand.py +++ b/commands2/coroutinecommand.py @@ -6,77 +6,91 @@ Coroutine = Generator[None, None, None] CoroutineFunction = Callable[[], Generator[None, None, None]] -Coroutineable = Union[Callable[[], None], CoroutineFunction] +Coroutineable = Union[Callable[[], None], CoroutineFunction] -def is_coroutine(func: Any) -> TypeGuard[Coroutine]: - return inspect.isgenerator(func) - -def is_coroutine_function(func: Any) -> TypeGuard[CoroutineFunction]: - return inspect.isgeneratorfunction(func) - -def is_coroutineable(func: Any) -> TypeGuard[Coroutineable]: - return is_coroutine_function(func) or callable(func) - -def ensure_generator_function(func: Coroutineable) -> Callable[..., Coroutine]: - if is_coroutine_function(func): - return func - @wraps(func) - def wrapper(*args, **kwargs): - func(*args, **kwargs) - yield +def is_coroutine(func: Any) -> TypeGuard[Coroutine]: + return inspect.isgenerator(func) - return wrapper -class CoroutineCommand(CommandBase): - coroutine: Optional[Coroutine] - coroutine_function: Optional[Coroutineable] - is_finished: bool +def is_coroutine_function(func: Any) -> TypeGuard[CoroutineFunction]: + return inspect.isgeneratorfunction(func) - def __init__(self, coroutine: Union[Coroutine, Coroutineable], requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False) -> None: - self.coroutine = None - self.coroutine_function = None - self.runsWhenDisabled = lambda: runs_when_disabled - if is_coroutine(coroutine): - self.coroutine = coroutine - elif is_coroutine_function(coroutine): - self.coroutine_function = coroutine - else: - raise TypeError("The coroutine must be a coroutine or a coroutine function") +def is_coroutineable(func: Any) -> TypeGuard[Coroutineable]: + return is_coroutine_function(func) or callable(func) - if requirements is not None: - self.addRequirements(requirements) - self.is_finished = False +def ensure_generator_function(func: Coroutineable) -> Callable[..., Coroutine]: + if is_coroutine_function(func): + return func - def initialize(self) -> None: - if self.coroutine_function: - self.coroutine = ensure_generator_function(self.coroutine_function)() - elif self.coroutine and self.is_finished: - RuntimeError("Generator objects cannot be reused.") + @wraps(func) + def wrapper(*args, **kwargs): + func(*args, **kwargs) + yield - self.is_finished = False + return wrapper - def execute(self): - try: - if not self.is_finished: - if not self.coroutine: - raise TypeError("This command was not properly initialized") - next(self.coroutine) - except StopIteration: - self.is_finished = True +class CoroutineCommand(CommandBase): + coroutine: Optional[Coroutine] + coroutine_function: Optional[Coroutineable] + is_finished: bool + + def __init__( + self, + coroutine: Union[Coroutine, Coroutineable], + requirements: Optional[List[Subsystem]] = None, + runs_when_disabled: bool = False, + ) -> None: + self.coroutine = None + self.coroutine_function = None + self.runsWhenDisabled = lambda: runs_when_disabled + + if is_coroutine(coroutine): + self.coroutine = coroutine + elif is_coroutine_function(coroutine): + self.coroutine_function = coroutine + else: + raise TypeError("The coroutine must be a coroutine or a coroutine function") + + if requirements is not None: + self.addRequirements(requirements) + + self.is_finished = False + + def initialize(self) -> None: + if self.coroutine_function: + self.coroutine = ensure_generator_function(self.coroutine_function)() + elif self.coroutine and self.is_finished: + RuntimeError("Generator objects cannot be reused.") + + self.is_finished = False + + def execute(self): + try: + if not self.is_finished: + if not self.coroutine: + raise TypeError("This command was not properly initialized") + next(self.coroutine) + except StopIteration: + self.is_finished = True + + def isFinished(self): + return self.is_finished - def isFinished(self): - return self.is_finished class commandify: - def __init__(self, requirements: Optional[List[Subsystem]] = None) -> None: - self.requirements = requirements - - def __call__(self, func: Coroutineable): - @wraps(func) - def arg_accepter(*args, **kwargs) -> CoroutineCommand: - return CoroutineCommand(lambda: ensure_generator_function(func)(*args, **kwargs), self.requirements) - return arg_accepter + def __init__(self, requirements: Optional[List[Subsystem]] = None) -> None: + self.requirements = requirements + + def __call__(self, func: Coroutineable): + @wraps(func) + def arg_accepter(*args, **kwargs) -> CoroutineCommand: + return CoroutineCommand( + lambda: ensure_generator_function(func)(*args, **kwargs), + self.requirements, + ) + + return arg_accepter diff --git a/commands2/trigger.py b/commands2/trigger.py index 504917b5..388b24cc 100644 --- a/commands2/trigger.py +++ b/commands2/trigger.py @@ -5,72 +5,145 @@ from .coroutinecommand import CoroutineCommand, Coroutineable, Coroutine -class Trigger: - """ - A button that can be pressed or released. - """ - - def __init__(self, is_active: Callable[[], bool] = lambda: False) -> None: - self._trigger = _Trigger(is_active) - - def __bool__(self) -> bool: - return bool(self._trigger) - - def get(self) -> bool: - return bool(self) - - def __call__(self) -> bool: - return bool(self) - - def __and__(self, other: 'Trigger') -> 'Trigger': - return Trigger(lambda: self() and other()) - - def __or__(self, other: 'Trigger') -> 'Trigger': - return Trigger(lambda: self() or other()) - - def __not__(self) -> 'Trigger': - return Trigger(lambda: not self()) - - @overload - def whenActive(self, command: Command, /, interruptible: bool = True) -> None: ... - - @overload - def whenActive(self, coroutine: Union[Coroutine, Coroutineable], /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False) -> None: ... - - @overload - def whenActive(self, coroutine: None, /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False) -> Callable[[Coroutineable], None]: ... - def whenActive(self, command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]], /, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False) -> Union[None, Callable[[Coroutineable], None]]: - if command_or_coroutine is None: - def wrapper(coroutine: Coroutineable) -> None: - self.whenActive(coroutine, interruptible = interruptible, requirements = requirements, runs_when_disabled = runs_when_disabled) - return wrapper - - if isinstance(command_or_coroutine, Command): - self._trigger.whenActive(command_or_coroutine, interruptible) - return - - self._trigger.whenActive(CoroutineCommand(command_or_coroutine, requirements, runs_when_disabled), interruptible) - return - - @overload - def whenInactive(self, command: Command, /, interruptible: bool = True) -> None: ... - - @overload - def whenInactive(self, coroutine: Union[Coroutine, Coroutineable], /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False) -> None: ... - - @overload - def whenInactive(self, coroutine: None, /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False) -> Callable[[Coroutineable], None]: ... - - def whenInactive(self, command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]], /, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False) -> Union[None, Callable[[Coroutineable], None]]: - if command_or_coroutine is None: - def wrapper(coroutine: Coroutineable) -> None: - self.whenInactive(coroutine, interruptible = interruptible, requirements = requirements, runs_when_disabled = runs_when_disabled) - return wrapper - - if isinstance(command_or_coroutine, Command): - self._trigger.whenInactive(command_or_coroutine, interruptible) - return - - self._trigger.whenInactive(CoroutineCommand(command_or_coroutine, requirements, runs_when_disabled), interruptible) - return \ No newline at end of file +class Trigger: + """ + A button that can be pressed or released. + """ + + def __init__(self, is_active: Callable[[], bool] = lambda: False) -> None: + self._trigger = _Trigger(is_active) + + def __bool__(self) -> bool: + return bool(self._trigger) + + def get(self) -> bool: + return bool(self) + + def __call__(self) -> bool: + return bool(self) + + def __and__(self, other: "Trigger") -> "Trigger": + return Trigger(lambda: self() and other()) + + def __or__(self, other: "Trigger") -> "Trigger": + return Trigger(lambda: self() or other()) + + def __not__(self) -> "Trigger": + return Trigger(lambda: not self()) + + @overload + def whenActive(self, command: Command, /, interruptible: bool = True) -> None: + ... + + @overload + def whenActive( + self, + coroutine: Union[Coroutine, Coroutineable], + /, + *, + interruptible: bool = True, + requirements: Optional[List[Subsystem]] = None, + runs_when_disabled: bool = False, + ) -> None: + ... + + @overload + def whenActive( + self, + coroutine: None, + /, + *, + interruptible: bool = True, + requirements: Optional[List[Subsystem]] = None, + runs_when_disabled: bool = False, + ) -> Callable[[Coroutineable], None]: + ... + + def whenActive( + self, + command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]], + /, + interruptible: bool = True, + requirements: Optional[List[Subsystem]] = None, + runs_when_disabled: bool = False, + ) -> Union[None, Callable[[Coroutineable], None]]: + if command_or_coroutine is None: + + def wrapper(coroutine: Coroutineable) -> None: + self.whenActive( + coroutine, + interruptible=interruptible, + requirements=requirements, + runs_when_disabled=runs_when_disabled, + ) + + return wrapper + + if isinstance(command_or_coroutine, Command): + self._trigger.whenActive(command_or_coroutine, interruptible) + return + + self._trigger.whenActive( + CoroutineCommand(command_or_coroutine, requirements, runs_when_disabled), + interruptible, + ) + return + + @overload + def whenInactive(self, command: Command, /, interruptible: bool = True) -> None: + ... + + @overload + def whenInactive( + self, + coroutine: Union[Coroutine, Coroutineable], + /, + *, + interruptible: bool = True, + requirements: Optional[List[Subsystem]] = None, + runs_when_disabled: bool = False, + ) -> None: + ... + + @overload + def whenInactive( + self, + coroutine: None, + /, + *, + interruptible: bool = True, + requirements: Optional[List[Subsystem]] = None, + runs_when_disabled: bool = False, + ) -> Callable[[Coroutineable], None]: + ... + + def whenInactive( + self, + command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]], + /, + interruptible: bool = True, + requirements: Optional[List[Subsystem]] = None, + runs_when_disabled: bool = False, + ) -> Union[None, Callable[[Coroutineable], None]]: + if command_or_coroutine is None: + + def wrapper(coroutine: Coroutineable) -> None: + self.whenInactive( + coroutine, + interruptible=interruptible, + requirements=requirements, + runs_when_disabled=runs_when_disabled, + ) + + return wrapper + + if isinstance(command_or_coroutine, Command): + self._trigger.whenInactive(command_or_coroutine, interruptible) + return + + self._trigger.whenInactive( + CoroutineCommand(command_or_coroutine, requirements, runs_when_disabled), + interruptible, + ) + return From 3ee269a6999a51b95be3f6b57bfa4395934004c2 Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Fri, 22 Apr 2022 06:38:39 -0500 Subject: [PATCH 04/27] formatting --- commands2/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/commands2/__init__.py b/commands2/__init__.py index 466952a0..94f9b9b7 100644 --- a/commands2/__init__.py +++ b/commands2/__init__.py @@ -91,7 +91,6 @@ "WaitUntilCommand", # "button", "requirementsDisjoint", - # py side "commandify", "CoroutineCommand", "Trigger", From d285fb04bfa0deacbe554f7924a65f8bc09e58b8 Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Sat, 23 Apr 2022 09:50:26 -0500 Subject: [PATCH 05/27] underscore c++ trigger --- commands2/__init__.py | 2 +- commands2/trigger.py | 2 +- gen/Trigger.yml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/commands2/__init__.py b/commands2/__init__.py index 94f9b9b7..457ee853 100644 --- a/commands2/__init__.py +++ b/commands2/__init__.py @@ -86,7 +86,7 @@ "TrapezoidProfileCommandRadians", "TrapezoidProfileSubsystem", "TrapezoidProfileSubsystemRadians", - # "Trigger", + # "_Trigger", "WaitCommand", "WaitUntilCommand", # "button", diff --git a/commands2/trigger.py b/commands2/trigger.py index 388b24cc..f7ed4a32 100644 --- a/commands2/trigger.py +++ b/commands2/trigger.py @@ -1,7 +1,7 @@ from typing import Callable, Optional, overload, List, Union from ._impl import Command, Subsystem -from ._impl import Trigger as _Trigger +from ._impl import _Trigger from .coroutinecommand import CoroutineCommand, Coroutineable, Coroutine diff --git a/gen/Trigger.yml b/gen/Trigger.yml index 8f3d890e..1fe32072 100644 --- a/gen/Trigger.yml +++ b/gen/Trigger.yml @@ -5,6 +5,7 @@ extra_includes: classes: Trigger: + rename: "_Trigger" shared_ptr: true methods: Trigger: From 70f5715c0f739fc0f2fcd9f427c9fe5b6d6f1e05 Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Sat, 23 Apr 2022 10:03:47 -0500 Subject: [PATCH 06/27] fix bitwise operators for and/or/not --- commands2/trigger.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/commands2/trigger.py b/commands2/trigger.py index f7ed4a32..20b1939e 100644 --- a/commands2/trigger.py +++ b/commands2/trigger.py @@ -29,9 +29,18 @@ def __and__(self, other: "Trigger") -> "Trigger": def __or__(self, other: "Trigger") -> "Trigger": return Trigger(lambda: self() or other()) - def __not__(self) -> "Trigger": + def __invert__(self) -> "Trigger": return Trigger(lambda: not self()) + def not_(self) -> "Trigger": + return ~self + + def or_(self, other: "Trigger") -> "Trigger": + return self | other + + def and_(self, other: "Trigger") -> "Trigger": + return self & other + @overload def whenActive(self, command: Command, /, interruptible: bool = True) -> None: ... @@ -147,3 +156,6 @@ def wrapper(coroutine: Coroutineable) -> None: interruptible, ) return + + + def whileActiveContinuous \ No newline at end of file From 9122d077bad8788e9f3b1228fd781a604fe6e1e7 Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Sat, 23 Apr 2022 10:43:05 -0500 Subject: [PATCH 07/27] add trigger constructor and debounce --- commands2/trigger.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/commands2/trigger.py b/commands2/trigger.py index 20b1939e..0db61e7c 100644 --- a/commands2/trigger.py +++ b/commands2/trigger.py @@ -5,14 +5,24 @@ from .coroutinecommand import CoroutineCommand, Coroutineable, Coroutine +from wpimath.filter import Debouncer class Trigger: """ A button that can be pressed or released. """ - def __init__(self, is_active: Callable[[], bool] = lambda: False) -> None: - self._trigger = _Trigger(is_active) + @overload + def __init__(self, is_active: Callable[[], bool] = lambda: False) -> None: ... + + @overload + def __init__(self, is_active: _Trigger) -> None: ... + + def __init__(self, is_active: Union[Callable[[], bool], _Trigger] = lambda: False) -> None: + if isinstance(is_active, _Trigger): + self._trigger = is_active + else: + self._trigger = _Trigger(is_active) def __bool__(self) -> bool: return bool(self._trigger) @@ -41,6 +51,10 @@ def or_(self, other: "Trigger") -> "Trigger": def and_(self, other: "Trigger") -> "Trigger": return self & other + def debounce(self, debounce_time: float, type: Debouncer.DebounceType) -> "Trigger": + return Trigger(_Trigger.debounce(debounce_time, type)) + + @overload def whenActive(self, command: Command, /, interruptible: bool = True) -> None: ... From 7174dfb6d0fe1175f86181111a8a76551491a9f1 Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Sat, 23 Apr 2022 10:54:07 -0500 Subject: [PATCH 08/27] add missing trigger api --- commands2/trigger.py | 182 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 3 deletions(-) diff --git a/commands2/trigger.py b/commands2/trigger.py index 0db61e7c..007b720b 100644 --- a/commands2/trigger.py +++ b/commands2/trigger.py @@ -1,7 +1,6 @@ from typing import Callable, Optional, overload, List, Union -from ._impl import Command, Subsystem -from ._impl import _Trigger +from ._impl import Command, Subsystem, _Trigger from .coroutinecommand import CoroutineCommand, Coroutineable, Coroutine @@ -54,6 +53,8 @@ def and_(self, other: "Trigger") -> "Trigger": def debounce(self, debounce_time: float, type: Debouncer.DebounceType) -> "Trigger": return Trigger(_Trigger.debounce(debounce_time, type)) + def cancelWhenActive(self, command: Command) -> None: + self._trigger.cancelWhenActive(command) @overload def whenActive(self, command: Command, /, interruptible: bool = True) -> None: @@ -172,4 +173,179 @@ def wrapper(coroutine: Coroutineable) -> None: return - def whileActiveContinuous \ No newline at end of file + @overload + def whileActiveContinous(self, command: Command, /, interruptible: bool = True) -> None: + ... + + @overload + def whileActiveContinous( + self, + coroutine: Union[Coroutine, Coroutineable], + /, + *, + interruptible: bool = True, + requirements: Optional[List[Subsystem]] = None, + runs_when_disabled: bool = False, + ) -> None: + ... + + @overload + def whileActiveContinous( + self, + coroutine: None, + /, + *, + interruptible: bool = True, + requirements: Optional[List[Subsystem]] = None, + runs_when_disabled: bool = False, + ) -> Callable[[Coroutineable], None]: + ... + + def whileActiveContinous( + self, + command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]], + /, + interruptible: bool = True, + requirements: Optional[List[Subsystem]] = None, + runs_when_disabled: bool = False, + ) -> Union[None, Callable[[Coroutineable], None]]: + if command_or_coroutine is None: + + def wrapper(coroutine: Coroutineable) -> None: + self.whileActiveContinous( + coroutine, + interruptible=interruptible, + requirements=requirements, + runs_when_disabled=runs_when_disabled, + ) + + return wrapper + + if isinstance(command_or_coroutine, Command): + self._trigger.whileActiveContinous(command_or_coroutine, interruptible) + return + + self._trigger.whileActiveContinous( + CoroutineCommand(command_or_coroutine, requirements, runs_when_disabled), + interruptible, + ) + return + + + @overload + def whileActiveOnce(self, command: Command, /, interruptible: bool = True) -> None: + ... + + @overload + def whileActiveOnce( + self, + coroutine: Union[Coroutine, Coroutineable], + /, + *, + interruptible: bool = True, + requirements: Optional[List[Subsystem]] = None, + runs_when_disabled: bool = False, + ) -> None: + ... + + @overload + def whileActiveOnce( + self, + coroutine: None, + /, + *, + interruptible: bool = True, + requirements: Optional[List[Subsystem]] = None, + runs_when_disabled: bool = False, + ) -> Callable[[Coroutineable], None]: + ... + + def whileActiveOnce( + self, + command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]], + /, + interruptible: bool = True, + requirements: Optional[List[Subsystem]] = None, + runs_when_disabled: bool = False, + ) -> Union[None, Callable[[Coroutineable], None]]: + if command_or_coroutine is None: + + def wrapper(coroutine: Coroutineable) -> None: + self.whileActiveOnce( + coroutine, + interruptible=interruptible, + requirements=requirements, + runs_when_disabled=runs_when_disabled, + ) + + return wrapper + + if isinstance(command_or_coroutine, Command): + self._trigger.whileActiveOnce(command_or_coroutine, interruptible) + return + + self._trigger.whileActiveOnce( + CoroutineCommand(command_or_coroutine, requirements, runs_when_disabled), + interruptible, + ) + return + + + + @overload + def toggleWhenActive(self, command: Command, /, interruptible: bool = True) -> None: + ... + + @overload + def toggleWhenActive( + self, + coroutine: Union[Coroutine, Coroutineable], + /, + *, + interruptible: bool = True, + requirements: Optional[List[Subsystem]] = None, + runs_when_disabled: bool = False, + ) -> None: + ... + + @overload + def toggleWhenActive( + self, + coroutine: None, + /, + *, + interruptible: bool = True, + requirements: Optional[List[Subsystem]] = None, + runs_when_disabled: bool = False, + ) -> Callable[[Coroutineable], None]: + ... + + def toggleWhenActive( + self, + command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]], + /, + interruptible: bool = True, + requirements: Optional[List[Subsystem]] = None, + runs_when_disabled: bool = False, + ) -> Union[None, Callable[[Coroutineable], None]]: + if command_or_coroutine is None: + + def wrapper(coroutine: Coroutineable) -> None: + self.toggleWhenActive( + coroutine, + interruptible=interruptible, + requirements=requirements, + runs_when_disabled=runs_when_disabled, + ) + + return wrapper + + if isinstance(command_or_coroutine, Command): + self._trigger.toggleWhenActive(command_or_coroutine, interruptible) + return + + self._trigger.toggleWhenActive( + CoroutineCommand(command_or_coroutine, requirements, runs_when_disabled), + interruptible, + ) + return From decd65057f970656b3c0ff52973e06736fbf4730 Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Sat, 23 Apr 2022 10:59:27 -0500 Subject: [PATCH 09/27] add missing button api --- commands2/button/button.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/commands2/button/button.py b/commands2/button/button.py index ef21c2b4..5ed0a814 100644 --- a/commands2/button/button.py +++ b/commands2/button/button.py @@ -4,3 +4,7 @@ class Button(Trigger): whenPressed = Trigger.whenActive whenReleased = Trigger.whenInactive + whileHeld = Trigger.whileActiveContinous + whenHeld = Trigger.whileActiveOnce + toggleWhenPressed = Trigger.toggleWhenActive + cancelWhenPressed = Trigger.cancelWhenActive \ No newline at end of file From 1c7e4d92ab709f9f69cb99a4e9ab6c2d3d92a928 Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Sat, 23 Apr 2022 11:04:55 -0500 Subject: [PATCH 10/27] remove old c++ wrapping --- commands2/src/Trigger.cpp.inl | 28 ---------------------------- gen/Trigger.yml | 4 ---- 2 files changed, 32 deletions(-) delete mode 100644 commands2/src/Trigger.cpp.inl diff --git a/commands2/src/Trigger.cpp.inl b/commands2/src/Trigger.cpp.inl deleted file mode 100644 index 694f24d7..00000000 --- a/commands2/src/Trigger.cpp.inl +++ /dev/null @@ -1,28 +0,0 @@ -cls_Trigger - .def("and_", [](Trigger * self, Trigger * other) { - return *self && *other; - }, py::arg("other"), - "Composes this trigger with another trigger, returning a new trigger that is active when both\n" - "triggers are active.\n" - "\n" - ":param trigger: the trigger to compose with\n" - "\n" - ":returns: the trigger that is active when both triggers are active\n") - .def("or_", [](Trigger * self, Trigger * other) { - return *self || *other; - }, py::arg("other"), - "Composes this trigger with another trigger, returning a new trigger that is active when either\n" - "triggers are active.\n" - "\n" - ":param trigger: the trigger to compose with\n" - "\n" - ":returns: the trigger that is active when both triggers are active\n") - .def("not_", [](Trigger * self) { - return !*self; - }, - "Creates a new trigger that is active when this trigger is inactive, i.e. that acts as the\n" - "negation of this trigger.\n" - "\n" - ":param trigger: the trigger to compose with\n" - "\n" - ":returns: the trigger that is active when both triggers are active\n"); \ No newline at end of file diff --git a/gen/Trigger.yml b/gen/Trigger.yml index 1fe32072..66f83c34 100644 --- a/gen/Trigger.yml +++ b/gen/Trigger.yml @@ -50,7 +50,3 @@ classes: Debounce: Get: rename: __bool__ - - -inline_code: | - #include "src/Trigger.cpp.inl" \ No newline at end of file From ed5b106b93d8dfac3971d72a884327b19a0ef215 Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Sat, 23 Apr 2022 11:11:04 -0500 Subject: [PATCH 11/27] add __iter__ docstring --- commands2/src/Command.cpp.inl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/commands2/src/Command.cpp.inl b/commands2/src/Command.cpp.inl index 71da0f24..382d06fc 100644 --- a/commands2/src/Command.cpp.inl +++ b/commands2/src/Command.cpp.inl @@ -164,7 +164,12 @@ cls_Command return py::make_iterator(CommandIterator(self), CommandIteratorSentinel()); }, py::keep_alive<0, 1>(), - "qweqwe\n" + "Creates an Iterator for this command. The iterator will run the command and\n" + "will only exhaust when the command is finished.\n" + "Note that the iterator will not run the command in the background. It must be\n" + "explicitly be iterated over.\n" + "\n" + ":returns: an iterator for this command\n" ) ; From aafde42a99ec719789a36f8d1092a0a0db6ba935 Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Sat, 23 Apr 2022 11:16:34 -0500 Subject: [PATCH 12/27] better networkbutton error message --- commands2/button/networkbutton.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/commands2/button/networkbutton.py b/commands2/button/networkbutton.py index 53f65f31..7cda8e83 100644 --- a/commands2/button/networkbutton.py +++ b/commands2/button/networkbutton.py @@ -21,7 +21,7 @@ def __init__(self, *args, **kwargs) -> None: super().__init__( lambda: NetworkTables.isConnected and entry.getBoolean(False) ) - else: + elif num_args == 2: table = kwargs.get("table", args[0]) field = kwargs.get("field", args[-1]) @@ -30,3 +30,7 @@ def __init__(self, *args, **kwargs) -> None: entry = table.getEntry(field) self.__init__(entry) + else: + raise TypeError( + f"__init__() takes 1 or 2 positional arguments but {num_args} were given" + ) From 0bb3251cd52bd16d8a09b099923dc097ebdee56c Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Sat, 23 Apr 2022 12:12:52 -0500 Subject: [PATCH 13/27] add docstrings --- commands2/button/button.py | 6 + commands2/button/joystickbutton.py | 13 ++ commands2/button/networkbutton.py | 20 ++- commands2/button/povbutton.py | 14 ++ commands2/coroutinecommand.py | 11 ++ commands2/trigger.py | 233 ++++++++++++++++++++++++----- 6 files changed, 260 insertions(+), 37 deletions(-) diff --git a/commands2/button/button.py b/commands2/button/button.py index 5ed0a814..7779baaf 100644 --- a/commands2/button/button.py +++ b/commands2/button/button.py @@ -2,6 +2,12 @@ class Button(Trigger): + """ + A class used to bind command scheduling to button presses. + Can be composed with other buttons with the operators in Trigger. + + @see Trigger + """ whenPressed = Trigger.whenActive whenReleased = Trigger.whenInactive whileHeld = Trigger.whileActiveContinous diff --git a/commands2/button/joystickbutton.py b/commands2/button/joystickbutton.py index 8e42d871..c79c9dee 100644 --- a/commands2/button/joystickbutton.py +++ b/commands2/button/joystickbutton.py @@ -4,5 +4,18 @@ class JoystickButton(Button): + """ + A class used to bind command scheduling to joystick button presses. + Can be composed with other buttons with the operators in Trigger. + + @see Trigger + """ + def __init__(self, joystick: Joystick, button: int) -> None: + """ + Creates a JoystickButton that commands can be bound to. + + :param joystick: The joystick on which the button is located. + :param button: The number of the button on the joystick. + """ super().__init__(lambda: joystick.getRawButton(button)) diff --git a/commands2/button/networkbutton.py b/commands2/button/networkbutton.py index 7cda8e83..d982f05c 100644 --- a/commands2/button/networkbutton.py +++ b/commands2/button/networkbutton.py @@ -6,13 +6,29 @@ class NetworkButton(Button): + """ + A class used to bind command scheduling to a NetworkTable boolean fields. + Can be composed with other buttons with the operators in Trigger. + + @see Trigger + """ + @overload def __init__(self, entry: NetworkTableEntry) -> None: - ... + """ + Creates a NetworkButton that commands can be bound to. + + :param entry: The entry that is the value. + """ @overload def __init__(self, table: Union[NetworkTable, str], field: str) -> None: - ... + """ + Creates a NetworkButton that commands can be bound to. + + :param table: The table where the networktable value is located. + :param field: The field that is the value. + """ def __init__(self, *args, **kwargs) -> None: num_args = len(args) + len(kwargs) diff --git a/commands2/button/povbutton.py b/commands2/button/povbutton.py index aab05b07..b82fb204 100644 --- a/commands2/button/povbutton.py +++ b/commands2/button/povbutton.py @@ -4,5 +4,19 @@ class POVButton(Button): + """ + A class used to bind command scheduling to joystick POV presses. + Can be composed with other buttons with the operators in Trigger. + + @see Trigger + """ + def __init__(self, joystick: Joystick, angle: int, povNumber: int = 0) -> None: + """ + Creates a POVButton that commands can be bound to. + + :param joystick: The joystick on which the button is located. + :param angle: The angle of the POV corresponding to a button press. + :param povNumber: The number of the POV on the joystick. + """ super().__init__(lambda: joystick.getPOV(povNumber) == angle) diff --git a/commands2/coroutinecommand.py b/commands2/coroutinecommand.py index daaeb179..bae7f881 100644 --- a/commands2/coroutinecommand.py +++ b/commands2/coroutinecommand.py @@ -34,6 +34,10 @@ def wrapper(*args, **kwargs): class CoroutineCommand(CommandBase): + """ + A class that wraps a coroutine function into a command. + """ + coroutine: Optional[Coroutine] coroutine_function: Optional[Coroutineable] is_finished: bool @@ -44,6 +48,13 @@ def __init__( requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False, ) -> None: + """ + Creates a CoroutineCommand than can be used as a command. + + :param coroutine: The coroutine or coroutine function to bind. + :param requirements: The subsystems that this command requires. + :param runs_when_disabled: Whether or not this command runs when the robot is disabled. + """ self.coroutine = None self.coroutine_function = None self.runsWhenDisabled = lambda: runs_when_disabled diff --git a/commands2/trigger.py b/commands2/trigger.py index 007b720b..a988b7f7 100644 --- a/commands2/trigger.py +++ b/commands2/trigger.py @@ -8,14 +8,33 @@ class Trigger: """ - A button that can be pressed or released. + A class used to bind command scheduling to events. The + Trigger class is a base for all command-event-binding classes, and so the + methods are named fairly abstractly; for purpose-specific wrappers, see + Button. + + @see Button """ @overload - def __init__(self, is_active: Callable[[], bool] = lambda: False) -> None: ... + def __init__(self, is_active: Callable[[], bool] = lambda: False) -> None: + """ + Create a new trigger that is active when the given condition is true. + + :param is_active: Whether the trigger is active. + + Create a new trigger that is never active (default constructor) - activity + can be further determined by subclass code. + """ @overload - def __init__(self, is_active: _Trigger) -> None: ... + def __init__(self, is_active: _Trigger) -> None: + """ + Create a new trigger from an existing c++ trigger. + Robot code does not need to use this constructor. + + :param is_active: The c++ trigger to wrap. + """ def __init__(self, is_active: Union[Callable[[], bool], _Trigger] = lambda: False) -> None: if isinstance(is_active, _Trigger): @@ -24,41 +43,90 @@ def __init__(self, is_active: Union[Callable[[], bool], _Trigger] = lambda: Fals self._trigger = _Trigger(is_active) def __bool__(self) -> bool: + """ + Returns whether or not the trigger is currently active + """ return bool(self._trigger) def get(self) -> bool: + """ + Returns whether or not the trigger is currently active + """ return bool(self) def __call__(self) -> bool: + """ + Returns whether or not the trigger is currently active + """ return bool(self) def __and__(self, other: "Trigger") -> "Trigger": + """ + Composes this trigger with another trigger, returning a new trigger that is active when both + triggers are active. + + :param trigger: the trigger to compose with + + :returns: the trigger that is active when both triggers are active + """ return Trigger(lambda: self() and other()) def __or__(self, other: "Trigger") -> "Trigger": + """ + Composes this trigger with another trigger, returning a new trigger that is active when either + triggers are active. + + :param trigger: the trigger to compose with + + :returns: the trigger that is active when both triggers are active + """ return Trigger(lambda: self() or other()) def __invert__(self) -> "Trigger": + """ + Creates a new trigger that is active when this trigger is inactive, i.e. that acts as the + negation of this trigger. + + :param trigger: the trigger to compose with + + :returns: the trigger that is active when both triggers are active + """ return Trigger(lambda: not self()) - def not_(self) -> "Trigger": - return ~self + and_ = __and__ + or_ = __or__ + not_ = __invert__ - def or_(self, other: "Trigger") -> "Trigger": - return self | other + def debounce(self, debounceTime: float, type: Debouncer.DebounceType) -> "Trigger": + """ + Creates a new debounced trigger from this trigger - it will become active + when this trigger has been active for longer than the specified period. - def and_(self, other: "Trigger") -> "Trigger": - return self & other + :param debounceTime: The debounce period. + :param type: The debounce type. - def debounce(self, debounce_time: float, type: Debouncer.DebounceType) -> "Trigger": - return Trigger(_Trigger.debounce(debounce_time, type)) + :returns: The debounced trigger. + """ + return Trigger(_Trigger.debounce(debounceTime, type)) def cancelWhenActive(self, command: Command) -> None: + """ + Binds a command to be canceled when the trigger becomes active. Takes a + raw pointer, and so is non-owning; users are responsible for the lifespan + and scheduling of the command. + + :param command: The command to bind. + """ self._trigger.cancelWhenActive(command) @overload def whenActive(self, command: Command, /, interruptible: bool = True) -> None: - ... + """ + Binds a command to start when the trigger becomes active. + + :param command: The command to bind. + :param interruptible: Whether the command should be interruptible. + """ @overload def whenActive( @@ -70,23 +138,37 @@ def whenActive( requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False, ) -> None: - ... + """ + Binds a coroutine to start when the trigger becomes active. + + :param coroutine: The coroutine or coroutine function to bind. + :param interruptible: Whether the command should be interruptible. + :param requirements: The subsystems required to run the coroutine. + :param runs_when_disabled: Whether the coroutine should run when the subsystem is disabled. + """ @overload def whenActive( self, - coroutine: None, /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False, ) -> Callable[[Coroutineable], None]: - ... + """ + Binds a coroutine to start when the trigger becomes active (Decorator Form). + A def should be under this. + + :param interruptible: Whether the command should be interruptible. + :param requirements: The subsystems required to run the coroutine. + :param runs_when_disabled: Whether the coroutine should run when the subsystem is disabled. + """ + def whenActive( self, - command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]], + command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]] = None, /, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, @@ -116,8 +198,12 @@ def wrapper(coroutine: Coroutineable) -> None: @overload def whenInactive(self, command: Command, /, interruptible: bool = True) -> None: - ... + """ + Binds a command to start when the trigger becomes inactive. + :param command: The command to bind. + :param interruptible: Whether the command should be interruptible. + """ @overload def whenInactive( self, @@ -128,23 +214,36 @@ def whenInactive( requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False, ) -> None: - ... + """ + Binds a coroutine to start when the trigger becomes inactive. + + :param coroutine: The coroutine or coroutine function to bind. + :param interruptible: Whether the command should be interruptible. + :param requirements: The subsystems required to run the coroutine. + :param runs_when_disabled: Whether the coroutine should run when the subsystem is disabled. + """ @overload def whenInactive( self, - coroutine: None, /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False, ) -> Callable[[Coroutineable], None]: - ... + """ + Binds a coroutine to start when the trigger becomes active (Decorator Form). + A def should be under this. + + :param interruptible: Whether the command should be interruptible. + :param requirements: The subsystems required to run the coroutine. + :param runs_when_disabled: Whether the coroutine should run when the subsystem is disabled. + """ def whenInactive( self, - command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]], + command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]] = None, /, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, @@ -175,7 +274,13 @@ def wrapper(coroutine: Coroutineable) -> None: @overload def whileActiveContinous(self, command: Command, /, interruptible: bool = True) -> None: - ... + """ + Binds a command to be started repeatedly while the trigger is active, and + canceled when it becomes inactive. + + :param command: The command to bind. + :param interruptible: Whether the command should be interruptible. + """ @overload def whileActiveContinous( @@ -187,23 +292,38 @@ def whileActiveContinous( requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False, ) -> None: - ... + """ + Binds a command to be started repeatedly while the trigger is active, and + canceled when it becomes inactive. + + :param coroutine: The coroutine or coroutine function to bind. + :param interruptible: Whether the command should be interruptible. + :param requirements: The subsystems required to run the coroutine. + :param runs_when_disabled: Whether the coroutine should run when the subsystem is disabled. + """ @overload def whileActiveContinous( self, - coroutine: None, /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False, ) -> Callable[[Coroutineable], None]: - ... + """ + Binds a command to be started repeatedly while the trigger is active, and + canceled when it becomes inactive (Decorator Form). + A def should be under this. + + :param interruptible: Whether the command should be interruptible. + :param requirements: The subsystems required to run the coroutine. + :param runs_when_disabled: Whether the coroutine should run when the subsystem is disabled. + """ def whileActiveContinous( self, - command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]], + command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]] = None, /, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, @@ -234,7 +354,13 @@ def wrapper(coroutine: Coroutineable) -> None: @overload def whileActiveOnce(self, command: Command, /, interruptible: bool = True) -> None: - ... + """ + Binds a command to be started when the trigger becomes active, and + canceled when it becomes inactive. + + :param command: The command to bind. + :param interruptible: Whether the command should be interruptible. + """ @overload def whileActiveOnce( @@ -246,23 +372,38 @@ def whileActiveOnce( requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False, ) -> None: - ... + """ + Binds a command to be started when the trigger becomes active, and + canceled when it becomes inactive. + + :param coroutine: The coroutine or coroutine function to bind. + :param interruptible: Whether the command should be interruptible. + :param requirements: The subsystems required to run the coroutine. + :param runs_when_disabled: Whether the coroutine should run when the subsystem is disabled. + """ @overload def whileActiveOnce( self, - coroutine: None, /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False, ) -> Callable[[Coroutineable], None]: - ... + """ + Binds a command to be started when the trigger becomes active, and + canceled when it becomes inactive (Decorator Form). + A def should be under this. + + :param interruptible: Whether the command should be interruptible. + :param requirements: The subsystems required to run the coroutine. + :param runs_when_disabled: Whether the coroutine should run when the subsystem is disabled. + """ def whileActiveOnce( self, - command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]], + command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]] = None, /, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, @@ -294,6 +435,13 @@ def wrapper(coroutine: Coroutineable) -> None: @overload def toggleWhenActive(self, command: Command, /, interruptible: bool = True) -> None: + """ + Binds a command to start when the trigger becomes active, and be canceled + when it again becomes active. + + :param command: The command to bind. + :param interruptible: Whether the command should be interruptible. + """ ... @overload @@ -306,23 +454,38 @@ def toggleWhenActive( requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False, ) -> None: - ... + """ + Binds a command to start when the trigger becomes active, and be canceled + when it again becomes active. + + :param coroutine: The coroutine or coroutine function to bind. + :param interruptible: Whether the command should be interruptible. + :param requirements: The subsystems required to run the coroutine. + :param runs_when_disabled: Whether the coroutine should run when the subsystem is disabled. + """ @overload def toggleWhenActive( self, - coroutine: None, /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False, ) -> Callable[[Coroutineable], None]: - ... + """ + Binds a command to start when the trigger becomes active, and be canceled + when it again becomes active (Decorator Form). + A def should be under this. + + :param interruptible: Whether the command should be interruptible. + :param requirements: The subsystems required to run the coroutine. + :param runs_when_disabled: Whether the coroutine should run when the subsystem is disabled. + """ def toggleWhenActive( self, - command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]], + command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]] = None, /, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, From 8f91b2b065c6e4c46d2b77d3b880d010165471fe Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Sat, 23 Apr 2022 12:36:27 -0500 Subject: [PATCH 14/27] change commandify implementation to function --- commands2/coroutinecommand.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/commands2/coroutinecommand.py b/commands2/coroutinecommand.py index bae7f881..1339158a 100644 --- a/commands2/coroutinecommand.py +++ b/commands2/coroutinecommand.py @@ -1,5 +1,5 @@ from functools import wraps -from typing import Any, Callable, Generator, List, Union, Optional +from typing import Any, Callable, Generator, List, Union, Optional, overload from ._impl import CommandBase, Subsystem import inspect from typing_extensions import TypeGuard @@ -91,17 +91,36 @@ def execute(self): def isFinished(self): return self.is_finished +@overload +def commandify(*, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False) -> Callable[[Coroutineable], Callable[..., CoroutineCommand]]: + """ + A decorator that turns a coroutine function into a command. + A def should be under this. -class commandify: - def __init__(self, requirements: Optional[List[Subsystem]] = None) -> None: - self.requirements = requirements + :param requirements: The subsystems that this command requires. + :param runs_when_disabled: Whether or not this command runs when the robot is disabled. + """ - def __call__(self, func: Coroutineable): +@overload +def commandify(coroutine: Coroutineable, /) -> Callable[..., CoroutineCommand]: + """ + A decorator that turns a coroutine function into a command. + A def should be under this. + """ + +def commandify(coroutine: Optional[Coroutineable] = None, /, *, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False) -> Union[Callable[[Coroutineable], Callable[..., CoroutineCommand]],Callable[..., CoroutineCommand]]: + def wrapper(func: Coroutineable) -> Callable[..., CoroutineCommand]: @wraps(func) def arg_accepter(*args, **kwargs) -> CoroutineCommand: return CoroutineCommand( lambda: ensure_generator_function(func)(*args, **kwargs), - self.requirements, + requirements, ) return arg_accepter + + if coroutine is None: + return wrapper + + return wrapper(coroutine) + From 71322a3d7f6e5684b7b719ccb2dcfb64c4e08f86 Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Sat, 23 Apr 2022 12:36:53 -0500 Subject: [PATCH 15/27] black --- commands2/button/button.py | 3 ++- commands2/button/joystickbutton.py | 2 +- commands2/coroutinecommand.py | 21 +++++++++++++++++---- commands2/trigger.py | 15 ++++++++------- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/commands2/button/button.py b/commands2/button/button.py index 7779baaf..d0cafb86 100644 --- a/commands2/button/button.py +++ b/commands2/button/button.py @@ -8,9 +8,10 @@ class Button(Trigger): @see Trigger """ + whenPressed = Trigger.whenActive whenReleased = Trigger.whenInactive whileHeld = Trigger.whileActiveContinous whenHeld = Trigger.whileActiveOnce toggleWhenPressed = Trigger.toggleWhenActive - cancelWhenPressed = Trigger.cancelWhenActive \ No newline at end of file + cancelWhenPressed = Trigger.cancelWhenActive diff --git a/commands2/button/joystickbutton.py b/commands2/button/joystickbutton.py index c79c9dee..6964956a 100644 --- a/commands2/button/joystickbutton.py +++ b/commands2/button/joystickbutton.py @@ -10,7 +10,7 @@ class JoystickButton(Button): @see Trigger """ - + def __init__(self, joystick: Joystick, button: int) -> None: """ Creates a JoystickButton that commands can be bound to. diff --git a/commands2/coroutinecommand.py b/commands2/coroutinecommand.py index 1339158a..adfc93e2 100644 --- a/commands2/coroutinecommand.py +++ b/commands2/coroutinecommand.py @@ -91,8 +91,11 @@ def execute(self): def isFinished(self): return self.is_finished + @overload -def commandify(*, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False) -> Callable[[Coroutineable], Callable[..., CoroutineCommand]]: +def commandify( + *, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False +) -> Callable[[Coroutineable], Callable[..., CoroutineCommand]]: """ A decorator that turns a coroutine function into a command. A def should be under this. @@ -101,6 +104,7 @@ def commandify(*, requirements: Optional[List[Subsystem]] = None, runs_when_disa :param runs_when_disabled: Whether or not this command runs when the robot is disabled. """ + @overload def commandify(coroutine: Coroutineable, /) -> Callable[..., CoroutineCommand]: """ @@ -108,7 +112,17 @@ def commandify(coroutine: Coroutineable, /) -> Callable[..., CoroutineCommand]: A def should be under this. """ -def commandify(coroutine: Optional[Coroutineable] = None, /, *, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False) -> Union[Callable[[Coroutineable], Callable[..., CoroutineCommand]],Callable[..., CoroutineCommand]]: + +def commandify( + coroutine: Optional[Coroutineable] = None, + /, + *, + requirements: Optional[List[Subsystem]] = None, + runs_when_disabled: bool = False, +) -> Union[ + Callable[[Coroutineable], Callable[..., CoroutineCommand]], + Callable[..., CoroutineCommand], +]: def wrapper(func: Coroutineable) -> Callable[..., CoroutineCommand]: @wraps(func) def arg_accepter(*args, **kwargs) -> CoroutineCommand: @@ -118,9 +132,8 @@ def arg_accepter(*args, **kwargs) -> CoroutineCommand: ) return arg_accepter - + if coroutine is None: return wrapper return wrapper(coroutine) - diff --git a/commands2/trigger.py b/commands2/trigger.py index a988b7f7..ed4ccf67 100644 --- a/commands2/trigger.py +++ b/commands2/trigger.py @@ -6,6 +6,7 @@ from wpimath.filter import Debouncer + class Trigger: """ A class used to bind command scheduling to events. The @@ -36,7 +37,9 @@ def __init__(self, is_active: _Trigger) -> None: :param is_active: The c++ trigger to wrap. """ - def __init__(self, is_active: Union[Callable[[], bool], _Trigger] = lambda: False) -> None: + def __init__( + self, is_active: Union[Callable[[], bool], _Trigger] = lambda: False + ) -> None: if isinstance(is_active, _Trigger): self._trigger = is_active else: @@ -165,7 +168,6 @@ def whenActive( :param runs_when_disabled: Whether the coroutine should run when the subsystem is disabled. """ - def whenActive( self, command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]] = None, @@ -204,6 +206,7 @@ def whenInactive(self, command: Command, /, interruptible: bool = True) -> None: :param command: The command to bind. :param interruptible: Whether the command should be interruptible. """ + @overload def whenInactive( self, @@ -271,9 +274,10 @@ def wrapper(coroutine: Coroutineable) -> None: ) return - @overload - def whileActiveContinous(self, command: Command, /, interruptible: bool = True) -> None: + def whileActiveContinous( + self, command: Command, /, interruptible: bool = True + ) -> None: """ Binds a command to be started repeatedly while the trigger is active, and canceled when it becomes inactive. @@ -351,7 +355,6 @@ def wrapper(coroutine: Coroutineable) -> None: ) return - @overload def whileActiveOnce(self, command: Command, /, interruptible: bool = True) -> None: """ @@ -431,8 +434,6 @@ def wrapper(coroutine: Coroutineable) -> None: ) return - - @overload def toggleWhenActive(self, command: Command, /, interruptible: bool = True) -> None: """ From bac4080d230de15e7708de501dbd390fcb1353a9 Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Sat, 23 Apr 2022 21:50:57 -0500 Subject: [PATCH 16/27] fix test --- commands2/coroutinecommand.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands2/coroutinecommand.py b/commands2/coroutinecommand.py index adfc93e2..08a78a0d 100644 --- a/commands2/coroutinecommand.py +++ b/commands2/coroutinecommand.py @@ -61,7 +61,7 @@ def __init__( if is_coroutine(coroutine): self.coroutine = coroutine - elif is_coroutine_function(coroutine): + elif is_coroutineable(coroutine): self.coroutine_function = coroutine else: raise TypeError("The coroutine must be a coroutine or a coroutine function") From 78151119686a578018715c08050ca7434a701466 Mon Sep 17 00:00:00 2001 From: Vasista Vovveti Date: Sun, 24 Apr 2022 00:15:17 -0500 Subject: [PATCH 17/27] Update commands2/button/networkbutton.py Co-authored-by: David Vo --- commands2/button/networkbutton.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands2/button/networkbutton.py b/commands2/button/networkbutton.py index d982f05c..2dcec6c3 100644 --- a/commands2/button/networkbutton.py +++ b/commands2/button/networkbutton.py @@ -35,7 +35,7 @@ def __init__(self, *args, **kwargs) -> None: if num_args == 1: entry: NetworkTableEntry = kwargs.get("entry", args[0]) super().__init__( - lambda: NetworkTables.isConnected and entry.getBoolean(False) + lambda: NetworkTables.isConnected() and entry.getBoolean(False) ) elif num_args == 2: table = kwargs.get("table", args[0]) From 021ce431196429fd1fc698a4cea80e1380b624e0 Mon Sep 17 00:00:00 2001 From: Vasista Vovveti Date: Sun, 24 Apr 2022 00:15:27 -0500 Subject: [PATCH 18/27] Update commands2/coroutinecommand.py Co-authored-by: David Vo --- commands2/coroutinecommand.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands2/coroutinecommand.py b/commands2/coroutinecommand.py index 08a78a0d..c3fadffc 100644 --- a/commands2/coroutinecommand.py +++ b/commands2/coroutinecommand.py @@ -75,7 +75,7 @@ def initialize(self) -> None: if self.coroutine_function: self.coroutine = ensure_generator_function(self.coroutine_function)() elif self.coroutine and self.is_finished: - RuntimeError("Generator objects cannot be reused.") + raise RuntimeError("Generator objects cannot be reused.") self.is_finished = False From 401aa5fcc1d3395e5ccbe10e6f69c4bac12e7396 Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Wed, 27 Apr 2022 21:09:18 -0500 Subject: [PATCH 19/27] ci and nt args --- commands2/button/networkbutton.py | 6 +++--- commands2/coroutinecommand.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/commands2/button/networkbutton.py b/commands2/button/networkbutton.py index d982f05c..ce8ce4a2 100644 --- a/commands2/button/networkbutton.py +++ b/commands2/button/networkbutton.py @@ -33,13 +33,13 @@ def __init__(self, table: Union[NetworkTable, str], field: str) -> None: def __init__(self, *args, **kwargs) -> None: num_args = len(args) + len(kwargs) if num_args == 1: - entry: NetworkTableEntry = kwargs.get("entry", args[0]) + entry: NetworkTableEntry = kwargs.get("entry") or args[0] super().__init__( lambda: NetworkTables.isConnected and entry.getBoolean(False) ) elif num_args == 2: - table = kwargs.get("table", args[0]) - field = kwargs.get("field", args[-1]) + table = kwargs.get("table") or args[0] + field = kwargs.get("field") or args[-1] if isinstance(table, str): table = NetworkTables.getTable(table) diff --git a/commands2/coroutinecommand.py b/commands2/coroutinecommand.py index 08a78a0d..2d7b3c12 100644 --- a/commands2/coroutinecommand.py +++ b/commands2/coroutinecommand.py @@ -55,6 +55,7 @@ def __init__( :param requirements: The subsystems that this command requires. :param runs_when_disabled: Whether or not this command runs when the robot is disabled. """ + super().__init__() self.coroutine = None self.coroutine_function = None self.runsWhenDisabled = lambda: runs_when_disabled From ffbeeca3b02a5ac56c4d99a80eff71ae523642bd Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Thu, 28 Apr 2022 01:08:34 -0500 Subject: [PATCH 20/27] better error messages --- commands2/button/joystickbutton.py | 6 ++++++ commands2/button/networkbutton.py | 17 ++++++++++++++--- commands2/button/povbutton.py | 6 ++++++ 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/commands2/button/joystickbutton.py b/commands2/button/joystickbutton.py index 6964956a..f34a9ad2 100644 --- a/commands2/button/joystickbutton.py +++ b/commands2/button/joystickbutton.py @@ -18,4 +18,10 @@ def __init__(self, joystick: Joystick, button: int) -> None: :param joystick: The joystick on which the button is located. :param button: The number of the button on the joystick. """ + if not isinstance(joystick, Joystick) or not isinstance(button, int): + raise TypeError( + "JoystickButton.__init__(): incompatible constructor arguments. The following argument types are supported:\n" + "\t1. commands2.button.JoystickButton(joystick: Joystick, button: int)\n" + f"Invoked with: {joystick}, {button}" + ) super().__init__(lambda: joystick.getRawButton(button)) diff --git a/commands2/button/networkbutton.py b/commands2/button/networkbutton.py index 2171f578..943c90d6 100644 --- a/commands2/button/networkbutton.py +++ b/commands2/button/networkbutton.py @@ -34,6 +34,10 @@ def __init__(self, *args, **kwargs) -> None: num_args = len(args) + len(kwargs) if num_args == 1: entry: NetworkTableEntry = kwargs.get("entry") or args[0] + + if not isinstance(entry, NetworkTableEntry): + raise self._type_error(entry) + super().__init__( lambda: NetworkTables.isConnected() and entry.getBoolean(False) ) @@ -47,6 +51,13 @@ def __init__(self, *args, **kwargs) -> None: entry = table.getEntry(field) self.__init__(entry) else: - raise TypeError( - f"__init__() takes 1 or 2 positional arguments but {num_args} were given" - ) + raise self._type_error(args) + + def _type_error(self, *args): + return TypeError( + "NetworkButton.__init__(): incompatible constructor arguments. The following argument types are supported:\n" + "\t1. commands2.button.NetworkButton(entry: NetworkTableEntry)\n" + "\t2. commands2.button.NetworkButton(table: str, field: str)\n" + "\t3. commands2.button.NetworkButton(table: NetworkTable, field: str)\n" + f"Invoked with: {', '.join(map(str, args))}" + ) diff --git a/commands2/button/povbutton.py b/commands2/button/povbutton.py index b82fb204..b682d2bc 100644 --- a/commands2/button/povbutton.py +++ b/commands2/button/povbutton.py @@ -19,4 +19,10 @@ def __init__(self, joystick: Joystick, angle: int, povNumber: int = 0) -> None: :param angle: The angle of the POV corresponding to a button press. :param povNumber: The number of the POV on the joystick. """ + if not isinstance(joystick, Joystick) or not isinstance(angle, int) or not isinstance(povNumber, int): + raise TypeError( + "POVButton.__init__(): incompatible constructor arguments. The following argument types are supported:\n" + "\t1. commands2.button.POVButton(joystick: Joystick, angle: int, povNumber: int)\n" + f"Invoked with: {joystick}, {angle}, {povNumber}" + ) super().__init__(lambda: joystick.getPOV(povNumber) == angle) From f2cae932a600b6ccc2e91894fbb32caba974c993 Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Thu, 28 Apr 2022 23:45:08 -0500 Subject: [PATCH 21/27] 3.7 support --- commands2/coroutinecommand.py | 1 - commands2/trigger.py | 37 +++++++++++------------------------ 2 files changed, 11 insertions(+), 27 deletions(-) diff --git a/commands2/coroutinecommand.py b/commands2/coroutinecommand.py index f879ec20..78b76660 100644 --- a/commands2/coroutinecommand.py +++ b/commands2/coroutinecommand.py @@ -116,7 +116,6 @@ def commandify(coroutine: Coroutineable, /) -> Callable[..., CoroutineCommand]: def commandify( coroutine: Optional[Coroutineable] = None, - /, *, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False, diff --git a/commands2/trigger.py b/commands2/trigger.py index ed4ccf67..f35807a9 100644 --- a/commands2/trigger.py +++ b/commands2/trigger.py @@ -110,7 +110,7 @@ def debounce(self, debounceTime: float, type: Debouncer.DebounceType) -> "Trigge :returns: The debounced trigger. """ - return Trigger(_Trigger.debounce(debounceTime, type)) + return Trigger(self._trigger.debounce(debounceTime, type)) def cancelWhenActive(self, command: Command) -> None: """ @@ -123,7 +123,7 @@ def cancelWhenActive(self, command: Command) -> None: self._trigger.cancelWhenActive(command) @overload - def whenActive(self, command: Command, /, interruptible: bool = True) -> None: + def whenActive(self, command_or_coroutine: Command, interruptible: bool = True) -> None: """ Binds a command to start when the trigger becomes active. @@ -134,8 +134,7 @@ def whenActive(self, command: Command, /, interruptible: bool = True) -> None: @overload def whenActive( self, - coroutine: Union[Coroutine, Coroutineable], - /, + command_or_coroutine: Union[Coroutine, Coroutineable], *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, @@ -153,7 +152,6 @@ def whenActive( @overload def whenActive( self, - /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, @@ -171,7 +169,6 @@ def whenActive( def whenActive( self, command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]] = None, - /, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False, @@ -199,7 +196,7 @@ def wrapper(coroutine: Coroutineable) -> None: return @overload - def whenInactive(self, command: Command, /, interruptible: bool = True) -> None: + def whenInactive(self, command_or_coroutine: Command, interruptible: bool = True) -> None: """ Binds a command to start when the trigger becomes inactive. @@ -210,8 +207,7 @@ def whenInactive(self, command: Command, /, interruptible: bool = True) -> None: @overload def whenInactive( self, - coroutine: Union[Coroutine, Coroutineable], - /, + command_or_coroutine: Union[Coroutine, Coroutineable], *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, @@ -229,7 +225,6 @@ def whenInactive( @overload def whenInactive( self, - /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, @@ -247,7 +242,6 @@ def whenInactive( def whenInactive( self, command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]] = None, - /, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False, @@ -276,7 +270,7 @@ def wrapper(coroutine: Coroutineable) -> None: @overload def whileActiveContinous( - self, command: Command, /, interruptible: bool = True + self, command_or_coroutine: Command, interruptible: bool = True ) -> None: """ Binds a command to be started repeatedly while the trigger is active, and @@ -289,8 +283,7 @@ def whileActiveContinous( @overload def whileActiveContinous( self, - coroutine: Union[Coroutine, Coroutineable], - /, + command_or_coroutine: Union[Coroutine, Coroutineable], *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, @@ -309,7 +302,6 @@ def whileActiveContinous( @overload def whileActiveContinous( self, - /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, @@ -328,7 +320,6 @@ def whileActiveContinous( def whileActiveContinous( self, command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]] = None, - /, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False, @@ -356,7 +347,7 @@ def wrapper(coroutine: Coroutineable) -> None: return @overload - def whileActiveOnce(self, command: Command, /, interruptible: bool = True) -> None: + def whileActiveOnce(self, command_or_coroutine: Command, interruptible: bool = True) -> None: """ Binds a command to be started when the trigger becomes active, and canceled when it becomes inactive. @@ -368,8 +359,7 @@ def whileActiveOnce(self, command: Command, /, interruptible: bool = True) -> No @overload def whileActiveOnce( self, - coroutine: Union[Coroutine, Coroutineable], - /, + command_or_coroutine: Union[Coroutine, Coroutineable], *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, @@ -388,7 +378,6 @@ def whileActiveOnce( @overload def whileActiveOnce( self, - /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, @@ -407,7 +396,6 @@ def whileActiveOnce( def whileActiveOnce( self, command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]] = None, - /, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False, @@ -435,7 +423,7 @@ def wrapper(coroutine: Coroutineable) -> None: return @overload - def toggleWhenActive(self, command: Command, /, interruptible: bool = True) -> None: + def toggleWhenActive(self, command_or_coroutine: Command, interruptible: bool = True) -> None: """ Binds a command to start when the trigger becomes active, and be canceled when it again becomes active. @@ -448,8 +436,7 @@ def toggleWhenActive(self, command: Command, /, interruptible: bool = True) -> N @overload def toggleWhenActive( self, - coroutine: Union[Coroutine, Coroutineable], - /, + command_or_coroutine: Union[Coroutine, Coroutineable], *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, @@ -468,7 +455,6 @@ def toggleWhenActive( @overload def toggleWhenActive( self, - /, *, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, @@ -487,7 +473,6 @@ def toggleWhenActive( def toggleWhenActive( self, command_or_coroutine: Optional[Union[Command, Coroutine, Coroutineable]] = None, - /, interruptible: bool = True, requirements: Optional[List[Subsystem]] = None, runs_when_disabled: bool = False, From 606872317e97d237fdee5273e5cea1490f01dc5f Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Fri, 29 Apr 2022 01:48:16 -0500 Subject: [PATCH 22/27] skip test --- tests/test_button.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_button.py b/tests/test_button.py index b0efc194..acc75a3b 100644 --- a/tests/test_button.py +++ b/tests/test_button.py @@ -2,7 +2,7 @@ import commands2.button from util import Counter - +import pytest class MyButton(commands2.button.Button): def __init__(self): @@ -161,7 +161,7 @@ def test_cancel_when_pressed(scheduler: commands2.CommandScheduler): assert cmd1.canceled == 1 assert not scheduler.isScheduled(cmd1) - +@pytest.mark.xfail(strict=False) def test_function_bindings(scheduler: commands2.CommandScheduler): buttonWhenPressed = MyButton() @@ -174,9 +174,9 @@ def test_function_bindings(scheduler: commands2.CommandScheduler): counter = Counter() - buttonWhenPressed.whenPressed(counter.increment) - buttonWhileHeld.whileHeld(counter.increment) - buttonWhenReleased.whenReleased(counter.increment) + buttonWhenPressed.whenPressed(lambda: (print("wp"), counter.increment(), None)[2]) + buttonWhileHeld.whileHeld(lambda: (print("wh"), counter.increment(), None)[2]) + buttonWhenReleased.whenReleased(lambda: (print("wr"), counter.increment(), None)[2]) scheduler.run() buttonWhenPressed.setPressed(True) From d8078e7de0db94202d2268712e835f7d1566fc72 Mon Sep 17 00:00:00 2001 From: TheTripleV Date: Fri, 29 Apr 2022 02:06:00 -0500 Subject: [PATCH 23/27] more 3.7 --- commands2/coroutinecommand.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands2/coroutinecommand.py b/commands2/coroutinecommand.py index 78b76660..8d48d498 100644 --- a/commands2/coroutinecommand.py +++ b/commands2/coroutinecommand.py @@ -107,7 +107,7 @@ def commandify( @overload -def commandify(coroutine: Coroutineable, /) -> Callable[..., CoroutineCommand]: +def commandify(coroutine: Coroutineable) -> Callable[..., CoroutineCommand]: """ A decorator that turns a coroutine function into a command. A def should be under this. From c97771166df45798288127817d29e724983a8f4b Mon Sep 17 00:00:00 2001 From: Vasista Vovveti Date: Sat, 30 Apr 2022 22:30:31 +0000 Subject: [PATCH 24/27] fix whileHeld instant command --- commands2/coroutinecommand.py | 2 ++ tests/test_button.py | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/commands2/coroutinecommand.py b/commands2/coroutinecommand.py index 8d48d498..f91fc9ce 100644 --- a/commands2/coroutinecommand.py +++ b/commands2/coroutinecommand.py @@ -28,6 +28,7 @@ def ensure_generator_function(func: Coroutineable) -> Callable[..., Coroutine]: @wraps(func) def wrapper(*args, **kwargs): func(*args, **kwargs) + return yield return wrapper @@ -81,6 +82,7 @@ def initialize(self) -> None: self.is_finished = False def execute(self): + print("r") try: if not self.is_finished: if not self.coroutine: diff --git a/tests/test_button.py b/tests/test_button.py index acc75a3b..68363aed 100644 --- a/tests/test_button.py +++ b/tests/test_button.py @@ -161,7 +161,7 @@ def test_cancel_when_pressed(scheduler: commands2.CommandScheduler): assert cmd1.canceled == 1 assert not scheduler.isScheduled(cmd1) -@pytest.mark.xfail(strict=False) + def test_function_bindings(scheduler: commands2.CommandScheduler): buttonWhenPressed = MyButton() @@ -174,9 +174,9 @@ def test_function_bindings(scheduler: commands2.CommandScheduler): counter = Counter() - buttonWhenPressed.whenPressed(lambda: (print("wp"), counter.increment(), None)[2]) - buttonWhileHeld.whileHeld(lambda: (print("wh"), counter.increment(), None)[2]) - buttonWhenReleased.whenReleased(lambda: (print("wr"), counter.increment(), None)[2]) + buttonWhenPressed.whenPressed(counter.increment) + buttonWhileHeld.whileHeld(counter.increment) + buttonWhenReleased.whenReleased(counter.increment) scheduler.run() buttonWhenPressed.setPressed(True) From 40283200d1718e9a21b54c79abc0851cafd6ff02 Mon Sep 17 00:00:00 2001 From: Vasista Vovveti Date: Sat, 30 Apr 2022 22:33:18 +0000 Subject: [PATCH 25/27] black --- commands2/button/networkbutton.py | 2 +- commands2/button/povbutton.py | 6 +++++- commands2/trigger.py | 16 ++++++++++++---- tests/test_button.py | 1 + 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/commands2/button/networkbutton.py b/commands2/button/networkbutton.py index 943c90d6..1fa43851 100644 --- a/commands2/button/networkbutton.py +++ b/commands2/button/networkbutton.py @@ -37,7 +37,7 @@ def __init__(self, *args, **kwargs) -> None: if not isinstance(entry, NetworkTableEntry): raise self._type_error(entry) - + super().__init__( lambda: NetworkTables.isConnected() and entry.getBoolean(False) ) diff --git a/commands2/button/povbutton.py b/commands2/button/povbutton.py index b682d2bc..a029fc44 100644 --- a/commands2/button/povbutton.py +++ b/commands2/button/povbutton.py @@ -19,7 +19,11 @@ def __init__(self, joystick: Joystick, angle: int, povNumber: int = 0) -> None: :param angle: The angle of the POV corresponding to a button press. :param povNumber: The number of the POV on the joystick. """ - if not isinstance(joystick, Joystick) or not isinstance(angle, int) or not isinstance(povNumber, int): + if ( + not isinstance(joystick, Joystick) + or not isinstance(angle, int) + or not isinstance(povNumber, int) + ): raise TypeError( "POVButton.__init__(): incompatible constructor arguments. The following argument types are supported:\n" "\t1. commands2.button.POVButton(joystick: Joystick, angle: int, povNumber: int)\n" diff --git a/commands2/trigger.py b/commands2/trigger.py index f35807a9..42ab3dcd 100644 --- a/commands2/trigger.py +++ b/commands2/trigger.py @@ -123,7 +123,9 @@ def cancelWhenActive(self, command: Command) -> None: self._trigger.cancelWhenActive(command) @overload - def whenActive(self, command_or_coroutine: Command, interruptible: bool = True) -> None: + def whenActive( + self, command_or_coroutine: Command, interruptible: bool = True + ) -> None: """ Binds a command to start when the trigger becomes active. @@ -196,7 +198,9 @@ def wrapper(coroutine: Coroutineable) -> None: return @overload - def whenInactive(self, command_or_coroutine: Command, interruptible: bool = True) -> None: + def whenInactive( + self, command_or_coroutine: Command, interruptible: bool = True + ) -> None: """ Binds a command to start when the trigger becomes inactive. @@ -347,7 +351,9 @@ def wrapper(coroutine: Coroutineable) -> None: return @overload - def whileActiveOnce(self, command_or_coroutine: Command, interruptible: bool = True) -> None: + def whileActiveOnce( + self, command_or_coroutine: Command, interruptible: bool = True + ) -> None: """ Binds a command to be started when the trigger becomes active, and canceled when it becomes inactive. @@ -423,7 +429,9 @@ def wrapper(coroutine: Coroutineable) -> None: return @overload - def toggleWhenActive(self, command_or_coroutine: Command, interruptible: bool = True) -> None: + def toggleWhenActive( + self, command_or_coroutine: Command, interruptible: bool = True + ) -> None: """ Binds a command to start when the trigger becomes active, and be canceled when it again becomes active. diff --git a/tests/test_button.py b/tests/test_button.py index 68363aed..930b8594 100644 --- a/tests/test_button.py +++ b/tests/test_button.py @@ -4,6 +4,7 @@ from util import Counter import pytest + class MyButton(commands2.button.Button): def __init__(self): super().__init__(self.isPressed) From 243072cf3575a9347e53f2a9f637f2250305fa1e Mon Sep 17 00:00:00 2001 From: Vasista Vovveti Date: Tue, 3 May 2022 04:38:15 +0000 Subject: [PATCH 26/27] most tests --- tests/test_button_coroutine_functions.py | 165 +++++++++++++++++++++ tests/test_button_coroutines.py | 166 ++++++++++++++++++++++ tests/test_button_decorator_coroutines.py | 141 ++++++++++++++++++ tests/test_coroutines.py | 111 +++++++++++++++ 4 files changed, 583 insertions(+) create mode 100644 tests/test_button_coroutine_functions.py create mode 100644 tests/test_button_coroutines.py create mode 100644 tests/test_button_decorator_coroutines.py create mode 100644 tests/test_coroutines.py diff --git a/tests/test_button_coroutine_functions.py b/tests/test_button_coroutine_functions.py new file mode 100644 index 00000000..f901cce8 --- /dev/null +++ b/tests/test_button_coroutine_functions.py @@ -0,0 +1,165 @@ +import commands2 +import commands2.button + +from util import Counter +import pytest + + +class MyButton(commands2.button.Button): + def __init__(self): + super().__init__(self.isPressed) + self.pressed = False + + def isPressed(self) -> bool: + return self.pressed + + def setPressed(self, value: bool): + self.pressed = value + + +def test_when_pressed_coroutine(scheduler: commands2.CommandScheduler): + button = MyButton() + + class state: pass + state.executed = False + def cmd1(): + state.executed = True + return + yield + + button.setPressed(False) + button.whenPressed(cmd1) + scheduler.run() + + assert not state.executed + + button.setPressed(True) + scheduler.run() + scheduler.run() + + assert state.executed + + +def test_when_released_coroutine(scheduler: commands2.CommandScheduler): + button = MyButton() + + class state: pass + state.executed = False + def cmd1(): + state.executed = True + return + yield + + button.setPressed(True) + button.whenReleased(cmd1) + scheduler.run() + + assert not state.executed + + button.setPressed(False) + scheduler.run() + scheduler.run() + + assert state.executed + +def test_while_held_coroutine(scheduler: commands2.CommandScheduler): + button = MyButton() + + class state: pass + state.executed = 0 + def cmd1(): + state.executed += 1 + return + yield + + button.setPressed(False) + button.whileHeld(cmd1) + scheduler.run() + + assert not state.executed + + button.setPressed(True) + scheduler.run() + scheduler.run() + assert state.executed == 2 + + button.setPressed(False) + scheduler.run() + + assert state.executed == 2 + +def test_when_held_coroutine(scheduler: commands2.CommandScheduler): + button = MyButton() + + class state: pass + state.executed = 0 + def cmd1(): + while True: + state.executed += 1 + yield + + button.setPressed(False) + button.whenHeld(cmd1()) + scheduler.run() + + assert not state.executed + + button.setPressed(True) + scheduler.run() + scheduler.run() + assert state.executed == 2 + + button.setPressed(False) + + assert state.executed == 2 + +def test_toggle_when_pressed_coroutine(scheduler: commands2.CommandScheduler): + button = MyButton() + + class state: pass + state.executed = 0 + def cmd1(): + while True: + state.executed += 1 + yield + + button.setPressed(False) + + button.toggleWhenPressed(cmd1) + scheduler.run() + + assert not state.executed + + button.setPressed(True) + scheduler.run() + + assert state.executed + + +def test_function_bindings_coroutine(scheduler: commands2.CommandScheduler): + + buttonWhenPressed = MyButton() + buttonWhileHeld = MyButton() + buttonWhenReleased = MyButton() + + buttonWhenPressed.setPressed(False) + buttonWhileHeld.setPressed(True) + buttonWhenReleased.setPressed(True) + + counter = Counter() + + def increment(): + counter.increment() + return + yield + + buttonWhenPressed.whenPressed(increment) + buttonWhileHeld.whileHeld(increment) + buttonWhenReleased.whenReleased(increment) + + scheduler.run() + buttonWhenPressed.setPressed(True) + buttonWhenReleased.setPressed(False) + scheduler.run() + + assert counter.value == 4 \ No newline at end of file diff --git a/tests/test_button_coroutines.py b/tests/test_button_coroutines.py new file mode 100644 index 00000000..dffa4b5e --- /dev/null +++ b/tests/test_button_coroutines.py @@ -0,0 +1,166 @@ +import commands2 +import commands2.button + +from util import Counter +import pytest + + +class MyButton(commands2.button.Button): + def __init__(self): + super().__init__(self.isPressed) + self.pressed = False + + def isPressed(self) -> bool: + return self.pressed + + def setPressed(self, value: bool): + self.pressed = value + + +def test_when_pressed_coroutine(scheduler: commands2.CommandScheduler): + button = MyButton() + + class state: pass + state.executed = False + def cmd1(): + state.executed = True + return + yield + + button.setPressed(False) + button.whenPressed(cmd1()) + scheduler.run() + + assert not state.executed + + button.setPressed(True) + scheduler.run() + scheduler.run() + + assert state.executed + + +def test_when_released_coroutine(scheduler: commands2.CommandScheduler): + button = MyButton() + + class state: pass + state.executed = False + def cmd1(): + state.executed = True + return + yield + + button.setPressed(True) + button.whenReleased(cmd1()) + scheduler.run() + + assert not state.executed + + button.setPressed(False) + scheduler.run() + scheduler.run() + + assert state.executed + +@pytest.mark.xfail(strict=True) +def test_while_held_coroutine(scheduler: commands2.CommandScheduler): + button = MyButton() + + class state: pass + state.executed = 0 + def cmd1(): + state.executed += 1 + return + yield + + button.setPressed(False) + button.whileHeld(cmd1()) + scheduler.run() + + assert not state.executed + + button.setPressed(True) + scheduler.run() + scheduler.run() + assert state.executed == 2 + + button.setPressed(False) + scheduler.run() + + assert state.executed == 2 + +def test_when_held_coroutine(scheduler: commands2.CommandScheduler): + button = MyButton() + + class state: pass + state.executed = 0 + def cmd1(): + while True: + state.executed += 1 + yield + + button.setPressed(False) + button.whenHeld(cmd1()) + scheduler.run() + + assert not state.executed + + button.setPressed(True) + scheduler.run() + scheduler.run() + assert state.executed == 2 + + button.setPressed(False) + + assert state.executed == 2 + +def test_toggle_when_pressed_coroutine(scheduler: commands2.CommandScheduler): + button = MyButton() + + class state: pass + state.executed = 0 + def cmd1(): + while True: + state.executed += 1 + yield + + button.setPressed(False) + + button.toggleWhenPressed(cmd1()) + scheduler.run() + + assert not state.executed + + button.setPressed(True) + scheduler.run() + + assert state.executed + +@pytest.mark.xfail(strict=True) +def test_function_bindings_coroutine(scheduler: commands2.CommandScheduler): + + buttonWhenPressed = MyButton() + buttonWhileHeld = MyButton() + buttonWhenReleased = MyButton() + + buttonWhenPressed.setPressed(False) + buttonWhileHeld.setPressed(True) + buttonWhenReleased.setPressed(True) + + counter = Counter() + + def increment(): + counter.increment() + return + yield + + buttonWhenPressed.whenPressed(increment()) + buttonWhileHeld.whileHeld(increment()) + buttonWhenReleased.whenReleased(increment()) + + scheduler.run() + buttonWhenPressed.setPressed(True) + buttonWhenReleased.setPressed(False) + scheduler.run() + + assert counter.value == 4 \ No newline at end of file diff --git a/tests/test_button_decorator_coroutines.py b/tests/test_button_decorator_coroutines.py new file mode 100644 index 00000000..3c7eb4ed --- /dev/null +++ b/tests/test_button_decorator_coroutines.py @@ -0,0 +1,141 @@ +import commands2 +import commands2.button + +from util import Counter +import pytest + + +class MyButton(commands2.button.Button): + def __init__(self): + super().__init__(self.isPressed) + self.pressed = False + + def isPressed(self) -> bool: + return self.pressed + + def setPressed(self, value: bool): + self.pressed = value + + +def test_when_pressed_coroutine(scheduler: commands2.CommandScheduler): + button = MyButton() + button.setPressed(False) + + class state: pass + state.executed = False + + @button.whenPressed + def cmd1(): + state.executed = True + return + yield + + scheduler.run() + + assert not state.executed + + button.setPressed(True) + scheduler.run() + scheduler.run() + + assert state.executed + + +def test_when_released_coroutine(scheduler: commands2.CommandScheduler): + button = MyButton() + button.setPressed(True) + + class state: pass + state.executed = False + + @button.whenReleased() + def cmd1(): + state.executed = True + return + yield + + scheduler.run() + + assert not state.executed + + button.setPressed(False) + scheduler.run() + scheduler.run() + + assert state.executed + +def test_while_held_coroutine(scheduler: commands2.CommandScheduler): + button = MyButton() + button.setPressed(False) + + class state: pass + state.executed = 0 + + @button.whileHeld(interruptible=True) + def cmd1(): + state.executed += 1 + return + yield + + scheduler.run() + + assert not state.executed + + button.setPressed(True) + scheduler.run() + scheduler.run() + assert state.executed == 2 + + button.setPressed(False) + scheduler.run() + + assert state.executed == 2 + +def test_when_held_coroutine(scheduler: commands2.CommandScheduler): + button = MyButton() + button.setPressed(False) + + class state: pass + state.executed = 0 + + @button.whenHeld(runs_when_disabled=True) + def cmd1(): + while True: + state.executed += 1 + yield + + scheduler.run() + + assert not state.executed + + button.setPressed(True) + scheduler.run() + scheduler.run() + assert state.executed == 2 + + button.setPressed(False) + + assert state.executed == 2 + +def test_toggle_when_pressed_coroutine(scheduler: commands2.CommandScheduler): + button = MyButton() + button.setPressed(False) + + class state: pass + state.executed = 0 + + @button.toggleWhenPressed + def cmd1(): + while True: + state.executed += 1 + yield + + + scheduler.run() + + assert not state.executed + + button.setPressed(True) + scheduler.run() + + assert state.executed diff --git a/tests/test_coroutines.py b/tests/test_coroutines.py new file mode 100644 index 00000000..d7443459 --- /dev/null +++ b/tests/test_coroutines.py @@ -0,0 +1,111 @@ +import commands2 +import commands2.button +from commands2 import commandify + +from util import Counter +import pytest + +def test_coroutine_command(scheduler: commands2.CommandScheduler): + class state: pass + state.co1_count = 0 + def co1(): + for i in range(3): + state.co1_count += 1 + yield + + cmd1 = commands2.CoroutineCommand(co1) + + cmd1.schedule() + + for _ in range(10): + scheduler.run() + + assert state.co1_count == 3 + + +def test_coroutine_composition(scheduler: commands2.CommandScheduler): + class state: pass + state.co1_count = 0 + state.co2_count = 0 + def co1(): + for i in range(3): + state.co1_count += 1 + yield + + def co2(): + state.co2_count += 1 + yield from co1() + state.co2_count += 1 + + commands2.CoroutineCommand(co2).schedule() + + for _ in range(10): + scheduler.run() + + assert state.co1_count == 3 + assert state.co2_count == 2 + +def test_yield_from_command(scheduler: commands2.CommandScheduler): + class state: pass + state.co1_count = 0 + state.co2_count = 0 + + class Command1(commands2.CommandBase): + def execute(self) -> None: + state.co1_count += 1 + + def isFinished(self) -> bool: + return state.co1_count == 3 + + def co2(): + state.co2_count += 1 + yield from Command1() + state.co2_count += 1 + + commands2.CoroutineCommand(co2).schedule() + + for _ in range(10): + scheduler.run() + + assert state.co1_count == 3 + assert state.co2_count == 2 + +# def test_commandify(scheduler: commands2.CommandScheduler): +# class state: pass +# state.co1_count = 0 + +# def co1(n): +# for i in range(n): +# print(1) +# state.co1_count += 1 +# yield + +# Cmd1 = commandify(co1) +# Cmd1(5).schedule() +# # Cmd1(5).schedule() + +# for _ in range(10): +# scheduler.run() + +# assert state.co1_count == 5 + +def test_commandify_decorator(scheduler: commands2.CommandScheduler): + class state: pass + state.co1_count = 0 + + @commandify + def Cmd1(n): + for i in range(n): + print(1) + state.co1_count += 1 + yield + + cmd = Cmd1(5) + cmd.schedule() + # Cmd1(5).schedule() + for _ in range(10): + scheduler.run() + + assert state.co1_count == 5 + +def test_coroutine_command_lambda_pass_args(): ... \ No newline at end of file From cb5af470e513b3d204e4742dbf27be30a40a8a0d Mon Sep 17 00:00:00 2001 From: Vasista Vovveti Date: Wed, 23 Nov 2022 19:50:05 -0500 Subject: [PATCH 27/27] get it testable --- commands2/button/networkbutton.py | 2 +- tests/test_button_coroutine_functions.py | 38 ++++++++++++++----- tests/test_button_coroutines.py | 39 ++++++++++++++----- tests/test_button_decorator_coroutines.py | 30 ++++++++++----- tests/test_coroutines.py | 46 +++++++++++++++-------- 5 files changed, 110 insertions(+), 45 deletions(-) diff --git a/commands2/button/networkbutton.py b/commands2/button/networkbutton.py index 1fa43851..b0b9302d 100644 --- a/commands2/button/networkbutton.py +++ b/commands2/button/networkbutton.py @@ -1,4 +1,4 @@ -from networktables import NetworkTable, NetworkTables, NetworkTableEntry +from ntcore import NetworkTable, NetworkTableEntry from typing import Union, overload diff --git a/tests/test_button_coroutine_functions.py b/tests/test_button_coroutine_functions.py index f901cce8..b1744ad9 100644 --- a/tests/test_button_coroutine_functions.py +++ b/tests/test_button_coroutine_functions.py @@ -20,8 +20,11 @@ def setPressed(self, value: bool): def test_when_pressed_coroutine(scheduler: commands2.CommandScheduler): button = MyButton() - class state: pass + class state: + pass + state.executed = False + def cmd1(): state.executed = True return @@ -43,8 +46,11 @@ def cmd1(): def test_when_released_coroutine(scheduler: commands2.CommandScheduler): button = MyButton() - class state: pass + class state: + pass + state.executed = False + def cmd1(): state.executed = True return @@ -62,11 +68,15 @@ def cmd1(): assert state.executed + def test_while_held_coroutine(scheduler: commands2.CommandScheduler): button = MyButton() - class state: pass + class state: + pass + state.executed = 0 + def cmd1(): state.executed += 1 return @@ -82,17 +92,21 @@ def cmd1(): scheduler.run() scheduler.run() assert state.executed == 2 - + button.setPressed(False) scheduler.run() assert state.executed == 2 + def test_when_held_coroutine(scheduler: commands2.CommandScheduler): button = MyButton() - class state: pass + class state: + pass + state.executed = 0 + def cmd1(): while True: state.executed += 1 @@ -108,23 +122,27 @@ def cmd1(): scheduler.run() scheduler.run() assert state.executed == 2 - + button.setPressed(False) assert state.executed == 2 + def test_toggle_when_pressed_coroutine(scheduler: commands2.CommandScheduler): button = MyButton() - class state: pass + class state: + pass + state.executed = 0 + def cmd1(): while True: state.executed += 1 yield - + button.setPressed(False) - + button.toggleWhenPressed(cmd1) scheduler.run() @@ -162,4 +180,4 @@ def increment(): buttonWhenReleased.setPressed(False) scheduler.run() - assert counter.value == 4 \ No newline at end of file + assert counter.value == 4 diff --git a/tests/test_button_coroutines.py b/tests/test_button_coroutines.py index dffa4b5e..97fad40b 100644 --- a/tests/test_button_coroutines.py +++ b/tests/test_button_coroutines.py @@ -20,8 +20,11 @@ def setPressed(self, value: bool): def test_when_pressed_coroutine(scheduler: commands2.CommandScheduler): button = MyButton() - class state: pass + class state: + pass + state.executed = False + def cmd1(): state.executed = True return @@ -43,8 +46,11 @@ def cmd1(): def test_when_released_coroutine(scheduler: commands2.CommandScheduler): button = MyButton() - class state: pass + class state: + pass + state.executed = False + def cmd1(): state.executed = True return @@ -62,12 +68,16 @@ def cmd1(): assert state.executed + @pytest.mark.xfail(strict=True) def test_while_held_coroutine(scheduler: commands2.CommandScheduler): button = MyButton() - class state: pass + class state: + pass + state.executed = 0 + def cmd1(): state.executed += 1 return @@ -83,17 +93,21 @@ def cmd1(): scheduler.run() scheduler.run() assert state.executed == 2 - + button.setPressed(False) scheduler.run() assert state.executed == 2 + def test_when_held_coroutine(scheduler: commands2.CommandScheduler): button = MyButton() - class state: pass + class state: + pass + state.executed = 0 + def cmd1(): while True: state.executed += 1 @@ -109,23 +123,27 @@ def cmd1(): scheduler.run() scheduler.run() assert state.executed == 2 - + button.setPressed(False) assert state.executed == 2 + def test_toggle_when_pressed_coroutine(scheduler: commands2.CommandScheduler): button = MyButton() - class state: pass + class state: + pass + state.executed = 0 + def cmd1(): while True: state.executed += 1 yield - + button.setPressed(False) - + button.toggleWhenPressed(cmd1()) scheduler.run() @@ -136,6 +154,7 @@ def cmd1(): assert state.executed + @pytest.mark.xfail(strict=True) def test_function_bindings_coroutine(scheduler: commands2.CommandScheduler): @@ -163,4 +182,4 @@ def increment(): buttonWhenReleased.setPressed(False) scheduler.run() - assert counter.value == 4 \ No newline at end of file + assert counter.value == 4 diff --git a/tests/test_button_decorator_coroutines.py b/tests/test_button_decorator_coroutines.py index 3c7eb4ed..5f0a6710 100644 --- a/tests/test_button_decorator_coroutines.py +++ b/tests/test_button_decorator_coroutines.py @@ -21,7 +21,9 @@ def test_when_pressed_coroutine(scheduler: commands2.CommandScheduler): button = MyButton() button.setPressed(False) - class state: pass + class state: + pass + state.executed = False @button.whenPressed @@ -45,7 +47,9 @@ def test_when_released_coroutine(scheduler: commands2.CommandScheduler): button = MyButton() button.setPressed(True) - class state: pass + class state: + pass + state.executed = False @button.whenReleased() @@ -64,11 +68,14 @@ def cmd1(): assert state.executed + def test_while_held_coroutine(scheduler: commands2.CommandScheduler): button = MyButton() button.setPressed(False) - class state: pass + class state: + pass + state.executed = 0 @button.whileHeld(interruptible=True) @@ -85,17 +92,20 @@ def cmd1(): scheduler.run() scheduler.run() assert state.executed == 2 - + button.setPressed(False) scheduler.run() assert state.executed == 2 + def test_when_held_coroutine(scheduler: commands2.CommandScheduler): button = MyButton() button.setPressed(False) - class state: pass + class state: + pass + state.executed = 0 @button.whenHeld(runs_when_disabled=True) @@ -112,16 +122,19 @@ def cmd1(): scheduler.run() scheduler.run() assert state.executed == 2 - + button.setPressed(False) assert state.executed == 2 + def test_toggle_when_pressed_coroutine(scheduler: commands2.CommandScheduler): button = MyButton() button.setPressed(False) - class state: pass + class state: + pass + state.executed = 0 @button.toggleWhenPressed @@ -129,8 +142,7 @@ def cmd1(): while True: state.executed += 1 yield - - + scheduler.run() assert not state.executed diff --git a/tests/test_coroutines.py b/tests/test_coroutines.py index d7443459..14961421 100644 --- a/tests/test_coroutines.py +++ b/tests/test_coroutines.py @@ -5,13 +5,17 @@ from util import Counter import pytest + def test_coroutine_command(scheduler: commands2.CommandScheduler): - class state: pass + class state: + pass + state.co1_count = 0 + def co1(): for i in range(3): state.co1_count += 1 - yield + yield cmd1 = commands2.CoroutineCommand(co1) @@ -19,14 +23,17 @@ def co1(): for _ in range(10): scheduler.run() - + assert state.co1_count == 3 def test_coroutine_composition(scheduler: commands2.CommandScheduler): - class state: pass + class state: + pass + state.co1_count = 0 state.co2_count = 0 + def co1(): for i in range(3): state.co1_count += 1 @@ -41,19 +48,22 @@ def co2(): for _ in range(10): scheduler.run() - + assert state.co1_count == 3 assert state.co2_count == 2 + def test_yield_from_command(scheduler: commands2.CommandScheduler): - class state: pass + class state: + pass + state.co1_count = 0 state.co2_count = 0 class Command1(commands2.CommandBase): def execute(self) -> None: state.co1_count += 1 - + def isFinished(self) -> bool: return state.co1_count == 3 @@ -61,15 +71,16 @@ def co2(): state.co2_count += 1 yield from Command1() state.co2_count += 1 - + commands2.CoroutineCommand(co2).schedule() for _ in range(10): scheduler.run() - + assert state.co1_count == 3 assert state.co2_count == 2 + # def test_commandify(scheduler: commands2.CommandScheduler): # class state: pass # state.co1_count = 0 @@ -78,7 +89,7 @@ def co2(): # for i in range(n): # print(1) # state.co1_count += 1 -# yield +# yield # Cmd1 = commandify(co1) # Cmd1(5).schedule() @@ -86,11 +97,14 @@ def co2(): # for _ in range(10): # scheduler.run() - + # assert state.co1_count == 5 + def test_commandify_decorator(scheduler: commands2.CommandScheduler): - class state: pass + class state: + pass + state.co1_count = 0 @commandify @@ -98,14 +112,16 @@ def Cmd1(n): for i in range(n): print(1) state.co1_count += 1 - yield + yield cmd = Cmd1(5) cmd.schedule() # Cmd1(5).schedule() for _ in range(10): scheduler.run() - + assert state.co1_count == 5 -def test_coroutine_command_lambda_pass_args(): ... \ No newline at end of file + +def test_coroutine_command_lambda_pass_args(): + ...