From 92ddc9a4f8f78f4ec6c4cd8e535157c5ca6fd86d Mon Sep 17 00:00:00 2001 From: heevasti <87366176+heevasti@users.noreply.github.com> Date: Tue, 15 Oct 2024 10:54:23 +0200 Subject: [PATCH] Merge Stable-0-46 to main as new beta (#109) * bumping 0.46.0-beta.0 to 0.46.0 for minor release * [stable-0-46] Fixing a new mypy error * [stable-0-46] Trying to fix issue with Pypi uploading with tags * [stable-0-46] Prepare branch for merging back to main with new beta revision --------- Co-authored-by: heevasti --- .bumpversion.cfg | 2 +- .github/workflows/pypi-publish.yml | 6 +- CHANGELOG.md | 11 ++ documentation/sphinx/source/conf.py | 2 +- qmi/__init__.py | 2 +- qmi/instruments/montana/cryostation_s50.py | 11 +- setup.py | 2 +- tests/core/test_instrument.py | 114 +++++++++++++++++++++ 8 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 tests/core/test_instrument.py diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 56dbad48..aff3319c 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.46.0-beta.0 +current_version = 0.47.0-beta.0 commit = False tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+))?(\.(?P\d+))? diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index 38e81403..50f6fb8c 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -39,7 +39,7 @@ jobs: run: python -m build --sdist --wheel - name: Publish package + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: python -m twine upload --verbose dist/* - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index f8be2e11..eb40afbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## \[x.y.z] - Unreleased +### Added + +### Changed + +### Fixed + +### Removed + + +## [0.46.0] - 2024-10-14 + ### Added - The `QMI_Instrument` and `QMI_TaskRunner` (which inherit from `QMI_RpcObject`) are now equipped with specific `__enter__` and `__exit__` methods, which in the case of `QMI_Instrument` also open and close the instrument when run with a `with` context manager protocol. Meanwhile `QMI_TaskRunner` starts and stops then joins a QMI task thread. In practise, these context managers diff --git a/documentation/sphinx/source/conf.py b/documentation/sphinx/source/conf.py index 8800fabb..7ff63dfc 100644 --- a/documentation/sphinx/source/conf.py +++ b/documentation/sphinx/source/conf.py @@ -24,7 +24,7 @@ author = 'QuTech' # The full version, including alpha/beta/rc tags -release = '0.46.0-beta.0' +release = '0.47.0-beta.0' # The default master_doc used to be 'index', but it was changed to 'contents'. # Override that here (maybe rename the file to the new default later). diff --git a/qmi/__init__.py b/qmi/__init__.py index 48326f7a..f52b93ce 100644 --- a/qmi/__init__.py +++ b/qmi/__init__.py @@ -7,7 +7,7 @@ import atexit -__version__ = "0.46.0-beta.0" +__version__ = "0.47.0-beta.0" # Check Python version. diff --git a/qmi/instruments/montana/cryostation_s50.py b/qmi/instruments/montana/cryostation_s50.py index aa1fbe22..f08bd7ed 100644 --- a/qmi/instruments/montana/cryostation_s50.py +++ b/qmi/instruments/montana/cryostation_s50.py @@ -133,13 +133,16 @@ def _send_request(self, url: str, request_type: str, data: Optional[bytes] = Non try: with urllib.request.urlopen(req) as response: status = response.status - res = response.read() if request_type == 'GET' else None - if status != http.HTTPStatus.OK: - raise QMI_InstrumentException(f"HTTP error code: {status}") + if status != http.HTTPStatus.OK: + raise QMI_InstrumentException(f"HTTP error code: {status}") + + if request_type == 'GET': + return json.loads(response.read()) + except urllib.error.HTTPError as exc: raise QMI_InstrumentException("Error in communication with device") from exc - return json.loads(res) if request_type == 'GET' else None + return None def _set_property(self, subsystem: str, key: str, value: Union[str, float, bool]) -> None: """ diff --git a/setup.py b/setup.py index 0f7407ea..b335232c 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ def read(*names, **kwargs): setup( name="qmi", - version="0.46.0-beta.0", + version="0.47.0-beta.0", description="The Quantum Measurement Infrastructure framework", long_description="{}\n".format(read('README.md')), long_description_content_type='text/markdown', diff --git a/tests/core/test_instrument.py b/tests/core/test_instrument.py new file mode 100644 index 00000000..efa60883 --- /dev/null +++ b/tests/core/test_instrument.py @@ -0,0 +1,114 @@ +#! /usr/bin/env python3 +"""Unit-tests for testing context managing for `QMI_Instrument` class.""" +import logging +import unittest + +from numpy import random + +from tests.patcher import PatcherQmiContext as QMI_Context +from qmi.core.context import QMI_Instrument, rpc_method + + +class MyInstrument_TestDriver(QMI_Instrument): + + def __init__(self, context: QMI_Context, name: str) -> None: + super().__init__(context, name) + self._it_is_open = False + + @rpc_method + def open(self): + super().open() + self._it_is_open = True + + @rpc_method + def close(self): + super().close() + self._it_is_open = False + + @rpc_method + def is_it_open_then(self): + return self._it_is_open + + +class TestInstrumentContextManager(unittest.TestCase): + + def setUp(self): + # Suppress logging. + logging.getLogger("qmi.core.rpc").setLevel(logging.CRITICAL) + + self._ctx_qmi_id = f"test-qmi-instrument-{random.randint(0, 100)}" + self.qmi_patcher = QMI_Context("test_instrument_context_manager") + self.qmi_patcher.start(self._ctx_qmi_id) + + def tearDown(self) -> None: + self.qmi_patcher.stop() + + def test_create_instance(self): + """Test creating an instrument object works without starting qmi.""" + instr = MyInstrument_TestDriver(self.qmi_patcher, self._ctx_qmi_id) + # Get RPC object info + name = instr.get_name() + category = instr.get_category() + # Assert + self.assertFalse(instr.is_open()) + self.assertEqual(self._ctx_qmi_id, name) + self.assertEqual("instrument", category) + + def test_make_instrument_with_with(self): + """Test creating an instrument object with context manager and also opens and closes it.""" + with MyInstrument_TestDriver(self.qmi_patcher, self._ctx_qmi_id) as instr: + # Assert + name = instr.get_name() + category = instr.get_category() + # Assert + self.assertTrue(instr.is_open()) + self.assertEqual(self._ctx_qmi_id, name) + self.assertEqual("instrument", category) + # Also see extra action in the instrument driver's `open` has been executed + self.assertTrue(instr.is_it_open_then()) + + # Assert + self.assertFalse(instr.is_open()) + # Also see extra action in the instrument driver's `close` has been executed + self.assertFalse(instr.is_it_open_then()) + + +class TestInstrumentMakeFunction(unittest.TestCase): + + def setUp(self): + # Suppress logging. + logging.getLogger("qmi.core.rpc").setLevel(logging.CRITICAL) + + # Start a context with creating the "instrument". + self.c1 = QMI_Context("c1") + self.c1.start() + + def tearDown(self): + self.c1.stop() + self.c1 = None + + logging.getLogger("qmi.core.rpc").setLevel(logging.NOTSET) + + def test_make_instrument(self): + """Test making the instrument with 'make_instrument' works as expected""" + instr_proxy = self.c1.make_instrument("instr", MyInstrument_TestDriver) + + # Assert + self.assertFalse(instr_proxy.is_open()) + + def test_make_instrument_with_with(self): + """Test making the instrument with 'make_instrument' works also with context manager.""" + with self.c1.make_instrument("instr", MyInstrument_TestDriver) as instr_proxy: + # Assert + self.assertTrue(instr_proxy.is_open()) + # Also see extra action in the instrument driver's `open` has been executed + self.assertTrue(instr_proxy.is_it_open_then()) + + # Assert + self.assertFalse(instr_proxy.is_open()) + # Also see extra action in the instrument driver's `close` has been executed + self.assertFalse(instr_proxy.is_it_open_then()) + + +if __name__ == '__main__': + unittest.main()