diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 75e093c3..e7600463 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -42,6 +42,7 @@ jobs: matrix: os: [ubuntu-latest, windows-latest, macos-13] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + mpflash-version: ["beta"] include: # for testing - os: windows-latest python-version: "3.11" @@ -88,8 +89,13 @@ jobs: run: | poetry install --with test # use the latest source version of mpflash - pip uninstall mpflash - pip install src/mpflash + + - name: update mpflash to beta for testing + if: ${{ matrix.mpflash-version == 'beta' }} + run: | + # use poetry's environmnt + poetry run pip uninstall mpflash -y + poetry run pip install src/mpflash #---------------------------------------------- # stubber clone diff --git a/poetry.lock b/poetry.lock index 06fbc50c..0f16e6dd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "alabaster" @@ -1173,6 +1173,23 @@ files = [ [package.dependencies] attrs = ">=19.2.0" +[[package]] +name = "jsons" +version = "1.6.3" +description = "For serializing Python objects to JSON (dicts) and back" +optional = false +python-versions = ">=3.5" +files = [ + {file = "jsons-1.6.3-py3-none-any.whl", hash = "sha256:f07f8919316f72a3843c7ca6cc6c900513089f10092626934d1bfe4b5cf15401"}, + {file = "jsons-1.6.3.tar.gz", hash = "sha256:cd5815c7c6790ae11c70ad9978e0aa850d0d08a643a5105cc604eac8b29a30d7"}, +] + +[package.dependencies] +typish = ">=1.9.2" + +[package.extras] +test = ["attrs", "codecov", "coverage", "dataclasses", "pytest", "scons", "tzdata"] + [[package]] name = "jupyter-client" version = "8.6.1" @@ -2981,6 +2998,19 @@ files = [ {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, ] +[[package]] +name = "typish" +version = "1.9.3" +description = "Functionality for types" +optional = false +python-versions = "*" +files = [ + {file = "typish-1.9.3-py3-none-any.whl", hash = "sha256:03cfee5e6eb856dbf90244e18f4e4c41044c8790d5779f4e775f63f982e2f896"}, +] + +[package.extras] +test = ["codecov", "coverage", "mypy", "nptyping (>=1.3.0)", "numpy", "pycodestyle", "pylint", "pytest"] + [[package]] name = "urllib3" version = "2.2.1" @@ -3145,4 +3175,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = ">=3.9,<4.0" -content-hash = "d1b10d62fc9f36416502cd19b811085584f7250cf6480d1c90b1296e8915e321" +content-hash = "09bdf98390d14eb98aba7f281977e7082f6c33003273251b0f51362663df432c" diff --git a/pyproject.toml b/pyproject.toml index de6de74a..a6e801e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,6 +97,7 @@ pytest-mock = "^3.10.0" mock = "^4.0.3" distro = "^1.8.0" fasteners = "^0.19" +jsons = "^1.6.3" [tool.poetry.group.dev] optional = true diff --git a/src/mpflash/mpflash/ask_input.py b/src/mpflash/mpflash/ask_input.py index 72aae441..bce9154f 100644 --- a/src/mpflash/mpflash/ask_input.py +++ b/src/mpflash/mpflash/ask_input.py @@ -12,7 +12,7 @@ from loguru import logger as log from mpflash.config import config -from mpflash.mpboard_id import get_stored_boards_for_port, known_stored_boards, local_mp_ports +from mpflash.mpboard_id import get_known_boards_for_port, get_known_ports, known_stored_boards from mpflash.mpremoteboard import MPRemoteBoard from mpflash.vendor.versions import micropython_versions @@ -79,8 +79,8 @@ def ask_missing_params( if not params.boards or "?" in params.boards: ask_port_board(questions, action=action) - - answers = inquirer.prompt(questions, answers=answers) + if questions: + answers = inquirer.prompt(questions, answers=answers) if not answers: # input cancelled by user return [] # type: ignore @@ -88,13 +88,22 @@ def ask_missing_params( if isinstance(params, FlashParams) and "serial" in answers: params.serial = answers["serial"] if "port" in answers: - params.ports = [answers["port"]] + params.ports = [p for p in params.ports if p != "?"] # remove the "?" if present + params.ports.extend(answers["port"]) if "boards" in answers: - params.boards = answers["boards"] if isinstance(answers["boards"], list) else [answers["boards"]] + params.boards = [b for b in params.boards if b != "?"] # remove the "?" if present + params.boards.extend(answers["boards"] if isinstance(answers["boards"], list) else [answers["boards"]]) if "versions" in answers: + params.versions = [v for v in params.versions if v != "?"] # remove the "?" if present # make sure it is a list - params.versions = answers["versions"] if isinstance(answers["versions"], list) else [answers["versions"]] - + if isinstance(answers["versions"], (list, tuple)): + params.versions.extend(answers["versions"]) + else: + params.versions.append(answers["versions"]) + # remove duplicates + params.ports = list(set(params.ports)) + params.boards = list(set(params.boards)) + params.versions = list(set(params.versions)) log.debug(repr(params)) return params @@ -149,14 +158,15 @@ def ask_port_board(questions: list, *, action: str): # import only when needed to reduce load time import inquirer - # TODO: if action = flash, Use Inquirer.List for boards + # if action flash, single input + # if action download, multiple input inquirer_ux = inquirer.Checkbox if action == "download" else inquirer.List questions.extend( ( inquirer.List( "port", message="Which port do you want to {action} " + "to {serial} ?" if action == "flash" else "?", - choices=local_mp_ports(), + choices=get_known_ports(), autocomplete=True, ), inquirer_ux( @@ -194,7 +204,7 @@ def ask_versions(questions: list, *, action: str): # remove the versions for which there are no known boards in the board_info.json # todo: this may be a little slow - mp_versions = [v for v in mp_versions if get_stored_boards_for_port("stm32", [v])] + mp_versions = [v for v in mp_versions if get_known_boards_for_port("stm32", [v])] mp_versions.append("preview") mp_versions.reverse() # newest first diff --git a/src/mpflash/mpflash/cli_download.py b/src/mpflash/mpflash/cli_download.py index 1459b51c..a8e0bc64 100644 --- a/src/mpflash/mpflash/cli_download.py +++ b/src/mpflash/mpflash/cli_download.py @@ -7,7 +7,7 @@ from loguru import logger as log from mpflash.errors import MPFlashError -from mpflash.mpboard_id import find_stored_board +from mpflash.mpboard_id import find_known_board from mpflash.vendor.versions import clean_version from .ask_input import DownloadParams, ask_missing_params @@ -68,10 +68,17 @@ def cli_download(**kwargs) -> int: params.versions = list(params.versions) params.boards = list(params.boards) if params.boards: - pass - # TODO Clean board - same as in cli_flash.py + if not params.ports: + # no ports specified - resolve ports from specified boards by resolving board IDs + for board in params.boards: + if board != "?": + try: + board_ = find_known_board(board) + params.ports.append(board_["port"]) + except MPFlashError as e: + log.error(f"{e}") else: - # no boards specified - detect connected boards + # no boards specified - detect connected ports and boards params.ports, params.boards = connected_ports_boards() params = ask_missing_params(params, action="download") diff --git a/src/mpflash/mpflash/cli_flash.py b/src/mpflash/mpflash/cli_flash.py index fe9d06c7..3947739a 100644 --- a/src/mpflash/mpflash/cli_flash.py +++ b/src/mpflash/mpflash/cli_flash.py @@ -4,7 +4,7 @@ from loguru import logger as log from mpflash.errors import MPFlashError -from mpflash.mpboard_id import find_stored_board +from mpflash.mpboard_id import find_known_board from mpflash.vendor.versions import clean_version from .ask_input import FlashParams, ask_missing_params @@ -116,7 +116,7 @@ def cli_flash_board(**kwargs) -> int: continue if " " in board_id: try: - info = find_stored_board(board_id) + info = find_known_board(board_id) if info: log.info(f"Resolved board description: {info['board']}") params.boards.remove(board_id) diff --git a/src/mpflash/mpflash/download.py b/src/mpflash/mpflash/download.py index 47fa0b4b..ee808015 100644 --- a/src/mpflash/mpflash/download.py +++ b/src/mpflash/mpflash/download.py @@ -20,6 +20,7 @@ from mpflash.common import PORT_FWTYPES from mpflash.errors import MPFlashError +from mpflash.mpboard_id import get_known_ports jsonlines.ujson = None # type: ignore # ######################################################################################################### @@ -109,6 +110,8 @@ def get_boards(ports: List[str], boards: List[str], clean: bool) -> List[Firmwar """ board_urls: List[FirmwareInfo] = [] + if ports is None: + ports = get_known_ports() for port in ports: download_page_url = f"{MICROPYTHON_ORG_URL}download/?port={port}" _urls = get_board_urls(download_page_url) @@ -170,6 +173,7 @@ def download_firmwares( skipped = downloaded = 0 if versions is None: versions = [] + unique_boards = get_firmware_list(ports, boards, versions, clean) for b in unique_boards: diff --git a/src/mpflash/mpflash/flash_esp.py b/src/mpflash/mpflash/flash_esp.py index 873e5d65..3e68ea99 100644 --- a/src/mpflash/mpflash/flash_esp.py +++ b/src/mpflash/mpflash/flash_esp.py @@ -10,7 +10,7 @@ import esptool from loguru import logger as log -from mpflash.mpboard_id import find_stored_board +from mpflash.mpboard_id import find_known_board from mpflash.mpremoteboard import MPRemoteBoard @@ -22,7 +22,7 @@ def flash_esp(mcu: MPRemoteBoard, fw_file: Path, *, erase: bool = True) -> Optio log.info(f"Flashing {fw_file} on {mcu.board} on {mcu.serialport}") if not mcu.cpu: # Lookup CPU based on the board name - mcu.cpu = find_stored_board(mcu.board)["cpu"] + mcu.cpu = find_known_board(mcu.board)["cpu"] cmds: List[List[str]] = [] if erase: diff --git a/src/mpflash/mpflash/mpboard_id/__init__.py b/src/mpflash/mpflash/mpboard_id/__init__.py index 8a335bd8..d363b058 100644 --- a/src/mpflash/mpflash/mpboard_id/__init__.py +++ b/src/mpflash/mpflash/mpboard_id/__init__.py @@ -9,10 +9,13 @@ from pathlib import Path from typing import List, Optional, Tuple, TypedDict, Union -from mpflash.errors import MPFlashError from mpflash.common import PORT_FWTYPES +from mpflash.errors import MPFlashError from mpflash.vendor.versions import clean_version +# KNOWN ports and boards are sourced from the micropython repo, +# this info is stored in the board_info.json file + # Board based on the dataclass Board but changed to TypedDict # - source : get_boardnames.py @@ -30,27 +33,27 @@ class Board(TypedDict): @lru_cache(maxsize=None) -def read_stored_boardinfo() -> List[Board]: +def read_known_boardinfo() -> List[Board]: """Reads the board_info.json file and returns the data as a list of Board objects""" with open(Path(__file__).parent / "board_info.json", "r") as file: return json.load(file) -def local_mp_ports() -> List[str]: +def get_known_ports() -> List[str]: # TODO: Filter for Version - mp_boards = read_stored_boardinfo() + mp_boards = read_known_boardinfo() # select the unique ports from info ports = set({board["port"] for board in mp_boards if board["port"] in PORT_FWTYPES.keys()}) return sorted(list(ports)) -def get_stored_boards_for_port(port: str, versions: Optional[List[str]] = None): +def get_known_boards_for_port(port: str, versions: Optional[List[str]] = None): """ Returns a list of boards for the given port and version(s) port : str : The Micropython port to filter for versions : List[str] : The Micropython versions to filter for (actual versions required)""" - mp_boards = read_stored_boardinfo() + mp_boards = read_known_boardinfo() # filter for 'preview' as they are not in the board_info.json # instead use stable version @@ -75,16 +78,16 @@ def known_stored_boards(port: str, versions: Optional[List[str]] = None) -> List port : str : The Micropython port to filter for versions : List[str] : The Micropython versions to filter for (actual versions required) """ - mp_boards = get_stored_boards_for_port(port, versions) + mp_boards = get_known_boards_for_port(port, versions) boards = set({(f'{board["version"]} {board["description"]}', board["board"]) for board in mp_boards}) return sorted(list(boards)) @lru_cache(maxsize=20) -def find_stored_board(board_id: str) -> Board: - """Find the board for the given board_ID or 'board description' and return the board info as a Board object""" - info = read_stored_boardinfo() +def find_known_board(board_id: str) -> Board: + """Find the board for the given BOARD_ID or 'board description' and return the board info as a Board object""" + info = read_known_boardinfo() for board_info in info: if board_id in (board_info["board"], board_info["description"]): if "cpu" not in board_info or not board_info["cpu"]: diff --git a/src/mpflash/mpflash/mpboard_id/board_id.py b/src/mpflash/mpflash/mpboard_id/board_id.py index 3c2f8037..3f8c340f 100644 --- a/src/mpflash/mpflash/mpboard_id/board_id.py +++ b/src/mpflash/mpflash/mpboard_id/board_id.py @@ -8,7 +8,7 @@ from typing import Optional from mpflash.errors import MPFlashError -from mpflash.vendor.versions import clean_version +from mpflash.vendor.versions import clean_version, get_stable_mp_version ############################################################################################### HERE = Path(__file__).parent @@ -48,8 +48,8 @@ def _find_board_id_by_description( # filter for matching version if version == "preview": - # TODO: match last stable - version = "v1.22.2" + # match last stable + version = get_stable_mp_version() version_matches = [b for b in info if b["version"].startswith(version)] if not version_matches: raise MPFlashError(f"No board info found for version {version}") diff --git a/src/mpflash/mpflash/worklist.py b/src/mpflash/mpflash/worklist.py index 6dda41c7..2f5b4048 100644 --- a/src/mpflash/mpflash/worklist.py +++ b/src/mpflash/mpflash/worklist.py @@ -9,7 +9,7 @@ from .config import config from .downloaded import find_downloaded_firmware from .list import show_mcus -from .mpboard_id import find_stored_board +from .mpboard_id import find_known_board from .mpremoteboard import MPRemoteBoard # ######################################################################################################### @@ -131,7 +131,7 @@ def manual_worklist( # TODO : Find a way to avoid needing to specify the port # Lookup the matching port and cpu in board_info based in the board name try: - info = find_stored_board(board) + info = find_known_board(board) mcu.port = info["port"] # need the CPU type for the esptool mcu.cpu = info["cpu"] diff --git a/src/mpflash/tests/cli/test_cli_download.py b/src/mpflash/tests/cli/test_cli_download.py index 29825480..0ef534a2 100644 --- a/src/mpflash/tests/cli/test_cli_download.py +++ b/src/mpflash/tests/cli/test_cli_download.py @@ -30,6 +30,7 @@ ("40", 0, ["download", "--board", "ESP32_GENERIC"]), ("41", 0, ["download", "--board", "?"]), ("42", 0, ["download", "--board", "?", "--board", "ESP32_GENERIC"]), + ("43", 0, ["download", "--board", "ESP32_GENERIC", "--board", "?"]), ("50", 0, ["download", "--destination", "firmware", "--version", "1.22.0", "--board", "ESP32_GENERIC"]), ("60", 0, ["download", "--no-clean"]), ("61", 0, ["download", "--clean"]), @@ -38,6 +39,13 @@ ) def test_mpflash_download(id, ex_code, args: List[str], mocker: MockerFixture): def fake_ask_missing_params(params: DownloadParams, action: str = "download") -> DownloadParams: + if "?" in params.ports: + params.ports = ["esp32"] + if "?" in params.boards: + params.ports = ["esp32"] + params.boards = ["ESP32_GENERIC"] + if "?" in params.versions: + params.versions = ["1.22.0"] return params m_connected_ports_boards = mocker.patch( @@ -59,14 +67,16 @@ def fake_ask_missing_params(params: DownloadParams, action: str = "download") -> m_ask_missing_params.assert_called_once() m_download.assert_called_once() + assert m_download.call_args.args[1], "one or more ports should be specified for download" + if "--clean" in args: - assert m_download.call_args.args[5] == True + assert m_download.call_args.args[5] == True, "clean should be True" if "--no-clean" in args: - assert m_download.call_args.args[5] == False + assert m_download.call_args.args[5] == False, "clean should be False" else: - assert m_download.call_args.args[5] == True + assert m_download.call_args.args[5] == True, "clean should be True" if "--force" in args: - assert m_download.call_args.args[4] == True + assert m_download.call_args.args[4] == True, "force should be True" else: - assert m_download.call_args.args[4] == False + assert m_download.call_args.args[4] == False, "force should be False" diff --git a/src/mpflash/tests/mpboard_id/test_api.py b/src/mpflash/tests/mpboard_id/test_api.py index b4032a3a..f2a5a36e 100644 --- a/src/mpflash/tests/mpboard_id/test_api.py +++ b/src/mpflash/tests/mpboard_id/test_api.py @@ -1,18 +1,18 @@ import pytest -from mpflash.mpboard_id import find_stored_board, known_stored_boards, local_mp_ports, read_stored_boardinfo +from mpflash.mpboard_id import find_known_board, get_known_ports, known_stored_boards, read_known_boardinfo pytestmark = [pytest.mark.mpflash] def test_read_boardinfo(): - boards = read_stored_boardinfo() + boards = read_known_boardinfo() assert isinstance(boards, list) assert all(isinstance(board, dict) for board in boards) def test_known_mp_ports(): - ports = local_mp_ports() + ports = get_known_ports() assert isinstance(ports, list) assert all(isinstance(port, str) for port in ports) @@ -37,7 +37,7 @@ def test_known_mp_boards(port, versions): def test_find_mp_board(): - board = find_stored_board("PYBV11") + board = find_known_board("PYBV11") assert isinstance(board, dict) assert "board" in board assert "description" in board diff --git a/src/mpflash/tests/test_ask_input.py b/src/mpflash/tests/test_ask_input.py index ac1263c0..4b691af5 100644 --- a/src/mpflash/tests/test_ask_input.py +++ b/src/mpflash/tests/test_ask_input.py @@ -17,8 +17,8 @@ def test_ask_missing_params_no_interactivity(mocker: MockerFixture): _config.interactive = False input = { - "versions": ("?"), - "boards": ("?"), + "versions": ["?"], + "boards": ["?"], "fw_folder": Path("C:/Users/josverl/Downloads/firmware"), "clean": True, "force": False, @@ -34,27 +34,49 @@ def test_ask_missing_params_no_interactivity(mocker: MockerFixture): "id, download, input, answers, check", [ ( - "D ? preview", + "10 D -v ? -b ?", True, { - "versions": ("preview",), - "boards": ("?", "SEEED_WIO_TERMINAL"), + "versions": ["?"], + "boards": ["?"], "fw_folder": Path("C:/Users/josverl/Downloads/firmware"), "clean": True, "force": False, }, { "versions": ["1.14.0"], - "boards": ["SEEED_WIO_TERMINAL"], + "boards": ["OTHER_BOARD"], + }, + { + "versions": ["1.14.0"], + "boards": ["OTHER_BOARD"], + }, + ), + ( + "11 D -v ? -b ? -b SEEED_WIO_TERMINAL", + True, + { + "versions": ["?"], + "boards": ["?", "SEEED_WIO_TERMINAL"], + "fw_folder": Path("C:/Users/josverl/Downloads/firmware"), + "clean": True, + "force": False, + }, + { + "versions": ["1.14.0"], + "boards": ["OTHER_BOARD"], + }, + { + "versions": ["1.14.0"], + "boards": ["OTHER_BOARD", "SEEED_WIO_TERMINAL"], }, - {"versions": ["1.14.0"]}, ), ( - "D select version", + "20 D select version", True, { - "versions": ("?",), - "boards": ("SEEED_WIO_TERMINAL"), + "versions": ["?"], + "boards": ["SEEED_WIO_TERMINAL"], "fw_folder": Path("C:/Users/josverl/Downloads/firmware"), "clean": True, "force": False, @@ -66,33 +88,48 @@ def test_ask_missing_params_no_interactivity(mocker: MockerFixture): ), # versions as string ( - "D version string", + "21 D version string", + True, + { + "versions": ["preview"], + "boards": ["SEEED_WIO_TERMINAL"], + "fw_folder": Path("C:/Users/josverl/Downloads/firmware"), + "clean": True, + "force": False, + }, + {}, + {"versions": ["preview"]}, + ), + ( + "22 D -v preview -v ?", True, { - "versions": ("preview",), - "boards": ("?", "SEEED_WIO_TERMINAL"), + "versions": ["preview", "?"], + "boards": ["SEEED_WIO_TERMINAL"], "fw_folder": Path("C:/Users/josverl/Downloads/firmware"), "clean": True, "force": False, }, { "versions": "1.14.0", - "boards": ["SEEED_WIO_TERMINAL"], }, - {"versions": ["1.14.0"]}, + {"versions": ["preview", "1.14.0"]}, ), ( - "D no boards", + "30 D no boards", True, { - "versions": ("stable",), - "boards": (), + "versions": ["stable"], + "boards": [], "fw_folder": Path("C:/Users/josverl/Downloads/firmware"), "clean": True, "force": False, }, { - "boards": ["FAKE_BOARD", "SEEED_WIO_TERMINAL"], + "boards": [ + "SEEED_WIO_TERMINAL", + "FAKE_BOARD", + ], }, { # "versions": ["stable"] @@ -100,11 +137,11 @@ def test_ask_missing_params_no_interactivity(mocker: MockerFixture): ), # flash ( - "F ? preview", + "50 F -b ? -v preview", False, { - "versions": ("preview",), - "boards": ("?"), + "versions": ["preview"], + "boards": ["?"], "fw_folder": Path("C:/Users/josverl/Downloads/firmware"), "serial": "", "erase": True, @@ -112,7 +149,6 @@ def test_ask_missing_params_no_interactivity(mocker: MockerFixture): "cpu": "", }, { - "versions": ["1.14.0"], "boards": ["SEEED_WIO_TERMINAL"], "serial": "COM4", }, @@ -137,16 +173,26 @@ def test_ask_missing_params_with_interactivity( m_prompt: Mock = mocker.patch("inquirer.prompt", return_value=answers, autospec=True) result = ask_missing_params(params, action) - m_prompt.assert_called_once() + if answers: + m_prompt.assert_called_once() # explicit checks for key in check: - assert getattr(result, key) == check[key] + if isinstance(check[key], list): + assert sorted(getattr(result, key)) == sorted(check[key]) + else: + assert getattr(result, key) == check[key] # are all answers used in the result for key in answers: if key not in check: - assert getattr(result, key) == answers[key] + if isinstance(answers[key], list): + assert sorted(getattr(result, key)) == sorted(answers[key]) + else: + assert getattr(result, key) == answers[key] # also make sure that the other attributes are not changed for key in input: if key not in answers and key not in check: - assert getattr(result, key) == input[key] + if isinstance(input[key], list): + assert sorted(getattr(result, key)) == sorted(input[key]) + else: + assert getattr(result, key) == input[key] diff --git a/src/mpflash/tests/test_mp_board_filter.py b/src/mpflash/tests/test_mp_board_filter.py index 9ffd4176..dfdd1365 100644 --- a/src/mpflash/tests/test_mp_board_filter.py +++ b/src/mpflash/tests/test_mp_board_filter.py @@ -2,13 +2,13 @@ import pytest -from mpflash.mpboard_id import get_stored_boards_for_port, local_mp_ports +from mpflash.mpboard_id import get_known_boards_for_port, get_known_ports from mpflash.vendor.versions import get_stable_mp_version pytestmark = [pytest.mark.mpflash] -@pytest.mark.parametrize("port", local_mp_ports()) +@pytest.mark.parametrize("port", get_known_ports()) @pytest.mark.parametrize( "id, versions", [ @@ -24,7 +24,7 @@ def test_mp_board_filter(port: str, id, versions: List[str]): # Arrange # Act - result = get_stored_boards_for_port(port, versions) + result = get_known_boards_for_port(port, versions) # Assert assert len(result) >= 1 assert all(board["port"] == port for board in result)