Skip to content

Commit a98eaac

Browse files
authored
0.11.0 Release
2 parents 9fb273e + c90f438 commit a98eaac

File tree

12 files changed

+336
-34
lines changed

12 files changed

+336
-34
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Publish distributions to PyPI and TestPyPI
2+
on:
3+
push:
4+
tags:
5+
- "*"
6+
7+
jobs:
8+
build-and-publish:
9+
name: Build and publish distributions to PyPI and TestPyPI
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@master
13+
- name: Set up Python 3.7
14+
uses: actions/setup-python@v1
15+
with:
16+
version: 3.7
17+
- name: Install wheel
18+
run: >-
19+
pip install wheel
20+
- name: Build
21+
run: >-
22+
python3 setup.py sdist bdist_wheel
23+
- name: Publish distribution to PyPI
24+
uses: pypa/gh-action-pypi-publish@master
25+
with:
26+
password: ${{ secrets.PYPI_TOKEN }}

.pre-commit-config.yaml

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
repos:
2-
- repo: https://github.com/psf/black
3-
rev: 19.3b0
2+
- repo: https://github.com/psf/black
3+
rev: 19.10b0
44
hooks:
5-
- id: black
5+
- id: black
66
args:
77
- --safe
88
- --quiet
9-
- repo: https://gitlab.com/pycqa/flake8
10-
rev: 3.7.8
9+
- repo: https://gitlab.com/pycqa/flake8
10+
rev: 3.7.9
1111
hooks:
12-
- id: flake8
12+
- id: flake8
13+
- repo: https://github.com/pre-commit/mirrors-isort
14+
rev: v4.3.21
15+
hooks:
16+
- id: isort
17+
- repo: https://github.com/pre-commit/pre-commit-hooks
18+
rev: v2.4.0
19+
hooks:
20+
- id: no-commit-to-branch
21+
args:
22+
- --branch=dev
23+
- --branch=master
24+
- --branch=rc

.travis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ matrix:
44
include:
55
- python: "3.6"
66
env: TOXENV=lint
7+
- python: "3.6"
8+
env: TOXENV=black
79
- python: "3.6"
810
env: TOXENV=py36
911
- python: "3.7"

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@
1313
author_email="[email protected]",
1414
license="GPL-3.0",
1515
packages=find_packages(exclude=["*.tests"]),
16-
install_requires=["pyserial-asyncio", "zigpy-homeassistant >= 0.10.0"],
16+
install_requires=["pyserial-asyncio", "zigpy-homeassistant >= 0.17.0"],
1717
tests_require=["pytest"],
1818
)

tests/test_api.py

Lines changed: 151 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import asyncio
2-
from unittest import mock
2+
import logging
33

4+
from asynctest import CoroutineMock, mock
45
import pytest
6+
import serial
7+
import zigpy.exceptions
58

69
from zigpy_xbee import api as xbee_api, types as t, uart
710
from zigpy_xbee.zigbee.application import ControllerApplication
@@ -25,9 +28,10 @@ async def test_connect(monkeypatch):
2528

2629

2730
def test_close(api):
28-
api._uart.close = mock.MagicMock()
31+
uart = api._uart
2932
api.close()
30-
assert api._uart.close.call_count == 1
33+
assert api._uart is None
34+
assert uart.close.call_count == 1
3135

3236

3337
def test_commands():
@@ -84,6 +88,22 @@ def mock_api_frame(name, *args):
8488
api._uart.send.reset_mock()
8589

8690

