From 983b54e360490f10c72c22566fff05b024314ace Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Wed, 20 Mar 2024 13:04:21 -0500 Subject: [PATCH 01/22] WIP refactor --- openpmd_updater/Updater.py | 49 ++++++++---- openpmd_updater/__init__.py | 5 ++ openpmd_updater/backends/HDF5.py | 19 +++-- openpmd_updater/backends/IBackend.py | 3 +- openpmd_updater/cli.py | 80 ++++++++----------- openpmd_updater/transforms/ITransform.py | 18 +++-- .../transforms/v2_0_0/DataOrder.py | 22 ++--- .../transforms/v2_0_0/ExtensionString.py | 11 +-- openpmd_updater/transforms/v2_0_0/GridUnit.py | 16 ++-- .../transforms/v2_0_0/ParticleBoundary.py | 8 +- openpmd_updater/transforms/v2_0_0/Version.py | 2 +- openpmd_updater/utils/__init__.py | 0 pyproject.toml | 36 +++++++++ requirements.txt | 8 -- setup.py | 42 ---------- 15 files changed, 162 insertions(+), 157 deletions(-) delete mode 100644 openpmd_updater/utils/__init__.py create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 setup.py diff --git a/openpmd_updater/Updater.py b/openpmd_updater/Updater.py index 10561bd..a5e3c65 100644 --- a/openpmd_updater/Updater.py +++ b/openpmd_updater/Updater.py @@ -5,10 +5,16 @@ Authors: Axel Huebl License: ISC """ + import packaging.version -from openpmd_updater.backends.HDF5 import HDF5 -from openpmd_updater.transforms.v2_0_0 import \ - DataOrder, GridUnit, ParticleBoundary, ExtensionString, Version +from .backends.HDF5 import HDF5 +from .transforms.v2_0_0 import ( + DataOrder, + GridUnit, + ParticleBoundary, + ExtensionString, + Version, +) class Updater(object): @@ -18,9 +24,9 @@ class Updater(object): (see: openpmd_updater.backend.IBackend) for a given file and applies a set of transformations (see: openpmd_updater.transforms.ITransform) on it. """ + def __init__(self, filename, verbose=False): - """Open a file with suiteable file format backend. - """ + """Open a file with suiteable file format backend.""" # supported file format backends self.backends = [HDF5] @@ -28,12 +34,12 @@ def __init__(self, filename, verbose=False): # keys: "to_version" of targeted openPMD standard version # values: ordered list of transforms self.updates = { - "2.0.0" : [ + "2.0.0": [ DataOrder.DataOrder, # must be before move of particleBoundary GridUnit.GridUnit, ParticleBoundary.ParticleBoundary, ExtensionString.ExtensionString, - Version.Version # must be last + Version.Version, # must be last ] } @@ -49,8 +55,9 @@ def __init__(self, filename, verbose=False): self.fb = b(filename) break if self.fb is None: - raise RuntimeError("No matching file format backend found for " - "{0}!".format(filename)) + raise RuntimeError( + "No matching file format backend found for " "{0}!".format(filename) + ) def update(self, new_version="2.0.0", in_place=True): """Perform update to new version of the openPMD standard""" @@ -60,8 +67,11 @@ def update(self, new_version="2.0.0", in_place=True): # check if new version is known by the updater update_valid = False if new_version not in self.updates.keys(): - raise RuntimeError("Only updates to openPMD standard(s) '{0}' are " - "supported!".format(" ".join(self.updates.keys()))) + raise RuntimeError( + "Only updates to openPMD standard(s) '{0}' are " "supported!".format( + " ".join(self.updates.keys()) + ) + ) # select proper update depending on initial version # note: multiple updates over intermediate openPMD standard releases are possible @@ -70,18 +80,23 @@ def update(self, new_version="2.0.0", in_place=True): if file_version <= packaging.version.parse("1.1.0"): update_valid = True if self.verbose: - print("[Updater] Performing update from {0} to " - "{1}".format(file_version, new_version)) + print( + "[Updater] Performing update from {0} to " "{1}".format( + file_version, new_version + ) + ) for t in self.updates[new_version]: self.fb.cd(None) next_transform = t(self.fb) if self.verbose: name, desc = t.name - print("[Updater] Transform {0}: " - "{1}".format(name, desc)) + print("[Updater] Transform {0}: " "{1}".format(name, desc)) next_transform.transform(in_place) file_version = packaging.version.parse(new_version) if not update_valid: - raise RuntimeError("Unsupported openPMD standard '{0}' in file " - "'{1}':".format(file_version, self.filename)) + raise RuntimeError( + "Unsupported openPMD standard '{0}' in file " "'{1}':".format( + file_version, self.filename + ) + ) diff --git a/openpmd_updater/__init__.py b/openpmd_updater/__init__.py index e69de29..69d8108 100644 --- a/openpmd_updater/__init__.py +++ b/openpmd_updater/__init__.py @@ -0,0 +1,5 @@ +"⏫ Update openPMD files to newer versions of the openPMD standard" + +from .Updater import * +from .backends import * +from .transforms import * diff --git a/openpmd_updater/backends/HDF5.py b/openpmd_updater/backends/HDF5.py index 12d8050..42deb71 100644 --- a/openpmd_updater/backends/HDF5.py +++ b/openpmd_updater/backends/HDF5.py @@ -5,8 +5,10 @@ Authors: Axel Huebl License: ISC """ -from openpmd_updater.backends.IBackend import IBackend + +from .IBackend import IBackend import packaging.version + try: import h5py as h5 except: @@ -15,14 +17,14 @@ class HDF5(IBackend): """HDF5 File handling.""" - + def __init__(self, filename): """Open a HDF5 file""" if h5 is None: raise RuntimeError("h5py is not installed!") if self.can_handle(filename): - self.fh = h5.File(filename, 'r+') + self.fh = h5.File(filename, "r+") self.pwd = self.fh["/"] else: raise RuntimeError("HDF5 backend can not open non-HDF5 files!") @@ -30,9 +32,9 @@ def __init__(self, filename): @staticmethod def can_handle(filename): """Check if filename is a HDF5 file.""" - signature = b'\x89HDF\r\n\x1a\n' + signature = b"\x89HDF\r\n\x1a\n" try: - with open(filename, 'rb') as f: + with open(filename, "rb") as f: header = f.read(8) return header == signature except: @@ -94,8 +96,11 @@ def move(self, old_path, new_path): self.pwd.attrs[new_path] = self.pwd.attrs[old_path] self.delete(old_path) else: - NotImplementedError("Move is not implemented for " - "'{0}' at '{1}'!".format(type(obj), old_path)) + NotImplementedError( + "Move is not implemented for " "'{0}' at '{1}'!".format( + type(obj), old_path + ) + ) def del_goup(self, name): """Remove a group, attribute or dataset""" diff --git a/openpmd_updater/backends/IBackend.py b/openpmd_updater/backends/IBackend.py index abf0567..5e01335 100644 --- a/openpmd_updater/backends/IBackend.py +++ b/openpmd_updater/backends/IBackend.py @@ -5,6 +5,7 @@ Authors: Axel Huebl License: ISC """ + from abc import abstractmethod @@ -38,7 +39,7 @@ def pwd(self, path): """Return current directory in file.""" raise NotImplementedError("Directory pwd not implemented!") - @abstractmethod + @abstractmethod def list_groups(self, path): """Return a list of groups in a path""" raise NotImplementedError("Group listing not implemented!") diff --git a/openpmd_updater/cli.py b/openpmd_updater/cli.py index e93801c..51a55ca 100644 --- a/openpmd_updater/cli.py +++ b/openpmd_updater/cli.py @@ -1,61 +1,45 @@ """ This file is part of the openPMD-updater. -Copyright 2018 openPMD contributors -Authors: Axel Huebl +Copyright 2023 openPMD contributors +Authors: Axel Huebl, Sajid Ali License: ISC """ -import packaging.version -from openpmd_updater.Updater import Updater -import sys, getopt, os.path - - -def help(): - """ Print usage information for the command line interface""" - print('This is the openPMD updater HDF5 files.\n') - print('It allows to update openPMD flavored files from openPMD standard' - ' {0} to {1}'.format(111,222)) - print('Usage:\n openPMD_createExamples_h5 -i [-v] [-b|--backup] [--target <2.0.0>]') - sys.exit() - - -def parse_cmd(argv): - """ Parse the command line arguments """ - file_name = '' - verbose = False - create_backup = False - target_version = "2.0.0" - try: - opts, args = getopt.getopt(argv,"hvi:bt", - ["file=", "backup"]) - except getopt.GetoptError: - print('checkOpenPMD_h5.py -i ') - sys.exit(2) - for opt, arg in opts: - if opt == '-h': - help() - elif opt in ("-v", "--verbose"): - verbose = True - elif opt in ("-b", "--backup"): - create_backup = True - elif opt in ("-t", "--target"): - target_version = arg - elif opt in ("-i", "--file"): - file_name = arg - if not os.path.isfile(file_name): - print("File '%s' not found!" % file_name) - help() - return file_name, verbose, target_version, not create_backup +from .Updater import Updater +import argparse -def main(): - file_name, verbose, target_version, in_place = parse_cmd(sys.argv[1:]) - updater = Updater(file_name, verbose) - updater.update(target_version, in_place) +def parse_args(): + help_str: String = ( + "This is the openPMD updater HDF5 files.\n" + + "It allows to update openPMD flavored files from openPMD standard {0} to {1}\n".format( + 111, 222 + ) + ) + parser = argparse.ArgumentParser( + description=help_str, formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument("file", help="OpenPMD-series-filename", type=str) + + parser.add_argument("-b", "--backup", help="create a backup", action="store_true") + parser.add_argument( + "-v", "--verbose", help="increase output verbosity", action="store_false" + ) + parser.add_argument( + "-t", "--target", help="target version to update to", type=str, default="2.0.0" + ) + + args = parser.parse_args() + return args + +def main(): + inputs = parse_args() + updater = Updater(inputs.file, inputs.verbose) + updater.update(inputs.target, not inputs.backup) # return code: non-zero is Unix-style for errors occurred - #sys.exit(int(result_array[0])) + # sys.exit(int(result_array[0])) if __name__ == "__main__": diff --git a/openpmd_updater/transforms/ITransform.py b/openpmd_updater/transforms/ITransform.py index a055344..77ad950 100644 --- a/openpmd_updater/transforms/ITransform.py +++ b/openpmd_updater/transforms/ITransform.py @@ -5,13 +5,13 @@ Authors: Axel Huebl License: ISC """ + from abc import abstractmethod class ITransform(object): - """Transform an openPMD file from one standard version to another. - """ - + """Transform an openPMD file from one standard version to another.""" + @abstractmethod def __init__(self, backend): """Open a file""" @@ -25,14 +25,18 @@ def name(): @property def min_version(): """Minimum openPMD standard version that is supported by this transformation""" - raise NotImplementedError("Minimum supported openPMD standard version " - "of this transformation not implemented!") + raise NotImplementedError( + "Minimum supported openPMD standard version " + "of this transformation not implemented!" + ) @property def to_version(): """openPMD standard version is fulfulled by this transformation""" - raise NotImplementedError("Targeted openPMD standard version of " - "this transformation not implemented!") + raise NotImplementedError( + "Targeted openPMD standard version of " + "this transformation not implemented!" + ) @abstractmethod def transform(self, in_place=True): diff --git a/openpmd_updater/transforms/v2_0_0/DataOrder.py b/openpmd_updater/transforms/v2_0_0/DataOrder.py index 66e908b..f4983e1 100644 --- a/openpmd_updater/transforms/v2_0_0/DataOrder.py +++ b/openpmd_updater/transforms/v2_0_0/DataOrder.py @@ -6,7 +6,7 @@ License: ISC """ -from openpmd_updater.transforms.ITransform import ITransform +from ..ITransform import ITransform import numpy as np @@ -17,7 +17,7 @@ class DataOrder(ITransform): The order of attributes arises naturally from flattened out memory layout. Regarding previously stored `dataOrder='F'` attributes, the update needs to invert such attributes. - + Affects the *base standard* attributes: - `axisLabels` - `gridSpacing` @@ -37,8 +37,10 @@ class DataOrder(ITransform): """ """Name and description of the transformation""" - name = "dataOrder", \ - "remove the dataOrder attribute and transform Fortran attributes" + name = ( + "dataOrder", + "remove the dataOrder attribute and transform Fortran attributes", + ) """Minimum openPMD standard version that is supported by this transformation""" min_version = "1.0.0" @@ -58,7 +60,7 @@ def __init__(self, backend): "shape", # ED-PIC "fieldBoundary", - "particleBoundary" + "particleBoundary", ] def transform(self, in_place=True): @@ -69,19 +71,21 @@ def transform(self, in_place=True): self.fb.cd(None) basePath = "/data/" # fixed in openPMD v1 meshes_path = self.fb.get_attr("meshesPath").decode() - + iterations = self.fb.list_groups("/data/") - + for it in iterations: abs_meshes_path = "/data/" + str(it) + "/" + meshes_path # vector/tensor and scalar meshes - all_meshes = self.fb.list_groups(abs_meshes_path) + self.fb.list_data(abs_meshes_path) + all_meshes = self.fb.list_groups(abs_meshes_path) + self.fb.list_data( + abs_meshes_path + ) self.fb.cd(abs_meshes_path) for mesh in all_meshes: old_data_order = self.fb.get_attr("dataOrder", mesh) - if old_data_order == b'F': + if old_data_order == b"F": # mesh record attributes for attr_name in self.affected_attrs: if attr_name in self.fb.list_attrs(mesh): diff --git a/openpmd_updater/transforms/v2_0_0/ExtensionString.py b/openpmd_updater/transforms/v2_0_0/ExtensionString.py index 0807142..aaabec5 100644 --- a/openpmd_updater/transforms/v2_0_0/ExtensionString.py +++ b/openpmd_updater/transforms/v2_0_0/ExtensionString.py @@ -6,7 +6,7 @@ License: ISC """ -from openpmd_updater.transforms.ITransform import ITransform +from ..ITransform import ITransform import numpy as np @@ -43,15 +43,12 @@ def transform(self, in_place=True): self.fb.cd(None) extensionIDs = self.fb.get_attr("openPMDextension") self.fb.del_attr("openPMDextension") - + enabled_extensions = [] enabledExtMask = 0 for extension, bitmask in ext_list.items(): # This uses a bitmask to identify activated extensions if (bitmask & extensionIDs) == bitmask: enabled_extensions.append(extension) - - self.fb.add_attr( - "openPMDextension", - np.string_(";".join(enabled_extensions)) - ) + + self.fb.add_attr("openPMDextension", np.string_(";".join(enabled_extensions))) diff --git a/openpmd_updater/transforms/v2_0_0/GridUnit.py b/openpmd_updater/transforms/v2_0_0/GridUnit.py index 10a3d76..4386222 100644 --- a/openpmd_updater/transforms/v2_0_0/GridUnit.py +++ b/openpmd_updater/transforms/v2_0_0/GridUnit.py @@ -6,7 +6,7 @@ License: ISC """ -from openpmd_updater.transforms.ITransform import ITransform +from ..ITransform import ITransform import numpy as np @@ -25,8 +25,7 @@ class GridUnit(ITransform): """ """Name and description of the transformation""" - name = "gridUnit", \ - "allow non-spatial gridSpacing in meshes" + name = "gridUnit", "allow non-spatial gridSpacing in meshes" """Minimum openPMD standard version that is supported by this transformation""" min_version = "1.0.0" @@ -52,7 +51,9 @@ def transform(self, in_place=True): for it in iterations: abs_meshes_path = "/data/" + str(it) + "/" + meshes_path # vector/tensor and scalar meshes - all_meshes = self.fb.list_groups(abs_meshes_path) + self.fb.list_data(abs_meshes_path) + all_meshes = self.fb.list_groups(abs_meshes_path) + self.fb.list_data( + abs_meshes_path + ) self.fb.cd(abs_meshes_path) @@ -61,13 +62,14 @@ def transform(self, in_place=True): grid_ndim = len(self.fb.get_attr("gridSpacing", mesh)) - new_grid_unit_SI = np.ones((grid_ndim, ), dtype=np.float64) * \ - old_grid_unit_SI + new_grid_unit_SI = ( + np.ones((grid_ndim,), dtype=np.float64) * old_grid_unit_SI + ) self.fb.add_attr("gridUnitSI", new_grid_unit_SI, mesh) # openPMD 1.* dimensions were spatial (L) - grid_unit_dimension = np.zeros((grid_ndim * 7, ), dtype=np.float64) + grid_unit_dimension = np.zeros((grid_ndim * 7,), dtype=np.float64) grid_unit_dimension[::7] = 1.0 self.fb.add_attr("gridUnitDimension", grid_unit_dimension, mesh) diff --git a/openpmd_updater/transforms/v2_0_0/ParticleBoundary.py b/openpmd_updater/transforms/v2_0_0/ParticleBoundary.py index de0ce6a..fce3a83 100644 --- a/openpmd_updater/transforms/v2_0_0/ParticleBoundary.py +++ b/openpmd_updater/transforms/v2_0_0/ParticleBoundary.py @@ -6,7 +6,7 @@ License: ISC """ -from openpmd_updater.transforms.ITransform import ITransform +from ..ITransform import ITransform import numpy as np @@ -22,8 +22,10 @@ class ParticleBoundary(ITransform): """ """Name and description of the transformation""" - name = "particleBoundary", \ - "move the particleBoundary attribute from the mesh records to the species records" + name = ( + "particleBoundary", + "move the particleBoundary attribute from the mesh records to the species records", + ) """Minimum openPMD standard version that is supported by this transformation""" min_version = "1.0.0" diff --git a/openpmd_updater/transforms/v2_0_0/Version.py b/openpmd_updater/transforms/v2_0_0/Version.py index 1c4530e..3835e05 100644 --- a/openpmd_updater/transforms/v2_0_0/Version.py +++ b/openpmd_updater/transforms/v2_0_0/Version.py @@ -6,7 +6,7 @@ License: ISC """ -from openpmd_updater.transforms.ITransform import ITransform +from ..ITransform import ITransform import numpy as np diff --git a/openpmd_updater/utils/__init__.py b/openpmd_updater/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ed1f412 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +[build-system] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +[project] +name = "openpmd_updater" +version = "1.0.0" +authors = [] +readme = "README.md" +classifiers = [ + # "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: ISC License (ISCL)", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Topic :: Scientific/Engineering :: Physics" +] +dynamic = ["description"] +dependencies = [ + "numpy >=1.23", + "python-dateutil >=2.3.0", + "h5py>=2.0.0", + "packaging" +] + +[project.scripts] +openPMD_updater_h5 = "openpmd_updater.cli:main" + +[project.optional-dependencies] +test = [ + "nose" +] + +[project.urls] +Home = "/~https://github.com/openPMD/openPMD-updater" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index fe7f040..0000000 --- a/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -packaging -numpy>=1.6.1 -python-dateutil>=2.3.0 -h5py>=2.0.0 -# adios>=1.11.0 - -# only for testing -nose diff --git a/setup.py b/setup.py deleted file mode 100644 index 880a5c8..0000000 --- a/setup.py +++ /dev/null @@ -1,42 +0,0 @@ -from setuptools import setup - - -def read_requirements(): - with open('requirements.txt') as f: - return [line.strip() for line in f.readlines()] - - -def read_readme(): - with open('README.md') as f: - return f.read() - - -setup( - name='openPMD-updater', - version='1.0.0', - url='/~https://github.com/openPMD/openPMD-updater', - # author=..., # TODO - # author_email=..., # TODO - # maintainer=..., # TODO - # maintainer_email=..., # TODO - license='ISC', - install_requires=read_requirements(), - description='updater ', - long_description=read_readme(), - classifiers=[ - # 'Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: ISC License (ISCL)', - 'Operating System :: OS Independent', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Topic :: Scientific/Engineering :: Physics', - ], - packages=['openpmd_updater'], - entry_points={ - 'console_scripts': [ - 'openPMD_updater_h5 = openpmd_updater.cli:main' - ] - }, -) From 8dcbca0d2fe0a56e6bff99689afb923ca47639e8 Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Wed, 20 Mar 2024 13:08:34 -0500 Subject: [PATCH 02/22] Follow NEP29 --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index ed1f412..37de490 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "h5py>=2.0.0", "packaging" ] +requires-python = ">=3.10" [project.scripts] openPMD_updater_h5 = "openpmd_updater.cli:main" From b45c99fa2e3d6f1d694d7899f86b2b52471c52ea Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Wed, 20 Mar 2024 13:09:18 -0500 Subject: [PATCH 03/22] Modern h5py to prevent too old versions --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 37de490..45b53fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dynamic = ["description"] dependencies = [ "numpy >=1.23", "python-dateutil >=2.3.0", - "h5py>=2.0.0", + "h5py>=3.8", "packaging" ] requires-python = ">=3.10" From 8eb6862d467484c2acaaf2db5488bd5d50c6c3a5 Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Wed, 20 Mar 2024 13:12:51 -0500 Subject: [PATCH 04/22] Use gitignore/Python.gitignore and add .ruff_cache modified: .gitignore --- .gitignore | 169 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 160 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 49fe776..cfc7a18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,163 @@ -# setuptools +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python build/ +develop-eggs/ dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ *.egg-info/ -__pycache__ -*.pyc - -# conda -conda_recipe/linux-64/ -conda_recipe/linux-ppc64le/ -conda_recipe/osx-64/ -conda_recipe/win-64/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# ruff Python linter +.ruff_cache/ + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at /~https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ From d5bfa7bb2e235a2860f462cd2a23711c1cd47070 Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Wed, 20 Mar 2024 13:15:21 -0500 Subject: [PATCH 05/22] gitignore conda stuff --- .gitignore | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitignore b/.gitignore index cfc7a18..e65ae6b 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,12 @@ share/python-wheels/ *.egg MANIFEST +# conda +conda_recipe/linux-64/ +conda_recipe/linux-ppc64le/ +conda_recipe/osx-64/ +conda_recipe/win-64/ + # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. From af5307394dc6d21f55457be7e683fb43f6c1281e Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Wed, 20 Mar 2024 13:29:27 -0500 Subject: [PATCH 06/22] Add --pdf flag to aid in debugging --- openpmd_updater/cli.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/openpmd_updater/cli.py b/openpmd_updater/cli.py index 51a55ca..73dd672 100644 --- a/openpmd_updater/cli.py +++ b/openpmd_updater/cli.py @@ -7,11 +7,14 @@ """ from .Updater import Updater +import pdb +import traceback +import sys import argparse def parse_args(): - help_str: String = ( + help_str = ( "This is the openPMD updater HDF5 files.\n" + "It allows to update openPMD flavored files from openPMD standard {0} to {1}\n".format( 111, 222 @@ -29,6 +32,11 @@ def parse_args(): parser.add_argument( "-t", "--target", help="target version to update to", type=str, default="2.0.0" ) + parser.add_argument( + "--pdb", + help="wrap call in try/except with pdb post_mortem debugger", + action="store_true", + ) args = parser.parse_args() return args @@ -36,10 +44,21 @@ def parse_args(): def main(): inputs = parse_args() - updater = Updater(inputs.file, inputs.verbose) - updater.update(inputs.target, not inputs.backup) - # return code: non-zero is Unix-style for errors occurred - # sys.exit(int(result_array[0])) + print(inputs) + + if not inputs.pdb: + updater = Updater(inputs.file, inputs.verbose) + updater.update(inputs.target, not inputs.backup) + # return code: non-zero is Unix-style for errors occurred + # sys.exit(int(result_array[0])) + else: + try: + updater = Updater(inputs.file, inputs.verbose) + updater.update(inputs.target, not inputs.backup) + except: + extype, value, tb = sys.exc_info() + traceback.print_exc() + pdb.post_mortem(tb) if __name__ == "__main__": From 8042dafb093187bb888ba70b5583448f9051c454 Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Thu, 28 Mar 2024 12:42:23 -0500 Subject: [PATCH 07/22] minor --- openpmd_updater/cli.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/openpmd_updater/cli.py b/openpmd_updater/cli.py index 73dd672..b26a1d0 100644 --- a/openpmd_updater/cli.py +++ b/openpmd_updater/cli.py @@ -43,18 +43,17 @@ def parse_args(): def main(): - inputs = parse_args() - print(inputs) - - if not inputs.pdb: - updater = Updater(inputs.file, inputs.verbose) - updater.update(inputs.target, not inputs.backup) + args = parse_args() + + if not args.pdb: + updater = Updater(args.file, args.verbose) + updater.update(args.target, not args.backup) # return code: non-zero is Unix-style for errors occurred # sys.exit(int(result_array[0])) else: try: - updater = Updater(inputs.file, inputs.verbose) - updater.update(inputs.target, not inputs.backup) + updater = Updater(args.file, args.verbose) + updater.update(args.target, not args.backup) except: extype, value, tb = sys.exc_info() traceback.print_exc() From c76dc4ba9fdd030a84ba91fd66b98dda9f443132 Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Thu, 28 Mar 2024 15:25:09 -0500 Subject: [PATCH 08/22] h5py is a required dependency --- openpmd_updater/backends/HDF5.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/openpmd_updater/backends/HDF5.py b/openpmd_updater/backends/HDF5.py index 42deb71..72268a6 100644 --- a/openpmd_updater/backends/HDF5.py +++ b/openpmd_updater/backends/HDF5.py @@ -8,11 +8,7 @@ from .IBackend import IBackend import packaging.version - -try: - import h5py as h5 -except: - h5 = None +import h5py as h5 class HDF5(IBackend): @@ -20,8 +16,6 @@ class HDF5(IBackend): def __init__(self, filename): """Open a HDF5 file""" - if h5 is None: - raise RuntimeError("h5py is not installed!") if self.can_handle(filename): self.fh = h5.File(filename, "r+") From 76c854e938323be2a3ed58caedee500728b00a68 Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Thu, 28 Mar 2024 15:37:24 -0500 Subject: [PATCH 09/22] Use files from openPMD/openPMD-example-datasets repo directly --- example_files/1_1_0/structure.h5 | Bin 94920 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 example_files/1_1_0/structure.h5 diff --git a/example_files/1_1_0/structure.h5 b/example_files/1_1_0/structure.h5 deleted file mode 100644 index b2edd1bc37a801991a9c625bcbd9d96dbd2d1746..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 94920 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 From 639eff9dc3315edc402f7b859bbbf1726cd1edad Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Thu, 28 Mar 2024 16:28:03 -0500 Subject: [PATCH 10/22] Transforms are optional, skip if no related components! --- .../transforms/v2_0_0/DataOrder.py | 10 +++++++-- openpmd_updater/transforms/v2_0_0/GridUnit.py | 12 +++++++--- .../transforms/v2_0_0/ParticleBoundary.py | 22 +++++++++++++++---- 3 files changed, 35 insertions(+), 9 deletions(-) diff --git a/openpmd_updater/transforms/v2_0_0/DataOrder.py b/openpmd_updater/transforms/v2_0_0/DataOrder.py index f4983e1..0980d87 100644 --- a/openpmd_updater/transforms/v2_0_0/DataOrder.py +++ b/openpmd_updater/transforms/v2_0_0/DataOrder.py @@ -69,8 +69,14 @@ def transform(self, in_place=True): raise NotImplementedError("Only in-place transformation implemented!") self.fb.cd(None) - basePath = "/data/" # fixed in openPMD v1 - meshes_path = self.fb.get_attr("meshesPath").decode() + try: + basePath = "/data/" # fixed in openPMD v1 + meshes_path = self.fb.get_attr("meshesPath").decode() + except KeyError: + print( + "[DataOrder transform] Input file has no 'meshesPath' attr, skipping transform! " + ) + return iterations = self.fb.list_groups("/data/") diff --git a/openpmd_updater/transforms/v2_0_0/GridUnit.py b/openpmd_updater/transforms/v2_0_0/GridUnit.py index 4386222..b080412 100644 --- a/openpmd_updater/transforms/v2_0_0/GridUnit.py +++ b/openpmd_updater/transforms/v2_0_0/GridUnit.py @@ -42,9 +42,15 @@ def transform(self, in_place=True): if not in_place: raise NotImplementedError("Only in-place transformation implemented!") - self.fb.cd(None) - basePath = "/data/" # fixed in openPMD v1 - meshes_path = self.fb.get_attr("meshesPath").decode() + try: + self.fb.cd(None) + basePath = "/data/" # fixed in openPMD v1 + meshes_path = self.fb.get_attr("meshesPath").decode() + except KeyError: + print( + "[Grid Unit transform] Input file has no 'meshesPath' attr, skipping transform! " + ) + return iterations = self.fb.list_groups("/data/") diff --git a/openpmd_updater/transforms/v2_0_0/ParticleBoundary.py b/openpmd_updater/transforms/v2_0_0/ParticleBoundary.py index fce3a83..84e4bd6 100644 --- a/openpmd_updater/transforms/v2_0_0/ParticleBoundary.py +++ b/openpmd_updater/transforms/v2_0_0/ParticleBoundary.py @@ -42,10 +42,24 @@ def transform(self, in_place=True): if not in_place: raise NotImplementedError("Only in-place transformation implemented!") - self.fb.cd(None) - basePath = "/data/" # fixed in openPMD v1 - meshes_path = self.fb.get_attr("meshesPath").decode() - particles_path = self.fb.get_attr("particlesPath").decode() + try: + self.fb.cd(None) + basePath = "/data/" # fixed in openPMD v1 + meshes_path = self.fb.get_attr("meshesPath").decode() + except KeyError: + print( + "[ParticleBoundary transform] Input file has no 'meshesPath' attr, skipping transform! " + ) + return + + try: + particles_path = self.fb.get_attr("particlesPath").decode() + except KeyError: + print( + "[ParticleBoundary transform] Input file has no 'particlesPath' attr, skipping transform! " + ) + return + iterations = self.fb.list_groups("/data/") for it in iterations: From 5170a046a8646e00aa2690c1d3f66a790bbf47cf Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Thu, 28 Mar 2024 16:47:54 -0500 Subject: [PATCH 11/22] minor --- openpmd_updater/Updater.py | 6 +++++- openpmd_updater/cli.py | 2 +- tests/test_backend_h5.py | 15 +++++---------- tests/test_updater.py | 5 +++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/openpmd_updater/Updater.py b/openpmd_updater/Updater.py index a5e3c65..93b89d0 100644 --- a/openpmd_updater/Updater.py +++ b/openpmd_updater/Updater.py @@ -50,7 +50,7 @@ def __init__(self, filename, verbose=False): for b in self.backends: if self.verbose: - print("[Updater] Trying file format {0}".format(type(b))) + print("[Updater] Trying file format {0}".format(b.__name__)) if b.can_handle(filename): self.fb = b(filename) break @@ -73,6 +73,10 @@ def update(self, new_version="2.0.0", in_place=True): ) ) + if file_version == packaging.version.parse(new_version): + print("[Updater] File is at already at the requested version!") + return + # select proper update depending on initial version # note: multiple updates over intermediate openPMD standard releases are possible while file_version != packaging.version.parse(new_version): diff --git a/openpmd_updater/cli.py b/openpmd_updater/cli.py index b26a1d0..a1da5e5 100644 --- a/openpmd_updater/cli.py +++ b/openpmd_updater/cli.py @@ -44,7 +44,7 @@ def parse_args(): def main(): args = parse_args() - + if not args.pdb: updater = Updater(args.file, args.verbose) updater.update(args.target, not args.backup) diff --git a/tests/test_backend_h5.py b/tests/test_backend_h5.py index d8bf302..25526c0 100644 --- a/tests/test_backend_h5.py +++ b/tests/test_backend_h5.py @@ -5,6 +5,7 @@ Authors: Axel Huebl License: ISC """ + import unittest import packaging.version from openpmd_updater.backends.HDF5 import HDF5 @@ -14,14 +15,12 @@ class Test_BackendHDF5(unittest.TestCase): """ ... """ + def test_can_handle(self): """ ... """ - self.assertEqual( - HDF5.can_handle("example_files/1_1_0/structure.h5"), - True - ) + self.assertEqual(HDF5.can_handle("example_files/1_1_0/structure.h5"), True) def test_read(self): """ @@ -31,12 +30,8 @@ def test_read(self): fb = HDF5(filename) - self.assertEqual( - fb.version == packaging.version.parse("1.1.0"), - True - ) + self.assertEqual(fb.version == packaging.version.parse("1.1.0"), True) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() - diff --git a/tests/test_updater.py b/tests/test_updater.py index b0902c9..e094452 100644 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -5,6 +5,7 @@ Authors: Axel Huebl License: ISC """ + import unittest from openpmd_updater.Updater import Updater @@ -24,6 +25,6 @@ def test_update(self): up.update() -if __name__ == '__main__': - unittest.main() +if __name__ == "__main__": + unittest.main() From c5c1a33f7372c4daf884468cb7a1d0dad1403d8a Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Thu, 28 Mar 2024 16:52:56 -0500 Subject: [PATCH 12/22] pyproject.toml: Add authors --- pyproject.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 45b53fd..b83c928 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,11 @@ build-backend = "flit_core.buildapi" [project] name = "openpmd_updater" version = "1.0.0" -authors = [] +authors = [ + {name = "Axel Huebl", email = "axelbuebl@lbl.gov"}, + {name = "Sajid Ali", email = "sasyed@fnal.gov"}, + {name = "Remi Lehe"}, +] readme = "README.md" classifiers = [ # "Development Status :: 4 - Beta", From da436bf47468080f48b004a1c12f2d2e6a24d8b4 Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Wed, 17 Apr 2024 13:57:04 -0500 Subject: [PATCH 13/22] tests to pytest, need pytest-order to ensure that read checks happen before files are transformed! --- pyproject.toml | 2 +- tests/conftest.py | 28 ++++++++++++++++++++++++++++ tests/test_backend_h5.py | 40 ++++++++++++++-------------------------- tests/test_updater.py | 26 +++++--------------------- 4 files changed, 48 insertions(+), 48 deletions(-) create mode 100644 tests/conftest.py diff --git a/pyproject.toml b/pyproject.toml index b83c928..571c3e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ openPMD_updater_h5 = "openpmd_updater.cli:main" [project.optional-dependencies] test = [ - "nose" + "pytest", "pytest-order" ] [project.urls] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..a1f35fd --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,28 @@ +""" +This file is part of the openPMD-updater. + +Copyright 2024 openPMD contributors +Authors: Sajid Ali +License: ISC +""" + +import pytest +import requests + + +@pytest.fixture() +def get_file(tmp_path_factory): + test_file = "structure.h5" + test_file_url = ( + "/~https://github.com/openPMD/openPMD-example-datasets/raw/draft/structure.h5" + ) + response = requests.get(test_file_url) + + test_file_path = tmp_path_factory.mktemp("data") / test_file + + with open(test_file_path, "wb") as out_file: + out_file.write(response.content) + + print(test_file_path) + + return test_file_path diff --git a/tests/test_backend_h5.py b/tests/test_backend_h5.py index 25526c0..38ff4c2 100644 --- a/tests/test_backend_h5.py +++ b/tests/test_backend_h5.py @@ -1,37 +1,25 @@ """ This file is part of the openPMD-updater. -Copyright 2018 openPMD contributors -Authors: Axel Huebl +Copyright 2024 openPMD contributors +Authors: Axel Huebl, Sajid Ali License: ISC """ -import unittest +import pytest +import os import packaging.version from openpmd_updater.backends.HDF5 import HDF5 +@pytest.mark.order("first") +def test_can_handle(get_file): + filepath = get_file + print(filepath) + assert os.path.exists(filepath) == True + assert HDF5.can_handle(filepath) -class Test_BackendHDF5(unittest.TestCase): - """ - ... - """ +@pytest.mark.order("second") +def test_read(get_file): + fb = HDF5(get_file) - def test_can_handle(self): - """ - ... - """ - self.assertEqual(HDF5.can_handle("example_files/1_1_0/structure.h5"), True) - - def test_read(self): - """ - ... - """ - filename = "example_files/1_1_0/structure.h5" - - fb = HDF5(filename) - - self.assertEqual(fb.version == packaging.version.parse("1.1.0"), True) - - -if __name__ == "__main__": - unittest.main() + assert fb.version == packaging.version.parse("1.1.0") diff --git a/tests/test_updater.py b/tests/test_updater.py index e094452..d181dad 100644 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -1,30 +1,14 @@ """ This file is part of the openPMD-updater. -Copyright 2018 openPMD contributors -Authors: Axel Huebl +Copyright 2024 openPMD contributors +Authors: Axel Huebl, Sajid Ali License: ISC """ -import unittest from openpmd_updater.Updater import Updater -class Test_Updater(unittest.TestCase): - """ - ... - """ - - def test_update(self): - """ - ... - """ - filename = "example_files/1_1_0/structure.h5" - - up = Updater(filename, verbose=True) - - up.update() - - -if __name__ == "__main__": - unittest.main() +def test_update(get_file): + up = Updater(get_file, verbose=True) + up.update() From 9eeb66b0a0b04ff597fcde9674eb8ffde350be64 Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Wed, 17 Apr 2024 14:02:43 -0500 Subject: [PATCH 14/22] CI with Github Actions --- .github/dependabot.yml | 11 +++++++++++ .github/workflows/ci.yml | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..bf7c985 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# Set update schedule for GitHub Actions + +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..82662c8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: Python package + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: python -m pip install .[test] + - name: Test with pytest + run: pytest + From 9f31cd3e1564f20162f12564d154bd11134c32a2 Mon Sep 17 00:00:00 2001 From: Axel Huebl Date: Wed, 17 Apr 2024 12:18:08 -0700 Subject: [PATCH 15/22] Fix CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82662c8..fbc05e2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 From af00d80ae303128ea90e824ca1da109352691ba4 Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Wed, 17 Apr 2024 14:19:54 -0500 Subject: [PATCH 16/22] CI: add requests requirement for tests --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 571c3e5..6eea697 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ openPMD_updater_h5 = "openpmd_updater.cli:main" [project.optional-dependencies] test = [ - "pytest", "pytest-order" + "requests", "pytest", "pytest-order" ] [project.urls] From c15f9821d864c7d82615fa784395099a752efe0e Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Wed, 17 Apr 2024 14:21:11 -0500 Subject: [PATCH 17/22] Update openpmd_updater/cli.py Co-authored-by: Axel Huebl --- openpmd_updater/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpmd_updater/cli.py b/openpmd_updater/cli.py index a1da5e5..d8064aa 100644 --- a/openpmd_updater/cli.py +++ b/openpmd_updater/cli.py @@ -23,7 +23,7 @@ def parse_args(): parser = argparse.ArgumentParser( description=help_str, formatter_class=argparse.ArgumentDefaultsHelpFormatter ) - parser.add_argument("file", help="OpenPMD-series-filename", type=str) + parser.add_argument("file", help="openPMD-series-filename", type=str) parser.add_argument("-b", "--backup", help="create a backup", action="store_true") parser.add_argument( From 7c527f940555a92a6f197971b44cad2e87ff67b7 Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Wed, 17 Apr 2024 14:24:10 -0500 Subject: [PATCH 18/22] versioning make explicit instead of vague --- openpmd_updater/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpmd_updater/cli.py b/openpmd_updater/cli.py index d8064aa..05977a8 100644 --- a/openpmd_updater/cli.py +++ b/openpmd_updater/cli.py @@ -17,7 +17,7 @@ def parse_args(): help_str = ( "This is the openPMD updater HDF5 files.\n" + "It allows to update openPMD flavored files from openPMD standard {0} to {1}\n".format( - 111, 222 + "1.1.0", "2.0.0" ) ) parser = argparse.ArgumentParser( From 511b02e169805d73476f50548d785c7b15e5c6b1 Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Wed, 17 Apr 2024 14:26:12 -0500 Subject: [PATCH 19/22] CI: bump tool setup python modified: .github/workflows/ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fbc05e2..b42c500 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies From 4dc50de81c2b1a118b47770e851c1f3c2def7017 Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Wed, 17 Apr 2024 14:40:49 -0500 Subject: [PATCH 20/22] change badge to use GithubActions output instead of travisCI --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7cc4540..a314433 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # openPMD Updater -[![Build Status `master`](https://img.shields.io/travis/openPMD/openPMD-updater/master.svg?label=master)](https://travis-ci.com/openPMD/openPMD-updater/branches) +[![CI:ubuntu](/~https://github.com/openPMD/openPMD-updater/actions/workflows/ci.yml/badge.svg)](/~https://github.com/openPMD/openPMD-updater/actions/workflows/ci.yml) ![Supported Python Versions](https://img.shields.io/pypi/pyversions/openPMD-updater.svg) [![License](https://img.shields.io/badge/license-ISC-blue.svg)](https://opensource.org/licenses/ISC) From 1430ae2909a0e0889821fd5c66d162cebc3dce96 Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Wed, 17 Apr 2024 14:48:02 -0500 Subject: [PATCH 21/22] update README --- README.md | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index a314433..91fe0a4 100644 --- a/README.md +++ b/README.md @@ -43,10 +43,10 @@ git clone /~https://github.com/openPMD/openPMD-updater.git cd openPMD-updater # install dependencies -pip install -r requirements.txt +pip install . -# optional: append --user -python setup.py install +# with optional dependencies for testing +pip install .[test] ``` ## Usage @@ -77,26 +77,19 @@ updater.update(target_version="2.0.0", in_place=True) ### Testing +Additional packages need to be installed for the tests. To install them do: ```bash -export PYTHONPATH=$(pwd):$PYTHONPATH +pip install .[test] +``` +Then one can run the tests as follows: +```bash # all -nosetests +pytest # specific -nosetests tests/test_backend_h5.py +pytest tests/test_backend_h5.py # manual python tests/test_backend_h5.py ``` - -*note*: this changes your `example_files/1_1_0/structure.h5` files in-place. -`git checkout` it for a reset or do a comparison: - -```bash -h5diff -c example_files/1_1_0/structure.h5.bak example_files/1_1_0/structure.h5 - attribute: > and > - 35 differences found - Not comparable: is of class H5T_INTEGER and is of class H5T_STRING - # ... -``` From 821c96626339a053a536bd4d3d44a5f3837c5fd4 Mon Sep 17 00:00:00 2001 From: Sajid Ali Date: Wed, 17 Apr 2024 14:49:47 -0500 Subject: [PATCH 22/22] update README --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 91fe0a4..309f2a2 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,4 @@ pytest # specific pytest tests/test_backend_h5.py - -# manual -python tests/test_backend_h5.py ```