Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/actions/run-tests/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: "Run Tests"
description: "Sets up Python, installs dependencies, and runs tests"
inputs:
version:
description: "Python version"
required: true
extra:
description: "Optional dependency to install"
required: true
runs:
using: "composite"
steps:
- name: Update packages
run: sudo apt-get update
shell: bash
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ inputs.version }}
- name: Install Dependencies
run: |
if [ "${{ inputs.extra }}" = "<none>" ]; then
pip install -e '.[test]'
else
pip install -e '.[test,${{ inputs.extra }}]'
fi
shell: bash
- name: Run Pytest
run: make test
shell: bash
41 changes: 28 additions & 13 deletions .github/workflows/test.yml
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why split it like this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want to test the full product combination of all python versions and all dependency configurations of interest, because the number of tests quickly explodes. Options that I considered:

Would you prefer the last option?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, makes sense!

Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,45 @@ name: Unit Tests
on:
push:
branches:
- main
- main
pull_request:
workflow_dispatch:

jobs:
test:
test-all:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
version: [3.9, "3.10", 3.11, 3.12, 3.13, 3.14]

name: Tests
name: Tests (all, py${{ matrix.version }})
runs-on: ${{ matrix.os }}

steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Update packages
run: sudo apt-get update
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/checkout@v6
- name: Run Tests
uses: ./.github/actions/run-tests
with:
python-version: ${{ matrix.version }}
- name: Install Dependencies
run: pip install -e '.[dev]'
- name: Run Pytest
run: make test
version: ${{ matrix.version }}
extra: all

test-extras:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
extra: ["<none>", serial, usb, ftdi, hid, modbus, opentrons, sila, microscopy, pico]

name: Tests (${{ matrix.extra }}, py3.12)
runs-on: ${{ matrix.os }}

steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: Run Tests
uses: ./.github/actions/run-tests
with:
version: "3.12"
extra: ${{ matrix.extra }}
5 changes: 5 additions & 0 deletions docs/user_guide/_getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ Different machines use different communication modes. Replace `[usb]` with one o
| `hid` | hid | HID devices: e.g. Inheco Incubator/Shaker (HID mode) |
| `modbus` | pymodbus | Modbus devices: e.g. Agrow Pump Array |
| `opentrons` | opentrons-http-api-client | e.g. Opentrons backend |
| `microscopy` | numpy (1.26), opencv-python | e.g. Cytation imager |
| `sila` | zeroconf, grpcio | SiLA devices |
| `pico` | microscopy + sila | ImageXpress Pico microscope |
| `dev` | All of the above + testing/linting tools | Development |

