From e95386e23eb0345222c0a7197c38bc269bdfd2bb Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sat, 25 Jun 2022 23:54:49 -0500 Subject: [PATCH] jedi: improve signature information This fixes numerous issues where jedi provided incomplete or poorly formatted signatures and docstrings. Issue: https://github.com/pybricks/pybricks-code/issues/932 --- CHANGELOG.md | 5 + jedi/poetry.lock | 14 +- jedi/pyproject.toml | 1 + jedi/src/pybricks_jedi/__init__.py | 100 ++- jedi/tests/test_complete_inventor_hub.py | 191 +----- jedi/tests/test_get_signature.py | 779 +++++++++++++++++++++++ src/pybricks/_common.py | 93 ++- src/pybricks/geometry.py | 27 +- src/pybricks/hubs.py | 10 +- src/pybricks/pupdevices.py | 155 ++++- src/pybricks/robotics.py | 40 +- src/pybricks/tools.py | 16 +- 12 files changed, 1131 insertions(+), 300 deletions(-) create mode 100644 jedi/tests/test_get_signature.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fdc723..df4b246 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ +## Unreleased + +### Fixed +- Fixed more type hints and improved compatibility with jedi. + ## 3.2.0b1-r2 - 2022-06-24 ### Changed diff --git a/jedi/poetry.lock b/jedi/poetry.lock index 2435601..285d76c 100644 --- a/jedi/poetry.lock +++ b/jedi/poetry.lock @@ -60,6 +60,14 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "docstring-parser" +version = "0.14.1" +description = "Parse Python docstrings in reST, Google and Numpydoc format" +category = "main" +optional = false +python-versions = ">=3.6,<4.0" + [[package]] name = "flake8" version = "4.0.1" @@ -255,7 +263,7 @@ python-versions = ">=3.7" [metadata] lock-version = "1.1" python-versions = ">= 3.10, < 3.11" -content-hash = "9b91afcb8ccd46cc8be150f48170e45e86a6c65686d7e6e97ac3de67988c0ff5" +content-hash = "207f86e378bd5ea0d9751f27aa1b8eff5709ccce56851eeede184a4c343b8ac1" [metadata.files] atomicwrites = [ @@ -299,6 +307,10 @@ colorama = [ {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] +docstring-parser = [ + {file = "docstring_parser-0.14.1-py3-none-any.whl", hash = "sha256:14ac6ec1f1ba6905c4d8cb90fd0bc55394f5678183752c90e44812bf28d7a515"}, + {file = "docstring_parser-0.14.1.tar.gz", hash = "sha256:2c77522e31b7c88b1ab457a1f3c9ae38947ad719732260ba77ee8a3deb58622a"}, +] flake8 = [ {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, diff --git a/jedi/pyproject.toml b/jedi/pyproject.toml index cbd7376..2fbfad0 100644 --- a/jedi/pyproject.toml +++ b/jedi/pyproject.toml @@ -10,6 +10,7 @@ python = ">= 3.10, < 3.11" pybricks = "^3.2.0b1-r2" jedi = "^0.18.1" typing-extensions = "^4.2.0" +docstring-parser = "0.14.1" [tool.poetry.dev-dependencies] pytest = "^7.1.2" diff --git a/jedi/src/pybricks_jedi/__init__.py b/jedi/src/pybricks_jedi/__init__.py index 0570caf..2ad3b6f 100644 --- a/jedi/src/pybricks_jedi/__init__.py +++ b/jedi/src/pybricks_jedi/__init__.py @@ -1,8 +1,12 @@ -from enum import IntEnum +import io import json -from typing_extensions import TypedDict, NotRequired +import re +from enum import IntEnum + +import docstring_parser import jedi -from jedi.api.classes import Completion, Signature, ParamName +from jedi.api.classes import BaseName, Completion, Name, ParamName, Signature +from typing_extensions import NotRequired, TypedDict # Packages included in Pybricks firmware that ships with Pybricks Code. PYBRICKS_CODE_PACKAGES = { @@ -132,6 +136,23 @@ class Command(TypedDict): arguments: NotRequired[list] +class UriComponents(TypedDict): + scheme: str + authority: str + path: str + query: str + fragment: str + + +class IMarkdownString(TypedDict): + value: str + isTrusted: NotRequired[bool] + supportThemeIcons: NotRequired[bool] + supportHtml: NotRequired[bool] + baseUri: NotRequired[UriComponents] + uris: NotRequired[dict[str, UriComponents]] + + class CompletionItemKind(IntEnum): Method = 0 Function = 1 @@ -251,6 +272,66 @@ def _is_pybricks(c: Completion) -> bool: return True +def _get_docstring(name: BaseName) -> str: + """ + Gets the docstring for a name. + """ + + docstring = name.docstring(raw=True) + + # jedi does not appear to be smart enough to use __init__ docstring for class + if name.type == "class" and isinstance(name, Name): + n: Name + for n in name.defined_names(): + if n.name == "__init__": + docstring = "\n".join([docstring, _get_docstring(n)]) + + return docstring + + +def _parse_docstring(text: str) -> tuple[IMarkdownString, list[IMarkdownString]]: + """ + Parses a doc string, removes the overload declarations, performs some + fixups and extracts the individual parameter strings. + + Args: + The raw docstring. + + Returns: + A tuple with the fixed up doc string and a list of parameter doc strings. + """ + # docstring_parser does not support signatures at the beginning of the + # docstring, so we have to remove them + # https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#confval-autodoc_docstring_signature + + lines, end_of_signatures = [], False + + for line in io.StringIO(text).readlines(): + # signatures look like: "name(params...)" + if not end_of_signatures and re.match(r"^\w+\(.*\)", line): + continue + + end_of_signatures = True + + # TODO: we may want to do some restructured text to markdown fixes, + # e.g. strip off ":class:" from ":class:`SomeClass`" and replace + # ".. some-directive::" with an appropriate header + + lines.append(line) + + text = "".join(lines) + + doc = docstring_parser.parse(text, docstring_parser.DocstringStyle.GOOGLE) + + # convert to numpy doc for better markdown rendering (section names are underlined) + numpy_doc = docstring_parser.compose(doc, docstring_parser.Style.NUMPYDOC) + + docstring = IMarkdownString(value=numpy_doc) + param_docstrings = [IMarkdownString(value=p.description) for p in doc.params] + + return docstring, param_docstrings + + def _map_completion_kind(type: str) -> CompletionItemKind: match type: case "module": @@ -294,22 +375,23 @@ def _map_completion_item( endLineNumber=line, endColumn=column, ), - documentation=completion.docstring(), + documentation=_parse_docstring(_get_docstring(completion))[0], ) -def _map_parameter(param: ParamName) -> ParameterInformation: - # NB: it is not possible to get docstring for individual parameters from jedi - return ParameterInformation(label=param.to_string()) +def _map_parameter(param: ParamName, docstr: str) -> ParameterInformation: + return ParameterInformation(label=param.to_string(), documentation=docstr) def _map_signature(signature: Signature) -> SignatureInformation: optional = {} if signature.index is None else dict(activeParameter=signature.index) + docstr, param_docstr = _parse_docstring(_get_docstring(signature)) + return SignatureInformation( label=signature.to_string(), - documentation=signature.docstring(), - parameters=[_map_parameter(p) for p in signature.params], + documentation=docstr, + parameters=[_map_parameter(*p) for p in zip(signature.params, param_docstr)], **optional, ) diff --git a/jedi/tests/test_complete_inventor_hub.py b/jedi/tests/test_complete_inventor_hub.py index 6868497..2e0b4a9 100644 --- a/jedi/tests/test_complete_inventor_hub.py +++ b/jedi/tests/test_complete_inventor_hub.py @@ -7,7 +7,7 @@ Tests for correct code completion of the InventorHub class. import json -from pybricks_jedi import SignatureHelp, complete, get_signatures, CompletionItem +from pybricks_jedi import complete, CompletionItem IMPORT = "from pybricks.hubs import InventorHub" CREATE_INSTANCE = "hub = InventorHub()" @@ -53,22 +53,6 @@ def test_hub_dot_battery_dot(): ] -def test_hub_dot_battery_dot_current(): - line = "hub.battery.current(" - code = _create_snippet(line) - signatures: SignatureHelp = json.loads(get_signatures(code, 3, len(line) + 1)) - assert [s["label"] for s in signatures["signatures"]] == ["current() -> int"] - assert [s.get("activeParameter") for s in signatures["signatures"]] == [None] - - -def test_hub_dot_battery_dot_voltage(): - line = "hub.battery.voltage(" - code = _create_snippet(line) - signatures: SignatureHelp = json.loads(get_signatures(code, 3, len(line) + 1)) - assert [s["label"] for s in signatures["signatures"]] == ["voltage() -> int"] - assert [s.get("activeParameter") for s in signatures["signatures"]] == [None] - - def test_hub_dot_buttons_dot(): line = "hub.buttons." code = _create_snippet(line) @@ -78,16 +62,6 @@ def test_hub_dot_buttons_dot(): ] -def test_hub_dot_battery_dot_pressed(): - line = "hub.buttons.pressed(" - code = _create_snippet(line) - signatures: SignatureHelp = json.loads(get_signatures(code, 3, len(line) + 1)) - assert [s["label"] for s in signatures["signatures"]] == [ - "pressed() -> Tuple[Button]" - ] - assert [s.get("activeParameter") for s in signatures["signatures"]] == [None] - - def test_hub_dot_charger_dot(): line = "hub.charger." code = _create_snippet(line) @@ -99,22 +73,6 @@ def test_hub_dot_charger_dot(): ] -def test_hub_dot_charger_dot_connected(): - line = "hub.charger.connected(" - code = _create_snippet(line) - signatures: SignatureHelp = json.loads(get_signatures(code, 3, len(line) + 1)) - assert [s["label"] for s in signatures["signatures"]] == ["connected() -> bool"] - assert [s.get("activeParameter") for s in signatures["signatures"]] == [None] - - -def test_hub_dot_charger_dot_current(): - line = "hub.charger.current(" - code = _create_snippet(line) - signatures: SignatureHelp = json.loads(get_signatures(code, 3, len(line) + 1)) - assert [s["label"] for s in signatures["signatures"]] == ["current() -> int"] - assert [s.get("activeParameter") for s in signatures["signatures"]] == [None] - - def test_hub_dot_display_dot(): line = "hub.display." code = _create_snippet(line) @@ -157,62 +115,6 @@ def test_hub_dot_light_dot(): ] -def test_hub_dot_light_dot_animate(): - line = "hub.light.animate(" - code = _create_snippet(line) - signatures: SignatureHelp = json.loads(get_signatures(code, 3, len(line) + 1)) - assert [s["label"] for s in signatures["signatures"]] == [ - "animate(colors: Collection[Color], interval: Number) -> None", - ] - assert [s.get("activeParameter") for s in signatures["signatures"]] == [0] - assert [ - [p["label"] for p in s["parameters"]] for s in signatures["signatures"] - ] == [["colors: Collection[Color]", "interval: Number"]] - - -def test_hub_dot_light_dot_animate2(): - line = "hub.light.animate([]," - code = _create_snippet(line) - signatures: SignatureHelp = json.loads(get_signatures(code, 3, len(line) + 1)) - assert [s["label"] for s in signatures["signatures"]] == [ - "animate(colors: Collection[Color], interval: Number) -> None", - ] - assert [s.get("activeParameter") for s in signatures["signatures"]] == [1] - assert [ - [p["label"] for p in s["parameters"]] for s in signatures["signatures"] - ] == [["colors: Collection[Color]", "interval: Number"]] - - -def test_hub_dot_light_dot_blink(): - line = "hub.light.blink(" - code = _create_snippet(line) - signatures: SignatureHelp = json.loads(get_signatures(code, 3, len(line) + 1)) - assert [s["label"] for s in signatures["signatures"]] == [ - "blink(color: Color, durations: Collection[int]) -> None", - ] - assert [s.get("activeParameter") for s in signatures["signatures"]] == [0] - - -def test_hub_dot_light_dot_blink2(): - line = "hub.light.blink(Color.RED," - code = _create_snippet(line) - signatures: SignatureHelp = json.loads(get_signatures(code, 3, len(line) + 1)) - assert [s["label"] for s in signatures["signatures"]] == [ - "blink(color: Color, durations: Collection[int]) -> None", - ] - assert [s.get("activeParameter") for s in signatures["signatures"]] == [1] - - -def test_hub_dot_light_dot_on(): - line = "hub.light.on(" - code = _create_snippet(line) - signatures: SignatureHelp = json.loads(get_signatures(code, 3, len(line) + 1)) - assert [s["label"] for s in signatures["signatures"]] == [ - "on(color: Color) -> None", - ] - assert [s.get("activeParameter") for s in signatures["signatures"]] == [0] - - def test_hub_dot_speaker_dot(): line = "hub.speaker." code = _create_snippet(line) @@ -224,57 +126,6 @@ def test_hub_dot_speaker_dot(): ] -def test_hub_dot_speaker_dot_beep(): - line = "hub.speaker.beep(" - code = _create_snippet(line) - signatures: SignatureHelp = json.loads(get_signatures(code, 3, len(line) + 1)) - assert [s["label"] for s in signatures["signatures"]] == [ - "beep(frequency: Number=500, duration: Number=100) -> None", - ] - assert [s.get("activeParameter") for s in signatures["signatures"]] == [0] - - -def test_hub_dot_speaker_dot_beep2(): - line = "hub.speaker.beep(100," - code = _create_snippet(line) - signatures: SignatureHelp = json.loads(get_signatures(code, 3, len(line) + 1)) - assert [s["label"] for s in signatures["signatures"]] == [ - "beep(frequency: Number=500, duration: Number=100) -> None", - ] - assert [s.get("activeParameter") for s in signatures["signatures"]] == [1] - - -def test_hub_dot_speaker_dot_play_notes(): - line = "hub.speaker.play_notes(" - code = _create_snippet(line) - signatures: SignatureHelp = json.loads(get_signatures(code, 3, len(line) + 1)) - assert [s["label"] for s in signatures["signatures"]] == [ - "play_notes(notes: Iterable[str], tempo: Number=120) -> None", - ] - assert [s.get("activeParameter") for s in signatures["signatures"]] == [0] - - -def test_hub_dot_speaker_dot_play_notes2(): - line = "hub.speaker.play_notes([]," - code = _create_snippet(line) - signatures: SignatureHelp = json.loads(get_signatures(code, 3, len(line) + 1)) - assert [s["label"] for s in signatures["signatures"]] == [ - "play_notes(notes: Iterable[str], tempo: Number=120) -> None", - ] - assert [s.get("activeParameter") for s in signatures["signatures"]] == [1] - - -def test_hub_dot_speaker_dot_volume(): - line = "hub.speaker.volume(" - code = _create_snippet(line) - signatures: SignatureHelp = json.loads(get_signatures(code, 3, len(line) + 1)) - assert [s["label"] for s in signatures["signatures"]] == [ - "volume() -> int", - "volume(volume: Number) -> None", - ] - assert [s.get("activeParameter") for s in signatures["signatures"]] == [None, 0] - - def test_hub_dot_system_dot(): line = "hub.system." code = _create_snippet(line) @@ -285,43 +136,3 @@ def test_hub_dot_system_dot(): "set_stop_button", "shutdown", ] - - -def test_hub_dot_system_dot_name(): - line = "hub.system.name(" - code = _create_snippet(line) - signatures: SignatureHelp = json.loads(get_signatures(code, 3, len(line) + 1)) - assert [s["label"] for s in signatures["signatures"]] == [ - "name() -> str", - ] - assert [s.get("activeParameter") for s in signatures["signatures"]] == [None] - - -def test_hub_dot_system_dot_reset_reason(): - line = "hub.system.reset_reason(" - code = _create_snippet(line) - signatures: SignatureHelp = json.loads(get_signatures(code, 3, len(line) + 1)) - assert [s["label"] for s in signatures["signatures"]] == [ - "reset_reason() -> int", - ] - assert [s.get("activeParameter") for s in signatures["signatures"]] == [None] - - -def test_hub_dot_system_dot_set_stop_button(): - line = "hub.system.set_stop_button(" - code = _create_snippet(line) - signatures: SignatureHelp = json.loads(get_signatures(code, 3, len(line) + 1)) - assert [s["label"] for s in signatures["signatures"]] == [ - "set_stop_button(button: Optional[Union[Button, Iterable[Button]]]) -> None", - ] - assert [s.get("activeParameter") for s in signatures["signatures"]] == [0] - - -def test_hub_dot_system_dot_shutdown(): - line = "hub.system.shutdown(" - code = _create_snippet(line) - signatures: SignatureHelp = json.loads(get_signatures(code, 3, len(line) + 1)) - assert [s["label"] for s in signatures["signatures"]] == [ - "shutdown() -> None", - ] - assert [s.get("activeParameter") for s in signatures["signatures"]] == [None] diff --git a/jedi/tests/test_get_signature.py b/jedi/tests/test_get_signature.py new file mode 100644 index 0000000..d99673f --- /dev/null +++ b/jedi/tests/test_get_signature.py @@ -0,0 +1,779 @@ +# SPDX-License-Identifier: MIT +# Copyright (c) 2022 The Pybricks Authors + +""" +Tests for correct signatures of the pupdevices.Motor class. +""" + + +from itertools import zip_longest +import json + +import pytest +from pybricks_jedi import SignatureHelp, get_signatures + + +def _get_function_signature(module: str, function: str) -> SignatureHelp: + """ + Gets the signature help object for code like:: + + from {module} import {function} + {function}( + + Args: + module: the module name + func: the function name (a function in the module) + """ + code = f"from {module} import {function}; {function}(" + return json.loads(get_signatures(code, 1, len(code) + 1)) + + +FUNCTION_PARAMS = [ + pytest.param("pybricks.tools", "wait", [(["time: Number"], "None")]), + pytest.param( + "pybricks.geometry", + "vector", + [ + (["x: float", "y: float"], "Matrix"), + (["x: float", "y: float", "z: float"], "Matrix"), + ], + ), +] + + +@pytest.mark.parametrize("module,function,signatures", FUNCTION_PARAMS) +def test_get_signature_for_functions( + module: str, function: str, signatures: list[tuple[list[str], str]] +): + help = _get_function_signature(module, function) + + assert help["activeSignature"] == 0 + assert help["activeParameter"] == 0 + assert help["signatures"] + + for sig, (params, returns) in zip_longest(help["signatures"], signatures): + assert sig["label"] == f"{function}({', '.join(params)}) -> {returns}" + assert sig["documentation"]["value"] + # ensure signatures are stripped from doc comment + assert not sig["documentation"]["value"].startswith(f"{function}(") + + for pi, p in zip_longest(sig["parameters"], params): + assert pi["label"] == p + assert pi["documentation"]["value"] + + +def _get_constructor_signature(module: str, type: str) -> SignatureHelp: + """ + Gets the signature help object for code like:: + + from {module} import {type} + instance = {type}( + + Args: + module: the module name + type: the type name (a type in the module) + """ + code = f"from {module} import {type}; {type}(" + return json.loads(get_signatures(code, 1, len(code) + 1)) + + +CONSTRUCTOR_PARAMS = [ + pytest.param("pybricks.hubs", "MoveHub", [[]]), + pytest.param("pybricks.hubs", "CityHub", [[]]), + pytest.param( + "pybricks.hubs", + "TechnicHub", + [["top_side: Axis=Axis.Z", "front_side: Axis=Axis.X"]], + ), + pytest.param( + "pybricks.hubs", + "PrimeHub", + [["top_side: Axis=Axis.Z", "front_side: Axis=Axis.X"]], + ), + pytest.param( + "pybricks.pupdevices", + "DCMotor", + [["port: Port", "positive_direction: Direction=Direction.CLOCKWISE"]], + ), + pytest.param( + "pybricks.pupdevices", + "Motor", + [ + [ + "port: Port", + "positive_direction: Direction=Direction.CLOCKWISE", + "gears: Optional[Union[Collection[int], Collection[Collection[int]]]]=None", + "reset_angle: bool=True", + ] + ], + ), + pytest.param("pybricks.pupdevices", "TiltSensor", [["port: Port"]]), + pytest.param("pybricks.pupdevices", "InfraredSensor", [["port: Port"]]), + pytest.param("pybricks.pupdevices", "ColorDistanceSensor", [["port: Port"]]), + pytest.param( + "pybricks.pupdevices", + "PFMotor", + [ + [ + "sensor: ColorDistanceSensor", + "channel: int", + "color: Color", + "positive_direction: Direction=Direction.CLOCKWISE", + ] + ], + ), + pytest.param("pybricks.pupdevices", "ColorSensor", [["port: Port"]]), + pytest.param("pybricks.pupdevices", "UltrasonicSensor", [["port: Port"]]), + pytest.param("pybricks.pupdevices", "ForceSensor", [["port: Port"]]), + pytest.param("pybricks.pupdevices", "ColorLightMatrix", [["port: Port"]]), + pytest.param("pybricks.pupdevices", "Light", [["port: Port"]]), + pytest.param( + "pybricks.pupdevices", + "Remote", + [["name: Optional[str]=None", "timeout: int=10000"]], + ), + # TODO: iodevices go here + pytest.param( + "pybricks.parameters", + "Color", + [["h: Number", "s: Number=100", "v: Number=100"]], + ), + pytest.param( + "pybricks.robotics", + "DriveBase", + [ + [ + "left_motor: Motor", + "right_motor: Motor", + "wheel_diameter: Number", + "axle_track: Number", + ] + ], + ), + pytest.param( + "pybricks.geometry", + "Matrix", + [["rows: Sequence[Sequence[float]]"]], + ), +] + + +@pytest.mark.parametrize("module,type,signatures", CONSTRUCTOR_PARAMS) +def test_get_signature_for_constructors( + module: str, type: str, signatures: list[list[str]] +): + help = _get_constructor_signature(module, type) + + assert help["activeSignature"] == 0 + assert help["activeParameter"] == 0 + assert help["signatures"] + + for sig, params in zip_longest(help["signatures"], signatures): + assert sig["label"] == f"{type}({', '.join(params)})" + assert sig["documentation"]["value"] + # ensure signatures are stripped from doc comment + assert not sig["documentation"]["value"].startswith(f"{type}(") + + for pi, p in zip_longest(sig["parameters"], params): + assert pi["label"] == p + assert pi["documentation"]["value"] + + +def _get_method_signature(module: str, type: str, method: str) -> SignatureHelp: + """ + Gets the signature help object for code like:: + + from {module} import {type} + instance = {type}() + instance.{method}( + + Args: + module: the module name + type: the type name (a type in the module) + method: the method name (a method of the type) + """ + code = f"from {module} import {type}; x = {type}(); x.{method}(" + return json.loads(get_signatures(code, 1, len(code) + 1)) + + +METHOD_PARAMS = [ + pytest.param("pybricks.hubs", "MoveHub", "light.on", [(["color: Color"], "None")]), + pytest.param("pybricks.hubs", "MoveHub", "light.off", [([], "None")]), + pytest.param( + "pybricks.hubs", + "MoveHub", + "light.blink", + [(["color: Color", "durations: Collection[Number]"], "None")], + ), + pytest.param( + "pybricks.hubs", + "MoveHub", + "light.animate", + [(["colors: Collection[Color]", "interval: Number"], "None")], + ), + pytest.param("pybricks.hubs", "MoveHub", "imu.up", [([], "Side")]), + pytest.param( + "pybricks.hubs", "MoveHub", "imu.acceleration", [([], "Tuple[int, int, int]")] + ), + pytest.param("pybricks.hubs", "MoveHub", "battery.voltage", [([], "int")]), + pytest.param("pybricks.hubs", "MoveHub", "battery.current", [([], "int")]), + pytest.param( + "pybricks.hubs", "MoveHub", "button.pressed", [([], "Collection[Button]")] + ), + pytest.param( + "pybricks.hubs", + "MoveHub", + "system.set_stop_button", + [(["button: Optional[Union[Button, Iterable[Button]]]"], "None")], + ), + pytest.param("pybricks.hubs", "MoveHub", "system.name", [([], "str")]), + pytest.param("pybricks.hubs", "MoveHub", "system.shutdown", [([], "None")]), + pytest.param("pybricks.hubs", "MoveHub", "system.reset_reason", [([], "int")]), + pytest.param("pybricks.hubs", "CityHub", "light.on", [(["color: Color"], "None")]), + pytest.param("pybricks.hubs", "CityHub", "light.off", [([], "None")]), + pytest.param( + "pybricks.hubs", + "CityHub", + "light.blink", + [(["color: Color", "durations: Collection[Number]"], "None")], + ), + pytest.param( + "pybricks.hubs", + "CityHub", + "light.animate", + [(["colors: Collection[Color]", "interval: Number"], "None")], + ), + pytest.param("pybricks.hubs", "CityHub", "battery.voltage", [([], "int")]), + pytest.param("pybricks.hubs", "CityHub", "battery.current", [([], "int")]), + pytest.param( + "pybricks.hubs", "CityHub", "button.pressed", [([], "Collection[Button]")] + ), + pytest.param( + "pybricks.hubs", + "CityHub", + "system.set_stop_button", + [(["button: Optional[Union[Button, Iterable[Button]]]"], "None")], + ), + pytest.param("pybricks.hubs", "CityHub", "system.name", [([], "str")]), + pytest.param("pybricks.hubs", "CityHub", "system.shutdown", [([], "None")]), + pytest.param("pybricks.hubs", "CityHub", "system.reset_reason", [([], "int")]), + pytest.param( + "pybricks.hubs", "TechnicHub", "light.on", [(["color: Color"], "None")] + ), + pytest.param("pybricks.hubs", "TechnicHub", "light.off", [([], "None")]), + pytest.param( + "pybricks.hubs", + "TechnicHub", + "light.blink", + [(["color: Color", "durations: Collection[Number]"], "None")], + ), + pytest.param( + "pybricks.hubs", + "TechnicHub", + "light.animate", + [(["colors: Collection[Color]", "interval: Number"], "None")], + ), + pytest.param("pybricks.hubs", "TechnicHub", "imu.up", [([], "Side")]), + pytest.param("pybricks.hubs", "TechnicHub", "imu.tilt", [([], "Tuple[int, int]")]), + pytest.param( + "pybricks.hubs", + "TechnicHub", + "imu.acceleration", + [(["axis: Axis"], "float"), ([], "Matrix")], + ), + pytest.param( + "pybricks.hubs", + "TechnicHub", + "imu.angular_velocity", + [(["axis: Axis"], "float"), ([], "Matrix")], + ), + pytest.param("pybricks.hubs", "TechnicHub", "imu.heading", [([], "float")]), + pytest.param( + "pybricks.hubs", + "TechnicHub", + "imu.reset_heading", + [(["angle: Number"], "None")], + ), + pytest.param("pybricks.hubs", "TechnicHub", "battery.voltage", [([], "int")]), + pytest.param("pybricks.hubs", "TechnicHub", "battery.current", [([], "int")]), + pytest.param( + "pybricks.hubs", "TechnicHub", "button.pressed", [([], "Collection[Button]")] + ), + pytest.param( + "pybricks.hubs", + "TechnicHub", + "system.set_stop_button", + [(["button: Optional[Union[Button, Iterable[Button]]]"], "None")], + ), + pytest.param("pybricks.hubs", "TechnicHub", "system.name", [([], "str")]), + pytest.param("pybricks.hubs", "TechnicHub", "system.shutdown", [([], "None")]), + pytest.param("pybricks.hubs", "TechnicHub", "system.reset_reason", [([], "int")]), + pytest.param("pybricks.hubs", "PrimeHub", "light.on", [(["color: Color"], "None")]), + pytest.param("pybricks.hubs", "PrimeHub", "light.off", [([], "None")]), + pytest.param( + "pybricks.hubs", + "PrimeHub", + "light.blink", + [(["color: Color", "durations: Collection[Number]"], "None")], + ), + pytest.param( + "pybricks.hubs", + "PrimeHub", + "light.animate", + [(["colors: Collection[Color]", "interval: Number"], "None")], + ), + pytest.param( + "pybricks.hubs", "PrimeHub", "display.orientation", [(["up: Side"], "None")] + ), + pytest.param("pybricks.hubs", "PrimeHub", "display.off", [([], "None")]), + pytest.param( + "pybricks.hubs", + "PrimeHub", + "display.pixel", + [(["row: Number", "column: Number", "brightness: Number=100"], "None")], + ), + pytest.param( + "pybricks.hubs", "PrimeHub", "display.image", [(["matrix: Matrix"], "None")] + ), + pytest.param( + "pybricks.hubs", + "PrimeHub", + "display.animate", + [(["matrices: Collection[Matrix]", "interval: Number"], "None")], + ), + pytest.param( + "pybricks.hubs", "PrimeHub", "display.number", [(["number: Number"], "None")] + ), + pytest.param( + "pybricks.hubs", "PrimeHub", "display.char", [(["char: str"], "None")] + ), + pytest.param( + "pybricks.hubs", + "PrimeHub", + "display.text", + [(["text: str", "on: Number=500", "off: Number=50"], "None")], + ), + pytest.param( + "pybricks.hubs", "PrimeHub", "buttons.pressed", [([], "Collection[Button]")] + ), + pytest.param("pybricks.hubs", "PrimeHub", "imu.up", [([], "Side")]), + pytest.param("pybricks.hubs", "PrimeHub", "imu.tilt", [([], "Tuple[int, int]")]), + pytest.param( + "pybricks.hubs", + "PrimeHub", + "imu.acceleration", + [(["axis: Axis"], "float"), ([], "Matrix")], + ), + pytest.param( + "pybricks.hubs", + "PrimeHub", + "imu.angular_velocity", + [(["axis: Axis"], "float"), ([], "Matrix")], + ), + pytest.param("pybricks.hubs", "PrimeHub", "imu.heading", [([], "float")]), + pytest.param( + "pybricks.hubs", + "PrimeHub", + "imu.reset_heading", + [(["angle: Number"], "None")], + ), + pytest.param( + "pybricks.hubs", + "PrimeHub", + "speaker.volume", + [(["volume: Number"], "None"), ([], "int")], + ), + pytest.param( + "pybricks.hubs", + "PrimeHub", + "speaker.beep", + [(["frequency: Number=500", "duration: Number=100"], "None")], + ), + pytest.param( + "pybricks.hubs", + "PrimeHub", + "speaker.play_notes", + [(["notes: Iterable[str]", "tempo: Number=120"], "None")], + ), + pytest.param("pybricks.hubs", "PrimeHub", "battery.voltage", [([], "int")]), + pytest.param("pybricks.hubs", "PrimeHub", "battery.current", [([], "int")]), + pytest.param("pybricks.hubs", "PrimeHub", "charger.connected", [([], "bool")]), + pytest.param("pybricks.hubs", "PrimeHub", "charger.current", [([], "int")]), + pytest.param("pybricks.hubs", "PrimeHub", "charger.status", [([], "int")]), + pytest.param( + "pybricks.hubs", + "PrimeHub", + "system.set_stop_button", + [(["button: Optional[Union[Button, Iterable[Button]]]"], "None")], + ), + pytest.param("pybricks.hubs", "PrimeHub", "system.name", [([], "str")]), + pytest.param("pybricks.hubs", "PrimeHub", "system.shutdown", [([], "None")]), + pytest.param("pybricks.hubs", "PrimeHub", "system.reset_reason", [([], "int")]), + # TODO: iodevices module here + pytest.param("pybricks.pupdevices", "DCMotor", "dc", [(["duty: Number"], "None")]), + pytest.param("pybricks.pupdevices", "DCMotor", "stop", [([], "None")]), + pytest.param("pybricks.pupdevices", "DCMotor", "brake", [([], "None")]), + pytest.param( + "pybricks.pupdevices", + "DCMotor", + "settings", + [(["max_voltage: Number"], "None"), ([], "Tuple[int]")], + ), + pytest.param("pybricks.pupdevices", "Motor", "speed", [([], "int")]), + pytest.param("pybricks.pupdevices", "Motor", "angle", [([], "int")]), + pytest.param( + "pybricks.pupdevices", + "Motor", + "reset_angle", + [(["angle: Optional[Number]=None"], "None")], + ), + pytest.param("pybricks.pupdevices", "Motor", "stop", [([], "None")]), + pytest.param("pybricks.pupdevices", "Motor", "brake", [([], "None")]), + pytest.param("pybricks.pupdevices", "Motor", "hold", [([], "None")]), + pytest.param("pybricks.pupdevices", "Motor", "run", [(["speed: Number"], "None")]), + pytest.param( + "pybricks.pupdevices", + "Motor", + "run_time", + [ + ( + [ + "speed: Number", + "time: Number", + "then: Stop=Stop.HOLD", + "wait: bool=True", + ], + "None", + ) + ], + ), + pytest.param( + "pybricks.pupdevices", + "Motor", + "run_angle", + [ + ( + [ + "speed: Number", + "rotation_angle: Number", + "then: Stop=Stop.HOLD", + "wait: bool=True", + ], + "None", + ) + ], + ), + pytest.param( + "pybricks.pupdevices", + "Motor", + "run_target", + [ + ( + [ + "speed: Number", + "target_angle: Number", + "then: Stop=Stop.HOLD", + "wait: bool=True", + ], + "None", + ) + ], + ), + pytest.param( + "pybricks.pupdevices", + "Motor", + "track_target", + [(["target_angle: Number"], "None")], + ), + pytest.param( + "pybricks.pupdevices", + "Motor", + "run_until_stalled", + [ + ( + [ + "speed: Number", + "then: Stop=Stop.COAST", + "duty_limit: Optional[Number]=None", + ], + "int", + ) + ], + ), + pytest.param("pybricks.pupdevices", "Motor", "dc", [(["duty: Number"], "None")]), + pytest.param("pybricks.pupdevices", "Motor", "control.done", [([], "bool")]), + pytest.param("pybricks.pupdevices", "Motor", "control.stalled", [([], "bool")]), + pytest.param("pybricks.pupdevices", "Motor", "control.load", [([], "int")]), + pytest.param( + "pybricks.pupdevices", + "Motor", + "settings", + [(["max_voltage: Number"], "None"), ([], "Tuple[int]")], + ), + pytest.param( + "pybricks.pupdevices", + "Motor", + "control.limits", + [ + ( + [ + "speed: Optional[Number]=None", + "acceleration: Optional[Number]=None", + "torque: Optional[Number]=None", + ], + "None", + ), + ([], "Tuple[int, int, int]"), + ], + ), + pytest.param( + "pybricks.pupdevices", + "Motor", + "control.pid", + [ + ( + [ + "kp: Optional[Number]=None", + "ki: Optional[Number]=None", + "kd: Optional[Number]=None", + "reserved: Optional[Number]=None", + "integral_rate: Optional[Number]=None", + ], + "None", + ), + ([], "Tuple[int, int, int, None, int]"), + ], + ), + pytest.param( + "pybricks.pupdevices", + "Motor", + "control.target_tolerances", + [ + ( + [ + "speed: Optional[Number]=None", + "position: Optional[Number]=None", + ], + "None", + ), + ([], "Tuple[int, int]"), + ], + ), + pytest.param( + "pybricks.pupdevices", + "Motor", + "control.stall_tolerances", + [ + ( + [ + "speed: Optional[Number]=None", + "time: Optional[Number]=None", + ], + "None", + ), + ([], "Tuple[int, int]"), + ], + ), + pytest.param( + "pybricks.pupdevices", "TiltSensor", "tilt", [([], "Tuple[int, int]")] + ), + pytest.param("pybricks.pupdevices", "InfraredSensor", "distance", [([], "int")]), + pytest.param("pybricks.pupdevices", "InfraredSensor", "reflection", [([], "int")]), + pytest.param("pybricks.pupdevices", "InfraredSensor", "count", [([], "int")]), + pytest.param( + "pybricks.pupdevices", "ColorDistanceSensor", "color", [([], "Color")] + ), + pytest.param( + "pybricks.pupdevices", "ColorDistanceSensor", "reflection", [([], "int")] + ), + pytest.param( + "pybricks.pupdevices", "ColorDistanceSensor", "ambient", [([], "int")] + ), + pytest.param( + "pybricks.pupdevices", "ColorDistanceSensor", "distance", [([], "int")] + ), + pytest.param("pybricks.pupdevices", "ColorDistanceSensor", "hsv", [([], "Color")]), + pytest.param( + "pybricks.pupdevices", + "ColorDistanceSensor", + "detectable_colors", + [(["colors: Collection[Color]"], "None"), ([], "Collection[Color]")], + ), + pytest.param( + "pybricks.pupdevices", + "ColorDistanceSensor", + "light.on", + [(["color: Color"], "None")], + ), + pytest.param( + "pybricks.pupdevices", "ColorDistanceSensor", "light.off", [([], "None")] + ), + pytest.param("pybricks.pupdevices", "PFMotor", "dc", [(["duty: Number"], "None")]), + pytest.param("pybricks.pupdevices", "PFMotor", "stop", [([], "None")]), + pytest.param("pybricks.pupdevices", "PFMotor", "brake", [([], "None")]), + pytest.param( + "pybricks.pupdevices", + "ColorSensor", + "color", + [(["surface: bool=True"], "Optional[Color]")], + ), + pytest.param("pybricks.pupdevices", "ColorSensor", "reflection", [([], "int")]), + pytest.param("pybricks.pupdevices", "ColorSensor", "ambient", [([], "int")]), + pytest.param( + "pybricks.pupdevices", + "ColorSensor", + "hsv", + [(["surface: bool=True"], "Color")], + ), + pytest.param( + "pybricks.pupdevices", + "ColorSensor", + "detectable_colors", + [(["colors: Collection[Color]"], "None"), ([], "Collection[Color]")], + ), + pytest.param( + "pybricks.pupdevices", + "ColorSensor", + "lights.on", + [(["brightness: Union[Number, Tuple[Number, Number, Number]]"], "None")], + ), + pytest.param("pybricks.pupdevices", "ColorSensor", "lights.off", [([], "None")]), + pytest.param("pybricks.pupdevices", "UltrasonicSensor", "distance", [([], "int")]), + pytest.param("pybricks.pupdevices", "UltrasonicSensor", "presence", [([], "bool")]), + pytest.param( + "pybricks.pupdevices", + "UltrasonicSensor", + "lights.on", + [ + ( + ["brightness: Union[Number, Tuple[Number, Number, Number, Number]]"], + "None", + ) + ], + ), + pytest.param( + "pybricks.pupdevices", "UltrasonicSensor", "lights.off", [([], "None")] + ), + pytest.param("pybricks.pupdevices", "ForceSensor", "force", [([], "float")]), + pytest.param("pybricks.pupdevices", "ForceSensor", "distance", [([], "float")]), + pytest.param( + "pybricks.pupdevices", + "ForceSensor", + "pressed", + [(["force: Number=3"], "bool")], + ), + pytest.param("pybricks.pupdevices", "ForceSensor", "touched", [([], "bool")]), + pytest.param( + "pybricks.pupdevices", + "ColorLightMatrix", + "on", + [(["color: Union[Color, Collection[Color]]"], "None")], + ), + pytest.param("pybricks.pupdevices", "ColorLightMatrix", "off", [([], "None")]), + pytest.param( + "pybricks.pupdevices", "Light", "on", [(["brightness: Number=100"], "None")] + ), + pytest.param("pybricks.pupdevices", "Light", "off", [([], "None")]), + pytest.param( + "pybricks.pupdevices", + "Remote", + "name", + [(["name: str"], "None"), ([], "str")], + ), + pytest.param( + "pybricks.pupdevices", "Remote", "light.on", [(["color: Color"], "None")] + ), + pytest.param("pybricks.pupdevices", "Remote", "light.off", [([], "None")]), + pytest.param( + "pybricks.pupdevices", + "Remote", + "buttons.pressed", + [([], "Collection[Button]")], + ), + pytest.param("pybricks.tools", "StopWatch", "time", [([], "int")]), + pytest.param("pybricks.tools", "StopWatch", "pause", [([], "None")]), + pytest.param("pybricks.tools", "StopWatch", "resume", [([], "None")]), + pytest.param("pybricks.tools", "StopWatch", "reset", [([], "None")]), + pytest.param( + "pybricks.robotics", + "DriveBase", + "straight", + [(["distance: Number", "then: Stop=Stop.HOLD", "wait: bool=True"], "None")], + ), + pytest.param( + "pybricks.robotics", + "DriveBase", + "turn", + [(["angle: Number", "then: Stop=Stop.HOLD", "wait: bool=True"], "None")], + ), + pytest.param( + "pybricks.robotics", + "DriveBase", + "curve", + [ + ( + [ + "radius: Number", + "angle: Number", + "then: Stop=Stop.HOLD", + "wait: bool=True", + ], + "None", + ) + ], + ), + pytest.param( + "pybricks.robotics", + "DriveBase", + "settings", + [ + ( + [ + "straight_speed: Optional[Number]=None", + "straight_acceleration: Optional[Number]=None", + "turn_rate: Optional[Number]=None", + "turn_acceleration: Optional[Number]=None", + ], + "None", + ), + ([], "Tuple[int, int, int, int]"), + ], + ), + pytest.param( + "pybricks.robotics", + "DriveBase", + "drive", + [(["speed: Number", "turn_rate: Number"], "None")], + ), + pytest.param("pybricks.robotics", "DriveBase", "stop", [([], "None")]), + pytest.param("pybricks.robotics", "DriveBase", "distance", [([], "int")]), + pytest.param("pybricks.robotics", "DriveBase", "angle", [([], "int")]), + pytest.param( + "pybricks.robotics", "DriveBase", "state", [([], "Tuple[int, int, int, int]")] + ), + pytest.param("pybricks.robotics", "DriveBase", "reset", [([], "None")]), +] + + +@pytest.mark.parametrize("module,type,method,signatures", METHOD_PARAMS) +def test_get_signature_for_methods( + module: str, type: str, method: str, signatures: list[tuple[list[str], str]] +): + help = _get_method_signature(module, type, method) + + # strip method now that code has been generated (may have leading subcomponent) + method = method.split(".")[-1] + + assert help["activeSignature"] == 0 + assert help["activeParameter"] == 0 + assert help["signatures"] + + for sig, (params, returns) in zip_longest(help["signatures"], signatures): + assert sig["label"] == f"{method}({', '.join(params)}) -> {returns}" + assert sig["documentation"]["value"] + # ensure signatures are stripped from doc comment + assert not sig["documentation"]["value"].startswith(f"{method}(") + + for pi, p in zip_longest(sig["parameters"], params): + assert pi["label"] == p + assert pi["documentation"]["value"] diff --git a/src/pybricks/_common.py b/src/pybricks/_common.py index cb3df5e..7861dbd 100644 --- a/src/pybricks/_common.py +++ b/src/pybricks/_common.py @@ -107,7 +107,7 @@ class DCMotor: is generated while the motor is still moving.""" @overload - def settings(self, max_voltage: Optional[int] = None) -> None: + def settings(self, max_voltage: Number) -> None: ... @overload @@ -140,16 +140,16 @@ class Control: """ @overload - def limits(self) -> Tuple[int, int, int]: + def limits( + self, + speed: Optional[Number] = None, + acceleration: Optional[Number] = None, + torque: Optional[Number] = None, + ) -> None: ... @overload - def limits( - self, - speed: Optional[int] = None, - acceleration: Optional[int] = None, - torque: Optional[int] = None, - ) -> None: + def limits(self) -> Tuple[int, int, int]: ... def limits(self, *args): @@ -173,18 +173,18 @@ class Control: """ @overload - def pid(self) -> Tuple[int, int, int, None, int]: + def pid( + self, + kp: Optional[Number] = None, + ki: Optional[Number] = None, + kd: Optional[Number] = None, + reserved: Optional[Number] = None, + integral_rate: Optional[Number] = None, + ) -> None: ... @overload - def pid( - self, - kp: Optional[int] = None, - ki: Optional[int] = None, - kd: Optional[int] = None, - reserved: Optional[int] = None, - integral_rate: Optional[int] = None, - ) -> None: + def pid(self) -> Tuple[int, int, int, None, int]: ... def pid(self, *args): @@ -210,13 +210,13 @@ class Control: """ @overload - def target_tolerances(self) -> Tuple[int, int]: + def target_tolerances( + self, speed: Optional[Number] = None, position: Optional[Number] = None + ) -> None: ... @overload - def target_tolerances( - self, speed: Optional[int] = None, position: Optional[int] = None - ) -> None: + def target_tolerances(self) -> Tuple[int, int]: ... def target_tolerances(self, *args): @@ -236,13 +236,13 @@ class Control: """ @overload - def stall_tolerances(self) -> Tuple[int, int]: + def stall_tolerances( + self, speed: Optional[Number] = None, time: Optional[Number] = None + ) -> None: ... @overload - def stall_tolerances( - self, speed: Optional[int] = None, time: Optional[int] = None - ) -> None: + def stall_tolerances(self) -> Tuple[int, int]: ... def stall_tolerances(self, speed, time): @@ -356,7 +356,7 @@ class Motor(DCMotor): """ - def reset_angle(self, angle: Number) -> None: + def reset_angle(self, angle: Optional[Number]) -> None: """ reset_angle(angle) @@ -485,11 +485,11 @@ class Speaker: """Plays beeps and sounds using a speaker.""" @overload - def volume(self) -> int: + def volume(self, volume: Number) -> None: ... @overload - def volume(self, volume: Number) -> None: + def volume(self) -> int: ... def volume(self, *args): @@ -570,7 +570,7 @@ class ColorLight: Turns off the light.""" - def blink(self, color: Color, durations: Collection[int]) -> None: + def blink(self, color: Color, durations: Collection[Number]) -> None: """blink(color, durations) Blinks the light at a given color by turning it on and off for given @@ -599,7 +599,7 @@ class ColorLight: keeps running. When the animation completes, it repeats. Arguments: - colors (iter): Sequence of :class:`Color <.parameters.Color>` + colors (list): Sequence of :class:`Color <.parameters.Color>` values. interval (Number, ms): Time between color updates. """ @@ -617,7 +617,7 @@ class LightArray: n (int): Number of lights """ - def on(self, brightness: Union[int, Collection[int]]) -> None: + def on(self, brightness: Union[Number, Collection[Number]]) -> None: """on(brightness) Turns on the lights at the specified brightness. @@ -688,15 +688,15 @@ class LightMatrix: interval (Number, ms): Time to display each image in the list. """ - def pixel(self, row: int, column: int, brightness: Number = 100) -> None: + def pixel(self, row: Number, column: Number, brightness: Number = 100) -> None: """pixel(row, column, brightness=100) Turns on one pixel at the specified brightness. Arguments: - row (int): Vertical grid index, starting at 0 from the top. - column (int): Horizontal grid index, starting at 0 from the left. - brightness (:ref:`brightness`): Brightness of the pixel. + row (Number): Vertical grid index, starting at 0 from the top. + column (Number): Horizontal grid index, starting at 0 from the left. + brightness (Number :ref:`brightness`): Brightness of the pixel. """ def off(self) -> None: @@ -749,13 +749,13 @@ class Keypad: def __init__(self, active_buttons): ... - def pressed(self) -> Tuple[Button]: - """pressed() -> Tuple[Button] + def pressed(self) -> Collection[Button]: + """pressed() -> Collection[Button] Checks which buttons are currently pressed. Returns: - Tuple of pressed buttons. + Set of pressed buttons. """ @@ -846,11 +846,11 @@ class Accelerometer(SimpleAccelerometer): """Get measurements from an accelerometer.""" @overload - def acceleration(self) -> Matrix: + def acceleration(self, axis: Axis) -> float: ... @overload - def acceleration(self, axis: Axis) -> float: + def acceleration(self) -> Matrix: ... def acceleration(self, *args): @@ -915,11 +915,11 @@ class IMU(Accelerometer): """ @overload - def angular_velocity(self) -> Matrix: + def angular_velocity(self, axis: Axis) -> float: ... @overload - def angular_velocity(self, axis: Axis) -> float: + def angular_velocity(self) -> Matrix: ... def angular_velocity(self, *args): @@ -1003,13 +1003,13 @@ class CommonColorSensor: ... @overload - def detectable_colors(self) -> Tuple[Color]: + def detectable_colors(self) -> Collection[Color]: ... def detectable_colors(self, *args): """ detectable_colors(colors) - detectable_colors() -> Tuple[Color] + detectable_colors() -> Collection[Color] Configures which colors the ``color()`` method should detect. @@ -1017,11 +1017,10 @@ class CommonColorSensor: This way, the full-color measurements are rounded to the nearest desired color, and other colors are ignored. This improves reliability. - If you give no arguments, the currently chosen colors will be returned - as a tuple. + If you give no arguments, the currently chosen colors will be returned. Arguments: - colors (tuple): Tuple of :class:`Color <.parameters.Color>` + colors (list or tuple): List of :class:`Color <.parameters.Color>` objects: the colors that you want to detect. You can pick standard colors such as ``Color.MAGENTA``, or provide your own colors like ``Color(h=348, s=96, v=40)`` for even diff --git a/src/pybricks/geometry.py b/src/pybricks/geometry.py index 2527c85..9a07218 100644 --- a/src/pybricks/geometry.py +++ b/src/pybricks/geometry.py @@ -5,7 +5,7 @@ from __future__ import annotations -from typing import Tuple, Collection, overload +from typing import Sequence, Tuple, overload class Matrix: @@ -51,7 +51,7 @@ class Matrix: def __ifloordiv__(self, other) -> Matrix: ... - def __init__(self, rows: Collection[Collection[int]]): + def __init__(self, rows: Sequence[Sequence[float]]): """Matrix(rows) Arguments: @@ -73,12 +73,31 @@ class Matrix: @overload def vector(x: float, y: float) -> Matrix: - ... + """ + Convenience function to create a :class:`.Matrix` with the shape (``2``, ``1``). + + Arguments: + x (float): x-coordinate of the vector. + y (float): y-coordinate of the vector. + + Returns: + A matrix with the shape of a column vector. + """ @overload def vector(x: float, y: float, z: float) -> Matrix: - ... + """ + Convenience function to create a :class:`.Matrix` with the shape (``3``, ``1``). + + Arguments: + x (float): x-coordinate of the vector. + y (float): y-coordinate of the vector. + z (float): z-coordinate of the vector. + + Returns: + A matrix with the shape of a column vector. + """ def vector(*args): diff --git a/src/pybricks/hubs.py b/src/pybricks/hubs.py index 71d01af..5b4adb3 100644 --- a/src/pybricks/hubs.py +++ b/src/pybricks/hubs.py @@ -4,7 +4,7 @@ """LEGO® Programmable Hubs.""" from . import _common from .ev3dev import _speaker -from .geometry import Axis as _Axis +from .geometry import Axis from .media.ev3dev import Image as _Image from .parameters import Button as _Button @@ -63,7 +63,7 @@ class TechnicHub: system = _common.System() button = _common.Keypad([_Button.CENTER]) - def __init__(self, top_side: _Axis = _Axis.Z, front_side: _Axis = _Axis.X): + def __init__(self, top_side: Axis = Axis.Z, front_side: Axis = Axis.X): """TechnicHub(top_side=Axis.Z, front_side=Axis.X) Initializes the hub. Optionally, specify how the hub is @@ -100,7 +100,7 @@ class PrimeHub: imu = _common.IMU() system = _common.System() - def __init__(self, top_side: _Axis = _Axis.Z, front_side: _Axis = _Axis.X): + def __init__(self, top_side: Axis = Axis.Z, front_side: Axis = Axis.X): """PrimeHub(top_side=Axis.Z, front_side=Axis.X) Initializes the hub. Optionally, specify how the hub is @@ -118,3 +118,7 @@ class PrimeHub: class InventorHub(PrimeHub): """LEGO® MINDSTORMS Inventor Hub.""" + + +# HACK: hide from jedi +del Axis diff --git a/src/pybricks/pupdevices.py b/src/pybricks/pupdevices.py index be35648..8242653 100644 --- a/src/pybricks/pupdevices.py +++ b/src/pybricks/pupdevices.py @@ -8,25 +8,63 @@ from __future__ import annotations from typing import TYPE_CHECKING, Collection, Optional, Union, overload, Tuple from . import _common -from .parameters import ( - Button as _Button, - Color as _Color, - Direction as _Direction, - Port as _Port, -) +from .parameters import Button, Color, Direction if TYPE_CHECKING: - from .parameters import Number as _Number + from .parameters import Number, Port class DCMotor(_common.DCMotor): """LEGO® Powered Up motor without rotation sensors.""" + # HACK: jedi can't find inherited __init__ so we have to duplicate docs + def __init__(self, port: Port, positive_direction: Direction = Direction.CLOCKWISE): + """__init__(port, positive_direction=Direction.CLOCKWISE) + + Arguments: + port (Port): Port to which the motor is connected. + positive_direction (Direction): Which direction the motor should + turn when you give a positive duty cycle value. + """ + class Motor(_common.Motor): """LEGO® Powered Up motor with rotation sensors.""" - def reset_angle(self, angle: Optional[int]) -> None: + # HACK: jedi can't find inherited __init__ so we have to duplicate docs + def __init__( + self, + port: Port, + positive_direction: Direction = Direction.CLOCKWISE, + gears: Optional[Union[Collection[int], Collection[Collection[int]]]] = None, + reset_angle: bool = True, + ): + """__init__(port, positive_direction=Direction.CLOCKWISE, gears=None, reset_angle=True) + + Arguments: + port (Port): Port to which the motor is connected. + positive_direction (Direction): Which direction the motor should + turn when you give a positive speed value or + angle. + gears (list): + List of gears linked to the motor. + + For example: ``[12, 36]`` represents a gear train with a + 12-tooth and a 36-tooth gear. Use a list of lists for multiple + gear trains, such as ``[[12, 36], [20, 16, 40]]``. + + When you specify a gear train, all motor commands and settings + are automatically adjusted to account for the resulting gear + ratio. The motor direction remains unchanged by this. + reset_angle(bool): + Choose ``True`` to reset the rotation sensor value to the + absolute marker angle (between -180 and 179). + Choose ``False`` to keep the + current value, so your program knows where it left off last + time. + """ + + def reset_angle(self, angle: Optional[Number] = None) -> None: """reset_angle(angle=None) Sets the accumulated rotation angle of the motor to a desired value. @@ -45,13 +83,13 @@ class Remote: light = _common.ColorLight() buttons = _common.Keypad( ( - _Button.LEFT_MINUS, - _Button.RIGHT_MINUS, - _Button.LEFT, - _Button.CENTER, - _Button.RIGHT, - _Button.LEFT_PLUS, - _Button.RIGHT_PLUS, + Button.LEFT_MINUS, + Button.RIGHT_MINUS, + Button.LEFT, + Button.CENTER, + Button.RIGHT, + Button.LEFT_PLUS, + Button.RIGHT_PLUS, ) ) addresss: Union[str, None] @@ -94,7 +132,7 @@ class Remote: class TiltSensor: """LEGO® Powered Up Tilt Sensor.""" - def __init__(self, port: _Port): + def __init__(self, port: Port): """TiltSensor(port) Arguments: @@ -116,6 +154,14 @@ class ColorDistanceSensor(_common.CommonColorSensor): light = _common.ColorLight() + # HACK: jedi can't find inherited __init__ so docs have to be duplicated + def __init__(self, port: Port): + """__init__(port) + + Arguments: + port (Port): Port to which the sensor is connected. + """ + def distance(self) -> int: """distance() -> int: % @@ -135,8 +181,8 @@ class PFMotor(DCMotor): self, sensor: ColorDistanceSensor, channel: int, - color: _Color, - positive_direction: _Direction = _Direction.CLOCKWISE, + color: Color, + positive_direction: Direction = Direction.CLOCKWISE, ): """PFMotor(sensor, channel, color, positive_direction=Direction.CLOCKWISE) @@ -157,15 +203,59 @@ class PFMotor(DCMotor): class ColorSensor(_common.AmbientColorSensor): """LEGO® SPIKE Color Sensor.""" - lights = _common.LightArray(3) + class _LightArray(_common.LightArray): + def __init__(self): + super().__init__(3) + + def on(self, brightness: Union[Number, Tuple[Number, Number, Number]]) -> None: + """on(brightness) + + Turns on the lights at the specified brightness. + + Arguments: + brightness (Number or tuple, %): + A single value will set the brightness of all three lights + to the same value. A tuple of 3 values will set the + brightness of each LED individually. + """ + return super().on(brightness) + + lights = _LightArray() + + # HACK: jedi can't find inherited __init__ so docs have to be duplicated + def __init__(self, port: Port): + """__init__(port) + + Arguments: + port (Port): Port to which the sensor is connected. + """ class UltrasonicSensor: """LEGO® SPIKE Color Sensor.""" - lights = _common.LightArray(3) + class _LightArray(_common.LightArray): + def __init__(self): + super().__init__(4) - def __init__(self, port: _Port): + def on( + self, brightness: Union[Number, Tuple[Number, Number, Number, Number]] + ) -> None: + """on(brightness) + + Turns on the lights at the specified brightness. + + Arguments: + brightness (Number or tuple, %): + A single value will set the brightness of all four lights + to the same value. A tuple of 4 values will set the + brightness of each LED individually. + """ + return super().on(brightness) + + lights = _LightArray() + + def __init__(self, port: Port): """UltrasonicSensor(port) Arguments: @@ -199,7 +289,7 @@ class UltrasonicSensor: class ForceSensor: """LEGO® SPIKE Force Sensor.""" - def __init__(self, port: _Port): + def __init__(self, port: Port): """ForceSensor(port) Arguments: @@ -224,7 +314,7 @@ class ForceSensor: Movement up to approximately 8.00 mm. """ - def pressed(self, force: _Number = 3) -> bool: + def pressed(self, force: Number = 3) -> bool: """pressed(force=3) -> bool Checks if the sensor button is pressed. @@ -255,7 +345,7 @@ class ColorLightMatrix: LEGO® SPIKE 3x3 Color Light Matrix. """ - def __init__(self, port: _Port): + def __init__(self, port: Port): """ColorLightMatrix(port) Arguments: @@ -264,7 +354,7 @@ class ColorLightMatrix: """ ... - def on(self, color: Union[_Color, Collection[_Color]]) -> None: + def on(self, color: Union[Color, Collection[Color]]) -> None: """on(colors) Turns the lights on. @@ -288,7 +378,7 @@ class ColorLightMatrix: class InfraredSensor: """LEGO® Powered Up Infrared Sensor.""" - def __init__(self, port: _Port): + def __init__(self, port: Port): """InfraredSensor(port) Arguments: @@ -328,14 +418,14 @@ class InfraredSensor: class Light: """LEGO® Powered Up Light.""" - def __init__(self, port: _Port): + def __init__(self, port: Port): """Light(port) Arguments: port (Port): Port to which the device is connected. """ - def on(self, brightness: _Number = 100) -> None: + def on(self, brightness: Number = 100) -> None: """on(brightness=100) Turns on the light at the specified brightness. @@ -349,3 +439,12 @@ class Light: """off() Turns off the light.""" + + +# HACK: exclude from jedi +if TYPE_CHECKING: + del Button + del Color + del Direction + del Number + del Port diff --git a/src/pybricks/robotics.py b/src/pybricks/robotics.py index 22bcbbf..5668cb5 100644 --- a/src/pybricks/robotics.py +++ b/src/pybricks/robotics.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MIT -# Copyright (c) 2018-2021 The Pybricks Authors +# Copyright (c) 2018-2022 The Pybricks Authors """Robotics module for the Pybricks API.""" @@ -8,10 +8,11 @@ from __future__ import annotations from typing import Tuple, Optional, overload, TYPE_CHECKING from . import _common -from .parameters import Stop as _Stop +from .parameters import Stop if TYPE_CHECKING: - from .parameters import Number as _Number + from ._common import Motor + from .parameters import Number class DriveBase: @@ -50,10 +51,10 @@ class DriveBase: def __init__( self, - left_motor: _common.Motor, - right_motor: _common.Motor, - wheel_diameter: _Number, - axle_track: _Number, + left_motor: Motor, + right_motor: Motor, + wheel_diameter: Number, + axle_track: Number, ): """DriveBase(left_motor, right_motor, wheel_diameter, axle_track) @@ -67,7 +68,7 @@ class DriveBase: both wheels touch the ground. """ - def drive(self, speed: _Number, turn_rate: _Number) -> None: + def drive(self, speed: Number, turn_rate: Number) -> None: """drive(speed, turn_rate) Starts driving at the specified speed and turn rate. Both values are @@ -118,10 +119,10 @@ class DriveBase: @overload def settings( self, - straight_speed: Optional[_Number], - straight_acceleration: Optional[_Number], - turn_rate: Optional[_Number], - turn_acceleration: Optional[_Number], + straight_speed: Optional[Number] = None, + straight_acceleration: Optional[Number] = None, + turn_rate: Optional[Number] = None, + turn_acceleration: Optional[Number] = None, ) -> None: ... @@ -147,7 +148,9 @@ class DriveBase: deceleration of the robot. """ - def straight(self, distance: _Number, then=_Stop.HOLD, wait=True) -> None: + def straight( + self, distance: Number, then: Stop = Stop.HOLD, wait: bool = True + ) -> None: """straight(distance, then=Stop.HOLD, wait=True) Drives straight for a given distance and then stops. @@ -159,7 +162,7 @@ class DriveBase: with the rest of the program. """ - def turn(self, angle: _Number, then=_Stop.HOLD, wait=True) -> None: + def turn(self, angle: Number, then: Stop = Stop.HOLD, wait: bool = True) -> None: """turn(angle, then=Stop.HOLD, wait=True) Turns in place by a given angle and then stops. @@ -172,7 +175,7 @@ class DriveBase: """ def curve( - self, radius: _Number, angle: _Number, then=_Stop.HOLD, wait=True + self, radius: Number, angle: Number, then: Stop = Stop.HOLD, wait: bool = True ) -> None: """curve(radius, angle, then=Stop.HOLD, wait=True) @@ -185,3 +188,10 @@ class DriveBase: wait (bool): Wait for the maneuver to complete before continuing with the rest of the program. """ + + +# HACK: hide from jedi +if TYPE_CHECKING: + del Motor + del Number + del Stop diff --git a/src/pybricks/tools.py b/src/pybricks/tools.py index 908cf49..ddd76a6 100644 --- a/src/pybricks/tools.py +++ b/src/pybricks/tools.py @@ -3,10 +3,15 @@ """Common tools for timing and data logging.""" -from typing import Any +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from .parameters import Number -def wait(time: int) -> None: +def wait(time: Number) -> None: """wait(time) Pauses the user program for a specified amount of time. @@ -63,7 +68,7 @@ class DataLog: name: str = "log", timestamp: bool = True, extension: str = "csv", - append: bool = False + append: bool = False, ): """DataLog(*headers, name='log', timestamp=True, extension='csv', append=False) @@ -90,3 +95,8 @@ class DataLog: Arguments: values (object, object, ...): One or more objects or values. """ + + +# HACK: hide from jedi +if TYPE_CHECKING: + del Number