91+
@pytest.mark.asyncio
92+
async def test_command_not_connected(api):
93+
api._uart = None
94+
95+
def mock_api_frame(name, *args):
96+
return mock.sentinel.api_frame_data, api._seq
97+
98+
api._api_frame = mock.MagicMock(side_effect=mock_api_frame)
99+
100+
for cmd, cmd_opts in xbee_api.COMMAND_REQUESTS.items():
101+
with pytest.raises(zigpy.exceptions.APIException):
102+
await api._command(cmd, mock.sentinel.cmd_data)
103+
assert api._api_frame.call_count == 0
104+
api._api_frame.reset_mock()
105+
106+
87107
async def _test_at_or_queued_at_command(api, cmd, monkeypatch, do_reply=True):
88108
monkeypatch.setattr(
89109
t, "serialize", mock.MagicMock(return_value=mock.sentinel.serialize)
@@ -518,3 +538,131 @@ def test_handle_many_to_one_rri(api):
518538
ieee = t.EUI64([t.uint8_t(a) for a in range(0, 8)])
519539
nwk = 0x1234
520540
api._handle_many_to_one_rri(ieee, nwk, 0)
541+
542+
543+
@pytest.mark.asyncio
544+
async def test_reconnect_multiple_disconnects(monkeypatch, caplog):
545+
api = xbee_api.XBee()
546+
dev = mock.sentinel.uart
547+
connect_mock = CoroutineMock()
548+
connect_mock.return_value = asyncio.Future()
549+
connect_mock.return_value.set_result(True)
550+
monkeypatch.setattr(uart, "connect", connect_mock)
551+
552+
await api.connect(dev, 115200)
553+
554+
caplog.set_level(logging.DEBUG)
555+
connected = asyncio.Future()
556+
connected.set_result(mock.sentinel.uart_reconnect)
557+
connect_mock.reset_mock()
558+
connect_mock.side_effect = [asyncio.Future(), connected]
559+
api.connection_lost("connection lost")
560+
await asyncio.sleep(0.3)
561+
api.connection_lost("connection lost 2")
562+
await asyncio.sleep(0.3)
563+
564+
assert "Cancelling reconnection attempt" in caplog.messages
565+
assert api._uart is mock.sentinel.uart_reconnect
566+
assert connect_mock.call_count == 2
567+
568+
569+
@pytest.mark.asyncio
570+
async def test_reconnect_multiple_attempts(monkeypatch, caplog):
571+
api = xbee_api.XBee()
572+
dev = mock.sentinel.uart
573+
connect_mock = CoroutineMock()
574+
connect_mock.return_value = asyncio.Future()
575+
connect_mock.return_value.set_result(True)
576+
monkeypatch.setattr(uart, "connect", connect_mock)
577+
578+
await api.connect(dev, 115200)
579+
580+
caplog.set_level(logging.DEBUG)
581+
connected = asyncio.Future()
582+
connected.set_result(mock.sentinel.uart_reconnect)
583+
connect_mock.reset_mock()
584+
connect_mock.side_effect = [asyncio.TimeoutError, OSError, connected]
585+
586+
with mock.patch("asyncio.sleep"):
587+
api.connection_lost("connection lost")
588+
await api._conn_lost_task
589+
590+
assert api._uart is mock.sentinel.uart_reconnect
591+
assert connect_mock.call_count == 3
592+
593+
594+
@pytest.mark.asyncio
595+
@mock.patch.object(xbee_api.XBee, "_at_command", new_callable=CoroutineMock)
596+
@mock.patch.object(uart, "connect")
597+
async def test_probe_success(mock_connect, mock_at_cmd):
598+
"""Test device probing."""
599+
600+
res = await xbee_api.XBee.probe(mock.sentinel.uart, mock.sentinel.baud)
601+
assert res is True
602+
assert mock_connect.call_count == 1
603+
assert mock_connect.await_count == 1
604+
assert mock_connect.call_args[0][0] is mock.sentinel.uart
605+
assert mock_at_cmd.call_count == 1
606+
assert mock_connect.return_value.close.call_count == 1
607+
608+
609+
@pytest.mark.asyncio
610+
@mock.patch.object(xbee_api.XBee, "init_api_mode", return_value=True)
611+
@mock.patch.object(xbee_api.XBee, "_at_command", side_effect=asyncio.TimeoutError)
612+
@mock.patch.object(uart, "connect")
613+
async def test_probe_success_api_mode(mock_connect, mock_at_cmd, mock_api_mode):
614+
"""Test device probing."""
615+
616+
res = await xbee_api.XBee.probe(mock.sentinel.uart, mock.sentinel.baud)
617+
assert res is True
618+
assert mock_connect.call_count == 1
619+
assert mock_connect.await_count == 1
620+
assert mock_connect.call_args[0][0] is mock.sentinel.uart
621+
assert mock_at_cmd.call_count == 1
622+
assert mock_api_mode.call_count == 1
623+
assert mock_connect.return_value.close.call_count == 1
624+
625+
626+
@pytest.mark.asyncio
627+
@mock.patch.object(xbee_api.XBee, "init_api_mode")
628+
@mock.patch.object(xbee_api.XBee, "_at_command", side_effect=asyncio.TimeoutError)
629+
@mock.patch.object(uart, "connect")
630+
@pytest.mark.parametrize(
631+
"exception",
632+
(asyncio.TimeoutError, serial.SerialException, zigpy.exceptions.APIException),
633+
)
634+
async def test_probe_fail(mock_connect, mock_at_cmd, mock_api_mode, exception):
635+
"""Test device probing fails."""
636+
637+
mock_api_mode.side_effect = exception
638+
mock_api_mode.reset_mock()
639+
mock_at_cmd.reset_mock()
640+
mock_connect.reset_mock()
641+
res = await xbee_api.XBee.probe(mock.sentinel.uart, mock.sentinel.baud)
642+
assert res is False
643+
assert mock_connect.call_count == 1
644+
assert mock_connect.await_count == 1
645+
assert mock_connect.call_args[0][0] is mock.sentinel.uart
646+
assert mock_at_cmd.call_count == 1
647+
assert mock_api_mode.call_count == 1
648+
assert mock_connect.return_value.close.call_count == 1
649+
650+
651+
@pytest.mark.asyncio
652+
@mock.patch.object(xbee_api.XBee, "init_api_mode", return_value=False)
653+
@mock.patch.object(xbee_api.XBee, "_at_command", side_effect=asyncio.TimeoutError)
654+
@mock.patch.object(uart, "connect")
655+
async def test_probe_fail_api_mode(mock_connect, mock_at_cmd, mock_api_mode):
656+
"""Test device probing fails."""
657+
658+
mock_api_mode.reset_mock()
659+
mock_at_cmd.reset_mock()
660+
mock_connect.reset_mock()
661+
res = await xbee_api.XBee.probe(mock.sentinel.uart, mock.sentinel.baud)
662+
assert res is False
663+
assert mock_connect.call_count == 1
664+
assert mock_connect.await_count == 1
665+
assert mock_connect.call_args[0][0] is mock.sentinel.uart
666+
assert mock_at_cmd.call_count == 1
667+
assert mock_api_mode.call_count == 1
668+
assert mock_connect.return_value.close.call_count == 1

tests/test_application.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from unittest import mock
33

44
import pytest
5-
65
from zigpy import types as t
76
from zigpy.zdo.types import ZDOCmd
87

tests/test_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pytest
2+
23
from zigpy_xbee import types as t
34

45

tests/test_uart.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
from unittest import mock
23

34
import pytest
@@ -47,13 +48,6 @@ def test_command_mode_rsp(gw):
4748
assert gw._api.handle_command_mode_rsp.call_args[0][0] == "OK"
4849

4950

50-
def test_command_mode_rsp_decode_exc(gw):
51-
data = b"OK\x81"
52-
with pytest.raises(UnicodeDecodeError):
53-
gw.command_mode_rsp(data)
54-
assert gw._api.handle_command_mode_rsp.call_count == 0
55-
56-
5751
def test_command_mode_send(gw):
5852
data = b"ATAP2\x0D"
5953
gw.command_mode_send(data)
@@ -202,3 +196,24 @@ def test_unescape_underflow(gw):
202196
unescaped, rest = gw._get_unescaped(escaped, 3)
203197
assert unescaped is None
204198
assert rest is None
199+
200+
201+
def test_connection_lost_exc(gw):
202+
gw._connected_future = asyncio.Future()
203+
204+
gw.connection_lost(ValueError())
205+
206+
conn_lost = gw._api.connection_lost
207+
assert conn_lost.call_count == 1
208+
assert isinstance(conn_lost.call_args[0][0], Exception)
209+
assert gw._connected_future.done()
210+
assert gw._connected_future.exception()
211+
212+
213+
def test_connection_closed(gw):
214+
gw._connected_future = asyncio.Future()
215+
gw.connection_lost(None)
216+
217+
assert gw._api.connection_lost.call_count == 0
218+
assert gw._connected_future.done()
219+
assert gw._connected_future.result() is True

tox.ini

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,20 @@ setenv = PYTHONPATH = {toxinidir}
1212
install_command = pip install {opts} {packages}
1313
commands = py.test --cov --cov-report=
1414
deps =
15+
asynctest
1516
coveralls
1617
pytest
1718
pytest-cov
1819
pytest-asyncio
1920

2021
[testenv:lint]
2122
basepython = python3
22-
deps = flake8
23-
commands = flake8
23+
deps =
24+
flake8
25+
isort
26+
commands =
27+
flake8
28+
isort --check -rc {toxinidir}/zigpy_xbee {toxinidir}/tests {toxinidir}/setup.py
2429

2530
[testenv:black]
2631
deps=black

zigpy_xbee/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
MAJOR_VERSION = 0
2-
MINOR_VERSION = 10
2+
MINOR_VERSION = 11
33
PATCH_VERSION = "0"
44
__short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION)
55
__version__ = "{}.{}".format(__short_version__, PATCH_VERSION)

0 commit comments

Comments
 (0)