Or install all dependencies:
Expand All @@ -57,6 +60,8 @@ Or install all dependencies:
pip install 'pylabrobot[all]'
```

Microscopy is not included in the `all` group because it requires an older version of numpy. If you want to use microscopy features, you need to install those dependencies separately through `pip install "pylabrobot[microscopy]"`.

### From source

You can install PyLabRobot from source. This is particularly useful if you want to contribute to the project or if you want to use the latest features that haven't been released on PyPI yet.
Expand Down
13 changes: 12 additions & 1 deletion pylabrobot/barcode_scanners/keyence/keyence_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
import logging
import time

import serial
try:
import serial

HAS_SERIAL = True
except ImportError as e:
HAS_SERIAL = False
_SERIAL_IMPORT_ERROR = e

from pylabrobot.barcode_scanners.backend import (
BarcodeScannerBackend,
Expand All @@ -24,6 +30,11 @@ def __init__(
self,
port: str,
):
if not HAS_SERIAL:
raise RuntimeError(
"pyserial is not installed. Install with: pip install pylabrobot[serial]. "
f"Import error: {_SERIAL_IMPORT_ERROR}"
)
super().__init__()

# BL-1300 Barcode reader factory default serial communication settings
Expand Down
7 changes: 4 additions & 3 deletions pylabrobot/io/sila/discovery_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from unittest.mock import MagicMock, patch

from pylabrobot.io.sila.discovery import (
HAS_ZEROCONF,
SiLADevice,
_decode_nbns_name,
_discover_sila2,
Expand Down Expand Up @@ -137,9 +138,9 @@ def test_no_zeroconf_returns_empty(self):
devices = asyncio.run(_discover_sila2(timeout=0.1))
self.assertEqual(devices, [])

@patch("pylabrobot.io.sila.discovery.HAS_ZEROCONF", True)
@patch("pylabrobot.io.sila.discovery.Zeroconf")
@patch("pylabrobot.io.sila.discovery.ServiceBrowser")
@unittest.skipIf(not HAS_ZEROCONF, "zeroconf not installed")
@patch("pylabrobot.io.sila.discovery.Zeroconf", create=True)
@patch("pylabrobot.io.sila.discovery.ServiceBrowser", create=True)
def test_discovers_device(self, mock_browser_cls, mock_zc_cls):
mock_zc = MagicMock()
mock_zc_cls.return_value = mock_zc
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import unittest
from unittest.mock import patch

import pytest

pytest.importorskip("ot_api")

from pylabrobot.liquid_handling import LiquidHandler
from pylabrobot.liquid_handling.backends.opentrons_backend import (
OpentronsOT2Backend,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@
from typing import Dict, List, Tuple
from unittest.mock import patch

import numpy as np
import pytest

pytest.importorskip("numpy")
pytest.importorskip("grpc")

import numpy as np # type: ignore[import-not-found]

from pylabrobot.io.sila.grpc import (
decode_fields,
Expand Down
13 changes: 12 additions & 1 deletion pylabrobot/peeling/xpeel_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
from dataclasses import dataclass
from typing import List, Literal, Tuple

import serial # type: ignore
try:
import serial # type: ignore

HAS_SERIAL = True
except ImportError as e:
HAS_SERIAL = False
_SERIAL_IMPORT_ERROR = e

from pylabrobot.io.serial import Serial
from pylabrobot.peeling.backend import PeelerBackend
Expand Down Expand Up @@ -43,6 +49,11 @@ class ErrorInfo:
}

def __init__(self, port: str, logger=None, timeout=None):
if not HAS_SERIAL:
raise RuntimeError(
"pyserial is not installed. Install with: pip install pylabrobot[serial]. "
f"Import error: {_SERIAL_IMPORT_ERROR}"
)
self.logger = logger or logging.getLogger(__name__)
self.port = port
self.response_timeout = timeout if timeout is not None else self.RESPONSE_TIMEOUT
Expand Down
8 changes: 7 additions & 1 deletion pylabrobot/plate_reading/agilent/biotek_synergyh1_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
import time
from typing import Optional

from pylibftdi import FtdiError
try:
from pylibftdi import FtdiError

HAS_PYLIBFTDI = True
except ImportError:
HAS_PYLIBFTDI = False
FtdiError = Exception # type: ignore[misc,assignment]

from pylabrobot.plate_reading.agilent.biotek_backend import BioTekPlateReaderBackend

Expand Down
4 changes: 4 additions & 0 deletions pylabrobot/plate_reading/agilent/biotek_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
import unittest.mock
from typing import Iterator

import pytest

pytest.importorskip("pylibftdi")

from pylabrobot.plate_reading.agilent.biotek_cytation_backend import CytationBackend
from pylabrobot.resources import CellVis_24_wellplate_3600uL_Fb, CellVis_96_wellplate_350uL_Fb

Expand Down
5 changes: 5 additions & 0 deletions pylabrobot/pumps/agrowpumps/agrowdosepump_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ class AgrowPumpArrayBackend(PumpArrayBackend):
"""

def __init__(self, port: str, address: Union[int, str]):
if _MODBUS_IMPORT_ERROR is not None:
raise RuntimeError(
"pymodbus is not installed. Install with: pip install pylabrobot[modbus]. "
f"Import error: {_MODBUS_IMPORT_ERROR}"
)
if not isinstance(port, str):
raise ValueError("Port must be a string")
self.port = port
Expand Down
4 changes: 4 additions & 0 deletions pylabrobot/pumps/agrowpumps/agrowdosepump_tests.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import unittest
from unittest.mock import AsyncMock, call

import pytest

pytest.importorskip("pymodbus")

from pymodbus.client import AsyncModbusSerialClient # type: ignore

from pylabrobot.pumps import PumpArray
Expand Down
13 changes: 12 additions & 1 deletion pylabrobot/pumps/cole_parmer/masterflex_backend.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import serial # type: ignore
try:
import serial # type: ignore

HAS_SERIAL = True
except ImportError as e:
HAS_SERIAL = False
_SERIAL_IMPORT_ERROR = e

from pylabrobot.io.serial import Serial
from pylabrobot.pumps.backend import PumpBackend
Expand All @@ -24,6 +30,11 @@ class MasterflexBackend(PumpBackend):
"""

def __init__(self, com_port: str):
if not HAS_SERIAL:
raise RuntimeError(
"pyserial is not installed. Install with: pip install pylabrobot[serial]. "
f"Import error: {_SERIAL_IMPORT_ERROR}"
)
self.com_port = com_port
self.io = Serial(
port=self.com_port,
Expand Down
13 changes: 12 additions & 1 deletion pylabrobot/sealing/a4s_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,25 @@
import time
from typing import Set

import serial
try:
import serial

HAS_SERIAL = True
except ImportError as e:
HAS_SERIAL = False
_SERIAL_IMPORT_ERROR = e

from pylabrobot.io.serial import Serial
from pylabrobot.sealing.backend import SealerBackend


class A4SBackend(SealerBackend):
def __init__(self, port: str, timeout=20) -> None:
if not HAS_SERIAL:
raise RuntimeError(
"pyserial is not installed. Install with: pip install pylabrobot[serial]. "
f"Import error: {_SERIAL_IMPORT_ERROR}"
)
super().__init__()
self.port = port
self.timeout = timeout
Expand Down
13 changes: 12 additions & 1 deletion pylabrobot/storage/cytomat/cytomat.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
import warnings
from typing import List, Literal, Optional, Union, cast

import serial
try:
import serial

HAS_SERIAL = True
except ImportError as e:
HAS_SERIAL = False
_SERIAL_IMPORT_ERROR = e

from pylabrobot.io.serial import Serial
from pylabrobot.resources import Plate, PlateCarrier, PlateHolder
Expand Down Expand Up @@ -49,6 +55,11 @@ class CytomatBackend(IncubatorBackend):
serial_message_encoding = "utf-8"

def __init__(self, model: Union[CytomatType, str], port: str):
if not HAS_SERIAL:
raise RuntimeError(
"pyserial is not installed. Install with: pip install pylabrobot[serial]. "
f"Import error: {_SERIAL_IMPORT_ERROR}"
)
super().__init__()

supported_models = [
Expand Down
13 changes: 12 additions & 1 deletion pylabrobot/storage/cytomat/heraeus_cytomat_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
import warnings
from typing import List, Tuple

import serial
try:
import serial

HAS_SERIAL = True
except ImportError as e:
HAS_SERIAL = False
_SERIAL_IMPORT_ERROR = e

from pylabrobot.io.serial import Serial
from pylabrobot.resources import Plate, PlateHolder
Expand All @@ -31,6 +37,11 @@ class HeraeusCytomatBackend(IncubatorBackend):
poll_interval = 0.2

def __init__(self, port: str):
if not HAS_SERIAL:
raise RuntimeError(
"pyserial is not installed. Install with: pip install pylabrobot[serial]. "
f"Import error: {_SERIAL_IMPORT_ERROR}"
)
super().__init__()
self.io = Serial(
port=port,
Expand Down
5 changes: 5 additions & 0 deletions pylabrobot/storage/inheco/incubator_shaker_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ def __init__(
vid: int = 0x0403,
pid: int = 0x6001,
):
if not HAS_SERIAL:
raise RuntimeError(
"pyserial is not installed. Install with: pip install pylabrobot[serial]. "
f"Import error: {_SERIAL_IMPORT_ERROR}"
)
super().__init__()

self.logger = logging.LoggerAdapter(
Expand Down
Loading
Loading