diff --git a/.github/workflows/ismrmrd_python_conda.yml b/.github/workflows/ismrmrd_python_conda.yml index c3117a4..1625717 100644 --- a/.github/workflows/ismrmrd_python_conda.yml +++ b/.github/workflows/ismrmrd_python_conda.yml @@ -1,10 +1,10 @@ on: + push: + branches: [master] + tags: ["v*.*.*"] pull_request: - branches: - - master - release: - types: - - created + branches: [master] + workflow_dispatch: jobs: build-conda-packages: @@ -13,12 +13,13 @@ jobs: os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - - uses: conda-incubator/setup-miniconda@e81abac10ce2c37423b54eae5af93aa3b4d3475c + - uses: actions/checkout@v5 + - uses: conda-incubator/setup-miniconda@v3 with: + miniforge-version: latest activate-environment: ismrmrd-python-build environment-file: conda/environment.yml - python-version: 3.9 + python-version: 3.12 auto-activate-base: false - name: Build conda package shell: bash -l {0} diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index a560f7d..c7f32a4 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,56 +1,53 @@ name: Publish Python 🐍 distributions 📦 to PyPI and TestPyPI -on: - release: - types: - - created +on: + push: + branches: [master] + tags: ["v*.*.*"] + pull_request: + branches: [master] workflow_dispatch: jobs: build: name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - - uses: actions/checkout@master - - name: Set up Python 3.9 - uses: actions/setup-python@v1 + - uses: actions/checkout@v5 + - name: Set up Python 3.x + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.12 + - name: Install runtime dependencies + run: python -m pip install -r requirements.txt --user + - name: Perform editable installation to generate the schema subpackage + run: python -m pip install -e . + - name: Run all tests + run: python -m pytest - name: Install pypa/build - run: >- - python -m - pip install - build - --user + run: python -m pip install build --user - name: Build a binary wheel and a source tarball - run: >- - python -m - build - --sdist - --wheel - --outdir dist/ - . + run: python -m build --sdist --wheel --outdir dist/ . - name: Store the distribution packages - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: python-package-distributions path: dist/ publish-to-pypi: - name: >- - Publish Python 🐍 distribution 📦 to PyPI - if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' # only publish to PyPI on tag pushes or manual trigger + name: Publish Python 🐍 distribution 📦 to PyPI + if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' && startsWith(github.ref, 'refs/tags') needs: - build runs-on: ubuntu-latest environment: name: pypi - url: https://pypi.org/p/ismrmrmd + url: https://pypi.org/p/ismrmrmd permissions: id-token: write steps: - name: Download all the dists - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v5 with: name: python-package-distributions path: dist/ diff --git a/README b/README.md similarity index 100% rename from README rename to README.md diff --git a/conda/conda_build_config.yaml b/conda/conda_build_config.yaml index 65ea6b8..a042c9f 100644 --- a/conda/conda_build_config.yaml +++ b/conda/conda_build_config.yaml @@ -1,3 +1,3 @@ python: - - 3.9 - - 3.10 \ No newline at end of file + - 3.10 + - 3.12 \ No newline at end of file diff --git a/conda/meta.yaml b/conda/meta.yaml index e33d45b..dafc854 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -10,12 +10,10 @@ source: requirements: build: - python {{ python }} - - xsdata >=24.0 + - xsdata>=24.0 - setuptools - - wheel - - jinja2 >=2.11.0 - pip - + run: - python - numpy>=1.22.0 @@ -29,10 +27,10 @@ test: - tests commands: - pip check - - nosetests + - pytest requires: - pip - - nose + - pytest about: home: https://github.com/ismrmrd/ismrmrd-python diff --git a/conda/run_test.sh b/conda/run_test.sh index 551d0e6..8bae610 100644 --- a/conda/run_test.sh +++ b/conda/run_test.sh @@ -2,6 +2,4 @@ set -euo pipefail -cd tests -nosetests - +pytest diff --git a/ismrmrd/xsd/__init__.py b/ismrmrd/xsd/__init__.py index c462532..ec08e78 100644 --- a/ismrmrd/xsd/__init__.py +++ b/ismrmrd/xsd/__init__.py @@ -1,2 +1,2 @@ -from .pyxb_compat import (CreateFromDocument, ToXML, ToDOM) -from .ismrmrdschema import * +from .pyxb_compat import CreateFromDocument, ToXML, ToDOM +from .ismrmrdschema import * diff --git a/pyproject.toml b/pyproject.toml index bf5365b..34dbfcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,29 @@ [build-system] -requires = ["setuptools","wheel","xsdata[cli]","jinja2 >= 2.11.0"] -build-backend = "setuptools.build_meta" \ No newline at end of file +requires = ["setuptools", "xsdata[cli]"] +build-backend = "setuptools.build_meta" + +[project] +name = "ismrmrd" +dynamic = ["version"] +dependencies = ["h5py>=2.3", "numpy>=1.22.0", "xsdata>=22.12"] +requires-python = ">=3.9" + +authors = [{name = "ISMRMRD Developers"}] + +description = "Python implementation of ISMRMRD" +readme = "README.md" +license-files = ["LICENSE"] +license = "LicenseRef-IsmrmrdSoftwareLicense" +keywords = ["ismrmrd"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", + "Intended Audience :: Science/Research", + "Topic :: Scientific/Engineering :: Medical Science Apps." +] + +[project.urls] +Homepage = "https://github.com/ismrmrd/ismrmrd-python" +Documentation = "https://github.com/ismrmrd/ismrmrd-python" +Repository = "https://github.com/ismrmrd/ismrmrd-python" diff --git a/requirements.txt b/requirements.txt index 311eac9..89b3e99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,4 @@ -h5py==2.9.0 -nose==1.3.7 -numpy==1.22.0 -PyXB==1.2.6 -six==1.12.0 -xsdata==24.4 \ No newline at end of file +h5py==3.14.0 +pytest==8.4.1 +numpy==2.3.2 +xsdata==25.7 \ No newline at end of file diff --git a/setup.py b/setup.py index a7cd7d2..04ef977 100644 --- a/setup.py +++ b/setup.py @@ -1,81 +1,74 @@ import os -from setuptools import setup, find_packages -from distutils.command.build import build -from distutils.command.build_py import build_py +from setuptools import setup, find_packages, Command -import logging import shutil from pathlib import Path -import re - -logging.basicConfig() -log_ = logging.getLogger(__name__) +import re schema_file = os.path.join('schema','ismrmrd.xsd') config_file = os.path.join('schema','.xsdata.xml') -class my_build_py(build_py): - def run(self): - # honor the --dry-run flag - if not self.dry_run: - outloc = self.build_lib - outloc = os.path.join(outloc,'ismrmrd/xsd/ismrmrdschema') - - generate_schema(schema_file, config_file, outloc) - # distutils uses old-style classes, so no super() - build_py.run(self) +import setuptools.command.build +setuptools.command.build.build.sub_commands.append(("generate_schema", None)) +class GenerateSchemaCommand(Command): + description = "Generate Python code from ISMRMRD XML schema using xsdata" + user_options = [] -def fix_init_file(package_name,filepath): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.build_lib = None + self.editable_mode = False - with open(filepath,'r+') as f: - text = f.read() - text = re.sub(f'from {package_name}.ismrmrd', 'from .ismrmrd',text) - f.seek(0) - f.write(text) - f.truncate() + def initialize_options(self): + pass + def finalize_options(self): + # Set build_lib for non-editable installs + self.set_undefined_options("build_py", ("build_lib", "build_lib")) + def run(self): + # Use editable_mode if present (PEP 660) + if self.editable_mode: + outdir = 'ismrmrd/xsd/' + else: + outdir = os.path.join(self.build_lib, 'ismrmrd/xsd/') + self.announce(f'Generating schema to {outdir} (editable_mode={self.editable_mode})', level=3) + self.generate_schema(schema_file, config_file, 'ismrmrdschema', outdir) + def get_source_files(self): + return [schema_file, config_file] -def generate_schema(schema_filename, config_filename, outloc ): - def to_uri(filename): - return Path(filename).absolute().as_uri() + def get_outputs(self): + return [ + "{build_lib}/ismrmrd/xsd/ismrmrdschema/__init__.py", + "{build_lib}/ismrmrd/xsd/ismrmrdschema/ismrmrd.py" + ] - import sys - import subprocess - - subpackage_name = 'ismrmrdschema' - args = [sys.executable,'-m','xsdata', str(schema_filename), '--config',str(config_filename), '--package',subpackage_name] - subprocess.run(args) - fix_init_file(subpackage_name,f"{subpackage_name}/__init__.py") - shutil.rmtree(os.path.join(outloc,subpackage_name),ignore_errors=True) - shutil.move(subpackage_name,outloc) + def fix_init_file(self, package_name,filepath): + with open(filepath,'r+') as f: + text = f.read() + text = re.sub(f'from {package_name}.ismrmrd', 'from .ismrmrd',text) + f.seek(0) + f.write(text) + f.truncate() -this_directory = Path(__file__).parent -long_description = (this_directory / "README").read_text() + def generate_schema(self, schema_filename, config_filename, subpackage_name, outdir): + import sys + import subprocess + # subpackage_name = 'ismrmrdschema' + args = [sys.executable, '-m', 'xsdata', str(schema_filename), '--config', str(config_filename), '--package', subpackage_name] + subprocess.run(args) + self.fix_init_file(subpackage_name, f"{subpackage_name}/__init__.py") + destination = os.path.join(outdir, subpackage_name) + shutil.rmtree(destination, ignore_errors=True) + shutil.move(subpackage_name, destination) setup( - name='ismrmrd', version='1.14.1', - author='ISMRMRD Developers', - description='Python implementation of the ISMRMRD', - license='Public Domain', - keywords='ismrmrd', - url='https://ismrmrd.github.io', - long_description = long_description, - long_description_content_type='text/markdown', packages=find_packages(), - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Science/Research', - 'License :: Public Domain', - 'Operating System :: OS Independent', - 'Topic :: Scientific/Engineering :: Medical Science Apps.' - ], - install_requires=['xsdata>=22.12', 'numpy>=1.22.0', 'h5py>=2.3'], - setup_requires=['nose>=1.0', 'xsdata[cli]>=22.12', 'jinja2 >= 2.11'], - test_suite='nose.collector', - cmdclass={'build_py':my_build_py} + cmdclass={ + 'generate_schema': GenerateSchemaCommand + } ) diff --git a/tests/test_acquisition.py b/tests/test_acquisition.py index 988b67f..f1b7512 100644 --- a/tests/test_acquisition.py +++ b/tests/test_acquisition.py @@ -1,56 +1,49 @@ import ismrmrd import ctypes import numpy as np - import io -import nose.tools - -from nose.tools import eq_ +import pytest import test_common as common def test_encoding_counters(): idx = ismrmrd.EncodingCounters() - eq_(ctypes.sizeof(idx), 34) + assert ctypes.sizeof(idx) == 34 def test_header(): head = ismrmrd.AcquisitionHeader() - eq_(ctypes.sizeof(head), 340) + assert ctypes.sizeof(head) == 340 def test_new_instance(): acq = ismrmrd.Acquisition() - eq_(type(acq.getHead()), ismrmrd.AcquisitionHeader) - eq_(type(acq.data), np.ndarray) - eq_(acq.data.dtype, np.complex64) - eq_(type(acq.traj), np.ndarray) - eq_(acq.traj.dtype, np.float32) + assert type(acq.getHead()) == ismrmrd.AcquisitionHeader + assert type(acq.data) == np.ndarray + assert acq.data.dtype == np.complex64 + assert type(acq.traj) == np.ndarray + assert acq.traj.dtype == np.float32 def test_read_only_fields(): acq = ismrmrd.Acquisition() for field in ['number_of_samples', 'active_channels', 'trajectory_dimensions']: - try: + with pytest.raises(AttributeError): setattr(acq, field, None) - except AttributeError: - pass - else: - assert False, "assigned to read-only field of Acquisition" def test_resize(): acq = ismrmrd.Acquisition() nsamples, nchannels, ntrajdims = 128, 8, 3 acq.resize(nsamples, nchannels, ntrajdims) - eq_(acq.data.shape, (nchannels, nsamples)) - eq_(acq.traj.shape, (nsamples, ntrajdims)) + assert acq.data.shape == (nchannels, nsamples) + assert acq.traj.shape == (nsamples, ntrajdims) head = acq.getHead() - eq_(head.number_of_samples, nsamples) - eq_(head.active_channels, nchannels) - eq_(head.trajectory_dimensions, ntrajdims) + assert head.number_of_samples == nsamples + assert head.active_channels == nchannels + assert head.trajectory_dimensions == ntrajdims def test_set_head(): @@ -63,28 +56,25 @@ def test_set_head(): acq.setHead(head) - eq_(acq.data.shape, (nchannels, nsamples)) - eq_(acq.traj.shape, (nsamples, ntrajdims)) + assert acq.data.shape == (nchannels, nsamples) + assert acq.traj.shape == (nsamples, ntrajdims) def test_flags(): acq = ismrmrd.Acquisition() for i in range(1, 65): - assert not acq.is_flag_set(i), \ - "Expected flag {} to not be set.".format(i) + assert not acq.is_flag_set(i) for i in range(1, 65): acq.set_flag(i) - assert acq.is_flag_set(i), \ - "Expected flag {} to be set.".format(i) + assert acq.is_flag_set(i) for i in range(1, 65): acq.clear_flag(i) - assert not acq.is_flag_set(i), \ - "Expected flag {} to not be set.".format(i) + assert not acq.is_flag_set(i) - eq_(acq.flags, 0) + assert acq.flags == 0 for i in range(1, 65): acq.set_flag(i) @@ -92,8 +82,7 @@ def test_flags(): acq.clear_all_flags() for i in range(1, 65): - assert not acq.is_flag_set(i), \ - "Expected flag {} to not be set.".format(i) + assert not acq.is_flag_set(i) def test_clearing_unset_flag_does_not_set_other_flags(): @@ -108,7 +97,6 @@ def test_clearing_unset_flag_does_not_set_other_flags(): "Clearing an unset flag sets other flags." -@nose.tools.with_setup() def test_acquisition_equality_test_header_field(): a = common.create_random_acquisition(42) b = common.create_random_acquisition(42) @@ -120,7 +108,6 @@ def test_acquisition_equality_test_header_field(): assert a != b -@nose.tools.with_setup() def test_acquisition_equality_test_header_array(): a = common.create_random_acquisition(42) b = common.create_random_acquisition(42) @@ -132,22 +119,17 @@ def test_acquisition_equality_test_header_array(): assert a != b -@nose.tools.with_setup(setup=common.seed_random_generators) def test_initialization_from_array(): - nchannels = 32 nsamples = 256 data = common.create_random_data((nchannels, nsamples)) acquisition = ismrmrd.Acquisition.from_array(data) - assert np.array_equal(acquisition.data, data), \ - "Acquisition data does not match data used to initialize acquisition." + assert np.array_equal(acquisition.data, data) -@nose.tools.with_setup(setup=common.seed_random_generators) def test_initialization_from_arrays(): - nchannels = 32 nsamples = 256 trajectory_dimensions = 2 @@ -157,25 +139,18 @@ def test_initialization_from_arrays(): acquisition = ismrmrd.Acquisition.from_array(data, trajectory) - assert np.array_equal(acquisition.data, data), \ - "Acquisition data does not match data used to initialize acquisition." - - assert np.array_equal(acquisition.traj, trajectory), \ - "Acquisition trajectory does not match trajectory used to initialize acquisition." + assert np.array_equal(acquisition.data, data) + assert np.array_equal(acquisition.traj, trajectory) -@nose.tools.with_setup(setup=common.seed_random_generators) def test_initialization_sets_nonzero_version(): - acquisition = ismrmrd.Acquisition.from_array(common.create_random_data()) assert acquisition.version != 0, \ "Default acquisition version should not be zero." -@nose.tools.with_setup(setup=common.seed_random_generators) def test_initialization_with_header_fields(): - fields = { 'version': 2, 'measurement_uid': 123456789, @@ -192,14 +167,12 @@ def test_initialization_with_header_fields(): getattr(acquisition, field)) -@nose.tools.raises(TypeError) def test_initialization_with_illegal_header_value(): - ismrmrd.Acquisition.from_array(common.create_random_data(), version='Bad version') + with pytest.raises(TypeError): + ismrmrd.Acquisition.from_array(common.create_random_data(), version='Bad version') -@nose.tools.with_setup(setup=common.seed_random_generators) def test_serialize_and_deserialize(): - acquisition = ismrmrd.Acquisition.from_array(common.create_random_data()) with io.BytesIO() as stream: @@ -213,9 +186,7 @@ def test_serialize_and_deserialize(): assert acquisition == deserialized_acquisition -@nose.tools.with_setup(setup=common.seed_random_generators) def test_to_and_from_bytes(): - acquisition = ismrmrd.Acquisition.from_array(common.create_random_data()) deserialized_acquisition = ismrmrd.Acquisition.from_bytes(acquisition.to_bytes()) @@ -223,9 +194,7 @@ def test_to_and_from_bytes(): assert acquisition == deserialized_acquisition -@nose.tools.with_setup(setup=common.seed_random_generators) def test_serialization_with_header_fields(): - properties = common.create_random_acquisition_properties() data = common.create_random_data() trajectory = common.create_random_trajectory() @@ -236,8 +205,6 @@ def test_serialization_with_header_fields(): assert acquisition == deserialized_acquisition -@nose.tools.raises(ValueError) def test_deserialization_from_too_few_bytes(): - ismrmrd.Acquisition.from_bytes(b'') - - + with pytest.raises(ValueError): + ismrmrd.Acquisition.from_bytes(b'') diff --git a/tests/test_common.py b/tests/test_common.py index 2112572..aa95764 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,4 +1,3 @@ - import ismrmrd import numpy as np @@ -7,15 +6,6 @@ import random -def random_32bit_float(): - return numpy.random.rand(1).astype(np.float32) - - -def seed_random_generators(seed=42): - numpy.random.seed(seed) - random.seed(seed) - - def random_tuple(size, random_fn): return tuple([random_fn() for _ in range(0, size)]) @@ -33,15 +23,15 @@ def create_random_acquisition_properties(): 'discard_post': random.randint(0, 1 << 16), 'center_sample': random.randint(0, 1 << 16), 'encoding_space_ref': random.randint(0, 1 << 16), - 'sample_time_us': random_32bit_float(), - 'position': random_tuple(3, random_32bit_float), - 'read_dir': random_tuple(3, random_32bit_float), - 'phase_dir': random_tuple(3, random_32bit_float), - 'slice_dir': random_tuple(3, random_32bit_float), - 'patient_table_position': random_tuple(3, random_32bit_float), + 'sample_time_us': random.random(), + 'position': random_tuple(3, random.random), + 'read_dir': random_tuple(3, random.random), + 'phase_dir': random_tuple(3, random.random), + 'slice_dir': random_tuple(3, random.random), + 'patient_table_position': random_tuple(3, random.random), 'idx': ismrmrd.EncodingCounters(), 'user_int': random_tuple(8, lambda: random.randint(0, 1 << 31)), - 'user_float': random_tuple(8, random_32bit_float) + 'user_float': random_tuple(8, random.random) } @@ -49,12 +39,12 @@ def create_random_image_properties(): return { 'flags': random.randint(0, 1 << 64), 'measurement_uid': random.randint(0, 1 << 32), - 'field_of_view': random_tuple(3, random_32bit_float), - 'position': random_tuple(3, random_32bit_float), - 'read_dir': random_tuple(3, random_32bit_float), - 'phase_dir': random_tuple(3, random_32bit_float), - 'slice_dir': random_tuple(3, random_32bit_float), - 'patient_table_position': random_tuple(3, random_32bit_float), + 'field_of_view': random_tuple(3, random.random), + 'position': random_tuple(3, random.random), + 'read_dir': random_tuple(3, random.random), + 'phase_dir': random_tuple(3, random.random), + 'slice_dir': random_tuple(3, random.random), + 'patient_table_position': random_tuple(3, random.random), 'average': random.randint(0, 1 << 16), 'slice': random.randint(0, 1 << 16), 'contrast': random.randint(0, 1 << 16), @@ -66,7 +56,7 @@ def create_random_image_properties(): 'image_index': random.randint(0, 1 << 16), 'image_series_index': random.randint(0, 1 << 16), 'user_int': random_tuple(8, lambda: random.randint(0, 1 << 31)), - 'user_float': random_tuple(8, random_32bit_float), + 'user_float': random_tuple(8, random.random), } @@ -77,7 +67,7 @@ def create_random_waveform_properties(): 'waveform_id': random.randint(0, 1 << 16), 'scan_counter': random.randint(0, 1 << 32), 'time_stamp': random.randint(0, 1 << 32), - 'sample_time_us': random_32bit_float() + 'sample_time_us': random.random() } @@ -120,7 +110,7 @@ def create_random_image(seed=42): header = create_random_image_properties() image = ismrmrd.Image.from_array(data, **header) - image.meta = {"Random attribute": random.randint(0,1000)} + image.meta = {"Random attribute": str(random.randint(0,1000))} return image @@ -135,3 +125,47 @@ def create_random_waveform(seed=42): header = create_random_waveform_properties() return ismrmrd.Waveform.from_array(data, **header) + + +def compare_acquisitions(a, b): + assert type(a) == type(b) + assert a.data.shape == b.data.shape + assert np.allclose(a.data, b.data) + assert a.traj.shape == b.traj.shape + assert np.allclose(a.traj, b.traj) + for key in a.__dict__: + if key not in ['data', 'traj']: + aval = getattr(a, key) + bval = getattr(b, key) + if isinstance(aval, np.ndarray) and isinstance(bval, np.ndarray): + assert np.array_equal(aval, bval), f"Field '{key}' does not match: {aval} != {bval}" + else: + assert aval == bval, f"Field '{key}' does not match: {aval} != {bval}" + + +def compare_images(a, b): + assert type(a) == type(b) + assert a.data.shape == b.data.shape + assert np.allclose(a.data, b.data) + for key in a.__dict__: + if key != 'data': + aval = getattr(a, key) + bval = getattr(b, key) + if isinstance(aval, np.ndarray) and isinstance(bval, np.ndarray): + assert np.array_equal(aval, bval), f"Field '{key}' does not match: {aval} != {bval}" + else: + assert aval == bval, f"Field '{key}' does not match: {aval} != {bval}" + + +def compare_waveforms(a, b): + assert type(a) == type(b) + assert a.data.shape == b.data.shape + assert np.array_equal(a.data, b.data) + for key in a.__dict__: + if key != 'data': + aval = getattr(a, key) + bval = getattr(b, key) + if isinstance(aval, np.ndarray) and isinstance(bval, np.ndarray): + assert np.array_equal(aval, bval), f"Field '{key}' does not match: {aval} != {bval}" + else: + assert aval == bval, f"Field '{key}' does not match: {aval} != {bval}" diff --git a/tests/test_file.py b/tests/test_file.py index 9bbc8d4..c92f45f 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -1,392 +1,264 @@ import ismrmrd - -import nose.tools - import shutil import os.path import tempfile - import numpy - +import pytest from test_common import * -temp_dir = None - - -def create_temp_dir(): +@pytest.fixture(autouse=True) +def temp_dir_fixture(): global temp_dir temp_dir = tempfile.mkdtemp(prefix='ismrmrd-python-', suffix='-test') - - -def delete_temp_dir(): + yield shutil.rmtree(temp_dir, ignore_errors=True) - def random_acquisitions(n): yield from (create_random_acquisition(i) for i in range(n)) - def random_waveforms(n): yield from (create_random_waveform(i) for i in range(n)) - def random_images(n): yield from (create_random_image(i) for i in range(n)) - -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_returns_none_when_no_acquisitions_present(): - filename = os.path.join(temp_dir, "acquisitions.h5") acquisitions = list(random_acquisitions(10)) - with ismrmrd.File(filename) as file: dataset = file['dataset'] - assert not dataset.has_acquisitions() assert dataset.acquisitions is None - dataset.acquisitions = acquisitions - with ismrmrd.File(filename) as file: dataset = file['dataset'] - assert dataset.has_acquisitions() assert not (dataset.acquisitions is None) - -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_can_read_and_write_acquisitions(): - filename = os.path.join(temp_dir, "acquisitions.h5") acquisitions = list(random_acquisitions(10)) - with ismrmrd.File(filename) as file: dataset = file['dataset'] dataset.acquisitions = acquisitions - with ismrmrd.File(filename) as file: dataset = file['dataset'] for a, b in zip(acquisitions, dataset.acquisitions): assert a == b - -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_can_delete_acquisitions(): - filename = os.path.join(temp_dir, "acquisitions.h5") - with ismrmrd.File(filename) as file: dataset = file['dataset'] - assert dataset.acquisitions is None del dataset.acquisitions assert dataset.acquisitions is None - dataset.acquisitions = random_acquisitions(10) - assert not (dataset.acquisitions is None) del dataset.acquisitions assert dataset.acquisitions is None - -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_can_access_random_acquisition(): - filename = os.path.join(temp_dir, "acquisitions.h5") acquisitions = list(random_acquisitions(256)) - with ismrmrd.File(filename) as file: dataset = file['dataset'] dataset.acquisitions = acquisitions - with ismrmrd.File(filename) as file: dataset = file['dataset'] assert acquisitions[255] == dataset.acquisitions[255] - -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_can_access_random_acquisition_slice(): - filename = os.path.join(temp_dir, "acquisitions.h5") acquisitions = list(random_acquisitions(256)) - with ismrmrd.File(filename) as file: dataset = file['dataset'] dataset.acquisitions = acquisitions - with ismrmrd.File(filename) as file: dataset = file['dataset'] - for a, b in zip(acquisitions[250:255], dataset.acquisitions[250:255]): assert a == b - -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_can_write_random_acquisition(): - filename = os.path.join(temp_dir, "acquisitions.h5") acquisitions = list(random_acquisitions(256)) acquisition = create_random_acquisition() - with ismrmrd.File(filename) as file: dataset = file['dataset'] dataset.acquisitions = acquisitions dataset.acquisitions[200] = acquisition - with ismrmrd.File(filename) as file: dataset = file['dataset'] assert acquisition == dataset.acquisitions[200] -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_can_append_acquisitions(): - filename = os.path.join(temp_dir, "acquisitions.h5") acquisitions = list(random_acquisitions(10)) acquisitions2 = list(random_acquisitions(10)) acquisition3 = next(random_acquisitions(1)) - combined = acquisitions + acquisitions2 + [acquisition3] - with ismrmrd.File(filename) as file: dataset = file['dataset'] dataset.acquisitions = acquisitions dataset.acquisitions.extend(acquisitions2) dataset.acquisitions.append(acquisition3) - with ismrmrd.File(filename) as file: dataset = file['dataset'] for a, b in zip(combined, dataset.acquisitions): assert a == b -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_can_write_random_acquisition_slice(): - filename = os.path.join(temp_dir, "acquisitions.h5") acquisitions = list(random_acquisitions(256)) slice = list(random_acquisitions(3)) - with ismrmrd.File(filename) as file: dataset = file['dataset'] dataset.acquisitions = acquisitions dataset.acquisitions[150:153] = slice - with ismrmrd.File(filename) as file: dataset = file['dataset'] - for a, b in zip(slice, dataset.acquisitions[150:153]): assert a == b - -@nose.tools.raises(TypeError) -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_cannot_write_mismatched_slice(): - filename = os.path.join(temp_dir, "acquisitions.h5") acquisitions = list(random_acquisitions(256)) slice = list(random_acquisitions(3)) + with pytest.raises(TypeError): + with ismrmrd.File(filename) as file: + dataset = file['dataset'] + dataset.acquisitions = acquisitions + dataset.acquisitions[150:155] = slice - with ismrmrd.File(filename) as file: - dataset = file['dataset'] - dataset.acquisitions = acquisitions - dataset.acquisitions[150:155] = slice - - -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_can_read_and_write_waveforms(): - filename = os.path.join(temp_dir, "waveforms.h5") waveforms = list(random_waveforms(10)) - with ismrmrd.File(filename) as file: dataset = file['dataset'] dataset.waveforms = waveforms - with ismrmrd.File(filename) as file: dataset = file['dataset'] for a, b in zip(waveforms, dataset.waveforms): assert a == b -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_can_append_waveforms(): - filename = os.path.join(temp_dir, "waveforms.h5") waveforms = list(random_waveforms(10)) waveforms2 = list(random_waveforms(10)) waveform3 = next(random_waveforms(1)) - combined = waveforms + waveforms2 + [waveform3] - with ismrmrd.File(filename) as file: dataset = file['dataset'] dataset.waveforms = waveforms dataset.waveforms.extend(waveforms2) dataset.waveforms.append(waveform3) - with ismrmrd.File(filename) as file: dataset = file['dataset'] for a, b in zip(combined, dataset.waveforms): assert a == b - -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_can_read_and_write_images(): - filename = os.path.join(temp_dir, "images.h5") images = list(random_images(10)) - with ismrmrd.File(filename) as file: imageset = file['dataset/image_1'] imageset.images = images - with ismrmrd.File(filename) as file: imageset = file['dataset/image_1'] for a, b in zip(images, imageset.images): assert a == b - -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_can_read_random_image(): - filename = os.path.join(temp_dir, "images.h5") images = list(random_images(10)) - with ismrmrd.File(filename) as file: imageset = file['dataset/image_1'] imageset.images = images - with ismrmrd.File(filename) as file: imageset = file['dataset/image_1'] - assert images[8] == imageset.images[8] - -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_can_write_random_image(): - filename = os.path.join(temp_dir, "images.h5") image = create_random_image() - with ismrmrd.File(filename) as file: imageset = file['dataset/image_1'] imageset.images = random_images(10) imageset.images[6] = image - with ismrmrd.File(filename) as file: imageset = file['dataset/image_1'] - assert image == imageset.images[6] - -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_can_read_image_slice(): - filename = os.path.join(temp_dir, "images.h5") images = list(random_images(10)) - with ismrmrd.File(filename) as file: imageset = file['dataset/image_1'] imageset.images = images - with ismrmrd.File(filename) as file: imageset = file['dataset/image_1'] for a, b in zip(images[5:10], imageset.images[5:10]): assert a == b - -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_can_write_image_slice(): - filename = os.path.join(temp_dir, "images.h5") images = list(random_images(10)) - with ismrmrd.File(filename) as file: imageset = file['dataset/image_1'] imageset.images = random_images(32) imageset.images[5:15] = images - with ismrmrd.File(filename) as file: imageset = file['dataset/image_1'] - for a, b in zip(images, imageset.images[5:15]): assert a == b - -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_can_list_contained_images(): - filename = os.path.join(temp_dir, "find_file.h5") - with ismrmrd.File(filename) as file: dataset = file['dataset'] dataset.acquisitions = random_acquisitions(10) - imageset = file['dataset/image_1'] imageset.images = random_images(1) - imageset = file['dataset/image_2'] imageset.images = random_images(2) - imageset = file['dataset/nested/image_3'] imageset.images = [create_random_image(3)] - with ismrmrd.File(filename) as file: assert(file.find_images() == {'dataset/image_1', 'dataset/image_2', 'dataset/nested/image_3'}) assert(file.find_data() == {'dataset'}) - - -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_can_list_keys(): - filename = os.path.join(temp_dir, "keys.h5") - with ismrmrd.File(filename) as file: dataset = file['dataset'] dataset.acquisitions = random_acquisitions(10) - - dataset2 = file['dataset2'] dataset2.acquisitions = random_acquisitions(10) - with ismrmrd.File(filename) as file: assert(file.keys() == {'dataset', 'dataset2' }) -@nose.tools.raises(TypeError) -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_cannot_write_image_on_data(): - filename = os.path.join(temp_dir, "file.h5") - with ismrmrd.File(filename) as file: dataset = file['dataset'] dataset.acquisitions = random_acquisitions(10) + with pytest.raises(TypeError): + with ismrmrd.File(filename) as file: + dataset = file['dataset'] + dataset.images = random_images(1) - with ismrmrd.File(filename) as file: - dataset = file['dataset'] - dataset.images = random_images(1) - - -@nose.tools.raises(TypeError) -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_cannot_write_data_on_image(): - filename = os.path.join(temp_dir, "file.h5") - with ismrmrd.File(filename) as file: dataset = file['dataset'] dataset.images = random_images(1) + with pytest.raises(TypeError): + with ismrmrd.File(filename) as file: + dataset = file['dataset'] + dataset.acquisitions = random_acquisitions(10) - with ismrmrd.File(filename) as file: - dataset = file['dataset'] - dataset.acquisitions = random_acquisitions(10) - - -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_can_rewrite_data_and_images(): - filename = os.path.join(temp_dir, "file.h5") - with ismrmrd.File(filename) as file: dataset = file['dataset'] dataset.acquisitions = random_acquisitions(20) @@ -394,14 +266,12 @@ def test_file_can_rewrite_data_and_images(): dataset.waveforms = random_waveforms(10) dataset.waveforms = random_waveforms(5) dataset.acquisitions = random_acquisitions(5) - with ismrmrd.File(filename) as file: imageset = file['dataset/images'] imageset.images = random_images(1) imageset.images = random_images(2) imageset.images = random_images(3) - example_header = """ @@ -439,17 +309,12 @@ def test_file_can_rewrite_data_and_images(): """ - -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_file_can_read_and_write_headers(): - filename = os.path.join(temp_dir, "file.h5") header = ismrmrd.xsd.CreateFromDocument(example_header) - with ismrmrd.File(filename) as file: dataset = file['dataset'] dataset.header = header - with ismrmrd.File(filename) as file: dataset = file['dataset'] assert ismrmrd.xsd.ToXML(header) == ismrmrd.xsd.ToXML(dataset.header) diff --git a/tests/test_hdf5.py b/tests/test_hdf5.py index 8117fdc..ed96fa9 100644 --- a/tests/test_hdf5.py +++ b/tests/test_hdf5.py @@ -1,8 +1,5 @@ - - import ismrmrd - -import nose.tools +import pytest import random import numpy.random @@ -16,24 +13,19 @@ from test_common import * -temp_dir = None - - -def create_temp_dir(): +@pytest.fixture(autouse=True) +def temp_dir_fixture(): global temp_dir temp_dir = tempfile.mkdtemp(prefix='ismrmrd-python-', suffix='-test') - - -def delete_temp_dir(): + yield shutil.rmtree(temp_dir, ignore_errors=True) -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_open_fresh_hdf5(): filename = os.path.join(temp_dir, 'open_fresh.h5') ismrmrd.Dataset(filename) -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) + def test_hdf5_fileinfo(): filename = os.path.join(temp_dir, 'stuff.h5') @@ -43,10 +35,9 @@ def test_hdf5_fileinfo(): other_dataset = ismrmrd.Dataset(filename, 'other_dataset') other_dataset.append_acquisition(create_random_acquisition()) - assert(ismrmrd.hdf5.fileinfo(filename) == ['dataset', 'other_dataset']) + assert ismrmrd.hdf5.fileinfo(filename) == ['dataset', 'other_dataset'] -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_read_and_write_acquisitions_to_hdf5(): filename = os.path.join(temp_dir, 'read_write_acquisitions.h5') @@ -64,10 +55,10 @@ def test_read_and_write_acquisitions_to_hdf5(): read_acquisitions = [dataset.read_acquisition(i) for i in range(0, nacquisitions)] - map(lambda acq_a, acq_b: compare_acquisitions(acq_a, acq_b), acquisitions, read_acquisitions) + for acq_a, acq_b in zip(acquisitions, read_acquisitions): + compare_acquisitions(acq_a, acq_b) -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_read_and_write_images_to_hdf5(): filename = os.path.join(temp_dir, 'read_write_images.h5') @@ -85,10 +76,10 @@ def test_read_and_write_images_to_hdf5(): read_images = [dataset.read_image('images', i) for i in range(0, nimages)] - map(lambda img_a, img_b: compare_images(img_a, img_b), images, read_images) + for img_a, img_b in zip(images, read_images): + compare_images(img_a, img_b) -@nose.tools.with_setup(create_temp_dir, delete_temp_dir) def test_read_and_write_waveforms_to_hdf5(): filename = os.path.join(temp_dir, 'read_write_waveforms.h5') @@ -106,7 +97,9 @@ def test_read_and_write_waveforms_to_hdf5(): read_waveforms = [dataset.read_waveform(i) for i in range(0, nwaveforms)] - map(lambda wav_a, wav_b: compare_waveforms(wav_a, wav_b), waveforms, read_waveforms) + for wav_a, wav_b in zip(waveforms, read_waveforms): + compare_waveforms(wav_a, wav_b) + def test_waveform_hdf5_size(): assert ismrmrd.hdf5.waveform_header_dtype.itemsize == 40 diff --git a/tests/test_image.py b/tests/test_image.py index c16f5fa..17ae87e 100644 --- a/tests/test_image.py +++ b/tests/test_image.py @@ -4,9 +4,7 @@ import ctypes import numpy as np import io - -import nose.tools -from nose.tools import eq_ +import pytest import test_common as common @@ -17,10 +15,10 @@ def test_header(): def test_new_instance(): img = ismrmrd.Image() - eq_(type(img.getHead()), ismrmrd.ImageHeader) - eq_(img.getHead().data_type, ismrmrd.DATATYPE_CXFLOAT) - eq_(type(img.data), np.ndarray) - eq_(img.data.dtype, np.complex64) + assert type(img.getHead()) == ismrmrd.ImageHeader + assert img.getHead().data_type == ismrmrd.DATATYPE_CXFLOAT + assert type(img.data) == np.ndarray + assert img.data.dtype == np.complex64 attr = "\nrandoblah" @@ -28,17 +26,13 @@ def test_new_instance(): head.attribute_string_len = len(attr) # must set attribute_string_len head.data_type = ismrmrd.DATATYPE_CXFLOAT # must set data_type img = ismrmrd.Image(head, attribute_string=attr) - eq_(img.attribute_string, attr) + assert img.attribute_string == attr def test_read_only_fields(): img = ismrmrd.Image() for field in ['data_type', 'matrix_size', 'channels', 'attribute_string_len']: - try: + with pytest.raises(AttributeError): setattr(img, field, None) - except AttributeError: - pass - else: - raise Exception("Setting read-only attribute did not raise exception.") def test_set_head(): @@ -53,164 +47,92 @@ def test_set_head(): img.setHead(head) - eq_(img.data.shape, (nchan, nz, ny, nx)) - eq_(img.data_type, ismrmrd.DATATYPE_CXDOUBLE) - eq_(img.data.dtype, np.complex128) + assert img.data.shape == (nchan, nz, ny, nx) + assert img.data_type == ismrmrd.DATATYPE_CXDOUBLE + assert img.data.dtype == np.complex128 def test_resize(): pass -@nose.tools.with_setup(setup=common.seed_random_generators) def test_initialization_sets_nonzero_version(): - image = ismrmrd.Image.from_array(common.create_random_array((128, 128), dtype=np.float32)) + assert image.version != 0 - assert image.version != 0, \ - "Default image version should not be zero." - - -@nose.tools.with_setup(setup=common.seed_random_generators) def test_initialization_with_array(): - image_data = common.create_random_array((256, 128), dtype=np.float32) image = ismrmrd.Image.from_array(image_data) + assert np.array_equal(image_data.transpose(), image.data.squeeze()) - assert np.array_equal(image_data.transpose(), image.data.squeeze()), \ - "Image data does not match data used to initialize image." - - -@nose.tools.with_setup(setup=common.seed_random_generators) def test_initialization_with_array_and_acquisition(): - acquisition = common.create_random_acquisition() - image_data = common.create_random_array((256, 128), dtype=np.float32) image = ismrmrd.Image.from_array(image_data, acquisition=acquisition) + assert np.array_equal(image_data.transpose(), image.data.squeeze()) + for field in ['version', 'measurement_uid', 'position', 'read_dir', 'phase_dir', 'slice_dir', 'patient_table_position', 'acquisition_time_stamp', 'physiology_time_stamp']: + assert bytes(getattr(acquisition, field)) == bytes(getattr(image, field)) - assert np.array_equal(image_data.transpose(), image.data.squeeze()), \ - "Image data does not match data used to initialize image." - - for field in ['version', - 'measurement_uid', - 'position', - 'read_dir', - 'phase_dir', - 'slice_dir', - 'patient_table_position', - 'acquisition_time_stamp', - 'physiology_time_stamp']: - - assert bytes(getattr(acquisition, field)) == bytes(getattr(image, field)), \ - "Acquisition header field not copied to image." - - -@nose.tools.with_setup(setup=common.seed_random_generators) def test_initialization_with_array_and_header_properties(): - properties = common.create_random_image_properties() image_data = common.create_random_array((256, 512), dtype=np.float32) - image = ismrmrd.Image.from_array(image_data, **properties) - for field in properties: - try: - assert all(map(lambda a, b: a == b, - properties.get(field), - getattr(image, field))), \ - "Image property doesn't match initialization value: " + field - except TypeError: - assert properties.get(field) == getattr(image, field), \ - "Image property doesn't match initialization value: " + field - + expected = properties.get(field) + actual = getattr(image, field) + # If both are sequences and contain floats, use np.allclose + if isinstance(expected, (tuple, list)) and hasattr(actual, '__len__') and len(expected) == len(actual): + try: + expected_arr = np.array(expected, dtype=float) + actual_arr = np.array(actual, dtype=float) + assert np.allclose(expected_arr, actual_arr), f"Field '{field}' does not match: {expected_arr} != {actual_arr}" + except Exception: + for i, (a, b) in enumerate(zip(expected, actual)): + assert a == b, f"Field '{field}' index {i} does not match: {a} != {b}" + else: + assert expected == actual, f"Field '{field}' does not match: {expected} != {actual}" -@nose.tools.with_setup(setup=common.seed_random_generators) def test_initialization_with_2d_image(): - image_data = common.create_random_array((128, 64), dtype=np.float32) image = ismrmrd.Image.from_array(image_data) + assert np.array_equal(image_data.transpose(), image.data.squeeze()) + assert image.channels == 1 + assert image.matrix_size == (1, 64, 128) - assert np.array_equal(image_data.transpose(), image.data.squeeze()), \ - "Image data does not match data used to initialize image." - - assert image.channels == 1, \ - "Unexpected number of channels: {}".format(image.channels) - - assert image.matrix_size == (1, 64, 128), \ - "Unexpected matrix size: {}".format(image.matrix_size) - - -@nose.tools.with_setup(setup=common.seed_random_generators) def test_initialization_with_3d_image(): - image_data = common.create_random_array((128, 64, 32), dtype=np.float32) image = ismrmrd.Image.from_array(image_data) + assert np.array_equal(image_data.transpose(), image.data.squeeze()) + assert image.channels == 1 + assert image.matrix_size == (32, 64, 128) - assert np.array_equal(image_data.transpose(), image.data.squeeze()), \ - "Image data does not match data used to initialize image." - - assert image.channels == 1, \ - "Unexpected number of channels: {}".format(image.channels) - - assert image.matrix_size == (32, 64, 128), \ - "Unexpected matrix size: {}".format(image.matrix_size) - - -@nose.tools.with_setup(setup=common.seed_random_generators) def test_initialization_with_3d_image_and_channels(): - image_data = common.create_random_array((128, 64, 32, 16), dtype=np.float32) image = ismrmrd.Image.from_array(image_data) + assert np.array_equal(image_data.transpose(), image.data.squeeze()) + assert image.channels == 16 + assert image.matrix_size == (32, 64, 128) - assert np.array_equal(image_data.transpose(), image.data.squeeze()), \ - "Image data does not match data used to initialize image." - - assert image.channels == 16, \ - "Unexpected number of channels: {}".format(image.channels) - - assert image.matrix_size == (32, 64, 128), \ - "Unexpected matrix size: {}".format(image.matrix_size) - - -@nose.tools.with_setup(setup=common.seed_random_generators) def test_serialize_and_deserialize(): - image_data = common.create_random_array((128, 128), dtype=np.float32) image = ismrmrd.Image.from_array(image_data) - with io.BytesIO(b'') as stream: image.serialize_into(stream.write) - stream.seek(0) - read_image = ismrmrd.Image.deserialize_from(stream.read) - assert image == read_image - -@nose.tools.with_setup(setup=common.seed_random_generators) def test_to_and_from_bytes(): - image_data = common.create_random_array((128, 128), dtype=np.float32) image = ismrmrd.Image.from_array(image_data) - read_image = ismrmrd.Image.from_bytes(image.to_bytes()) - assert image == read_image - -@nose.tools.with_setup(setup=common.seed_random_generators) def test_serialization_with_header_fields(): - image_data = common.create_random_array((128, 128), dtype=np.float32) image = ismrmrd.Image.from_array(image_data, **common.create_random_image_properties()) - read_image = ismrmrd.Image.from_bytes(image.to_bytes()) - assert image == read_image - -@nose.tools.raises(ValueError) -@nose.tools.with_setup(setup=common.seed_random_generators) def test_serialization_from_too_few_bytes(): - ismrmrd.Image.from_bytes(b'') + with pytest.raises(ValueError): + ismrmrd.Image.from_bytes(b'') diff --git a/tests/test_waveform.py b/tests/test_waveform.py index 9e25234..b0a04d0 100644 --- a/tests/test_waveform.py +++ b/tests/test_waveform.py @@ -1,19 +1,14 @@ - import ismrmrd import ctypes - import numpy as np import numpy.random - import io - -import nose.tools +import pytest import test_common as common -@nose.tools.with_setup(setup=common.seed_random_generators) -def test_initialization_from_array(): +def test_initialization_from_array(): nchannels = 32 nsamples = 256 @@ -25,38 +20,38 @@ def test_initialization_from_array(): "Waveform data does not match data used to initialize waveform." -@nose.tools.with_setup(setup=common.seed_random_generators) def test_initialization_sets_nonzero_version(): - waveform = common.create_random_waveform() assert waveform.version != 0, \ "Default acquisition version should not be zero." -@nose.tools.with_setup(setup=common.seed_random_generators) def test_initialization_with_header_fields(): - fields = common.create_random_waveform_properties() waveform = ismrmrd.Waveform.from_array(common.create_random_waveform_data(), **fields) for field in fields: - assert fields.get(field) == getattr(waveform, field), \ - "Field {} not preserved by waveform. ({} != {})".format(field, - fields.get(field), - getattr(waveform, field)) + expected = fields.get(field) + actual = getattr(waveform, field) + # Use np.isclose for scalar float comparison + if isinstance(expected, float) or isinstance(actual, float): + assert np.isclose(float(expected), float(actual)), f"Field '{field}' does not match: {expected} != {actual}" + elif isinstance(expected, (tuple, list)) and hasattr(actual, '__len__') and len(expected) == len(actual): + for i, (a, b) in enumerate(zip(expected, actual)): + assert a == b, f"Field '{field}' index {i} does not match: {a} != {b}" + else: + assert expected == actual, f"Field '{field}' does not match: {expected} != {actual}" -@nose.tools.raises(TypeError) def test_initialization_with_illegal_header_value(): - ismrmrd.Waveform.from_array(common.create_random_waveform_data(), version='Bad version') + with pytest.raises(TypeError): + ismrmrd.Waveform.from_array(common.create_random_waveform_data(), version='Bad version') -@nose.tools.with_setup(setup=common.seed_random_generators) def test_serialize_and_deserialize(): - waveform = common.create_random_waveform() with io.BytesIO() as stream: @@ -70,9 +65,7 @@ def test_serialize_and_deserialize(): assert waveform == deserialized_waveform -@nose.tools.with_setup(setup=common.seed_random_generators) def test_to_and_from_bytes(): - waveform = common.create_random_waveform() deserialized_waveform = ismrmrd.Waveform.from_bytes(waveform.to_bytes()) @@ -80,9 +73,7 @@ def test_to_and_from_bytes(): assert waveform == deserialized_waveform -@nose.tools.with_setup(setup=common.seed_random_generators) def test_serialization_with_header_fields(): - properties = common.create_random_waveform_properties() data = common.create_random_waveform_data() @@ -92,9 +83,9 @@ def test_serialization_with_header_fields(): assert waveform == deserialized_waveform -@nose.tools.raises(ValueError) def test_deserialization_from_too_few_bytes(): - ismrmrd.Acquisition.from_bytes(b'') + with pytest.raises(ValueError): + ismrmrd.Acquisition.from_bytes(b'') def test_waveformheader_size():