diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8e11052..ea3c82c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,7 @@ on: jobs: build: strategy: + fail-fast: false matrix: python-version: ["3.9", "3.10", "3.11"] os: [ubuntu-latest, windows-latest, macos-latest] diff --git a/bpx/__init__.py b/bpx/__init__.py index 6c62ecf..c7e2ca2 100644 --- a/bpx/__init__.py +++ b/bpx/__init__.py @@ -5,7 +5,7 @@ from .schema import BPX, check_sto_limits from .utilities import get_electrode_concentrations, get_electrode_stoichiometries -__version__ = "0.4.2" +__version__ = "0.5.0" __all__ = [ "BPX", diff --git a/bpx/function.py b/bpx/function.py index 51e2d71..fff4155 100644 --- a/bpx/function.py +++ b/bpx/function.py @@ -3,7 +3,6 @@ import copy import tempfile from importlib import util -from pathlib import Path from typing import TYPE_CHECKING, Any from pydantic_core import CoreSchema, core_schema @@ -87,26 +86,10 @@ def to_python_function(self, preamble: str | None = None) -> Callable: source_code = preamble + function_def + function_body with tempfile.NamedTemporaryFile(suffix=f"{function_name}.py", delete=False) as tmp: - # write to a tempory file so we can - # get the source later on using inspect.getsource - # (as long as the file still exists) - tmp.write((source_code).encode()) + tmp.write(source_code.encode()) tmp.flush() - - # Now load that file as a module spec = util.spec_from_file_location("tmp", tmp.name) module = util.module_from_spec(spec) spec.loader.exec_module(module) - # Delete - tmp.close() - Path(tmp.name).unlink(missing_ok=True) - if module.__cached__: - cached_file = Path(module.__cached__) - cached_path = cached_file.parent - cached_file.unlink(missing_ok=True) - if not any(cached_path.iterdir()): - cached_path.rmdir() - - # return the new function object return getattr(module, function_name) diff --git a/bpx/parsers.py b/bpx/parsers.py index e6405b4..a361646 100644 --- a/bpx/parsers.py +++ b/bpx/parsers.py @@ -1,3 +1,8 @@ +from __future__ import annotations + +import json +from pathlib import Path + from .schema import BPX @@ -26,13 +31,13 @@ def parse_bpx_obj(bpx: dict, v_tol: float = 0.001) -> BPX: return BPX.model_validate(bpx) -def parse_bpx_file(filename: str, v_tol: float = 0.001) -> BPX: +def parse_bpx_file(filename: str | Path, v_tol: float = 0.001) -> BPX: """ A convenience function to parse a bpx file into a BPX model. Parameters ---------- - filename: str + filename: str or Path a filepath to a bpx file v_tol: float absolute tolerance in [V] to validate the voltage limits, 1 mV by default @@ -42,18 +47,12 @@ def parse_bpx_file(filename: str, v_tol: float = 0.001) -> BPX: BPX: :class:`bpx.BPX` a parsed BPX model """ - - from pathlib import Path - - bpx = "" - if filename.endswith((".yml", ".yaml")): + if str(filename).endswith((".yml", ".yaml")): import yaml with Path(filename).open(encoding="utf-8") as f: bpx = yaml.safe_load(f) else: - import orjson as json - with Path(filename).open(encoding="utf-8") as f: bpx = json.loads(f.read()) @@ -77,7 +76,5 @@ def parse_bpx_str(bpx: str, v_tol: float = 0.001) -> BPX: BPX: a parsed BPX model """ - import orjson as json - bpx = json.loads(bpx) return parse_bpx_obj(bpx, v_tol) diff --git a/pyproject.toml b/pyproject.toml index 16e300a..57fd24f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,6 @@ dependencies = [ "pydantic >= 2.6", "pyparsing", "pyyaml", - "orjson", ] [project.urls] diff --git a/tests/test_schema.py b/tests/test_schema.py index 99be0a6..133e6c4 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -1,6 +1,7 @@ import copy import unittest import warnings +from typing import Any import pytest from pydantic import TypeAdapter, ValidationError @@ -12,7 +13,7 @@ class TestSchema(unittest.TestCase): def setUp(self) -> None: - self.base = { + self.base : dict[str, Any] = { "Header": { "BPX": 1.0, "Model": "DFN", @@ -200,26 +201,26 @@ def setUp(self) -> None: } def test_simple(self) -> None: - test = copy.copy(self.base) + test = copy.deepcopy(self.base) adapter.validate_python(test) def test_simple_spme(self) -> None: - test = copy.copy(self.base) + test = copy.deepcopy(self.base) test["Header"]["Model"] = "SPMe" adapter.validate_python(test) def test_simple_spm(self) -> None: - test = copy.copy(self.base_spm) + test = copy.deepcopy(self.base_spm) adapter.validate_python(test) def test_bad_model(self) -> None: - test = copy.copy(self.base) + test = copy.deepcopy(self.base) test["Header"]["Model"] = "Wrong model type" with pytest.raises(ValidationError): adapter.validate_python(test) def test_bad_dfn(self) -> None: - test = copy.copy(self.base_spm) + test = copy.deepcopy(self.base_spm) test["Header"]["Model"] = "DFN" with pytest.warns( UserWarning, @@ -228,7 +229,7 @@ def test_bad_dfn(self) -> None: adapter.validate_python(test) def test_bad_spme(self) -> None: - test = copy.copy(self.base_spm) + test = copy.deepcopy(self.base_spm) test["Header"]["Model"] = "SPMe" with pytest.warns( UserWarning, @@ -237,7 +238,7 @@ def test_bad_spme(self) -> None: adapter.validate_python(test) def test_bad_spm(self) -> None: - test = copy.copy(self.base) + test = copy.deepcopy(self.base) test["Header"]["Model"] = "SPM" with pytest.warns( UserWarning, @@ -246,7 +247,7 @@ def test_bad_spm(self) -> None: adapter.validate_python(test) def test_table(self) -> None: - test = copy.copy(self.base) + test = copy.deepcopy(self.base) test["Parameterisation"]["Electrolyte"]["Conductivity [S.m-1]"] = { "x": [1.0, 2.0], "y": [2.3, 4.5], @@ -254,7 +255,7 @@ def test_table(self) -> None: adapter.validate_python(test) def test_bad_table(self) -> None: - test = copy.copy(self.base) + test = copy.deepcopy(self.base) test["Parameterisation"]["Electrolyte"]["Conductivity [S.m-1]"] = { "x": [1.0, 2.0], "y": [2.3], @@ -266,23 +267,23 @@ def test_bad_table(self) -> None: adapter.validate_python(test) def test_function(self) -> None: - test = copy.copy(self.base) + test = copy.deepcopy(self.base) test["Parameterisation"]["Electrolyte"]["Conductivity [S.m-1]"] = "1.0 * x + 3" adapter.validate_python(test) def test_function_with_exp(self) -> None: - test = copy.copy(self.base) + test = copy.deepcopy(self.base) test["Parameterisation"]["Electrolyte"]["Conductivity [S.m-1]"] = "1.0 * exp(x) + 3" adapter.validate_python(test) def test_bad_function(self) -> None: - test = copy.copy(self.base) + test = copy.deepcopy(self.base) test["Parameterisation"]["Electrolyte"]["Conductivity [S.m-1]"] = "this is not a function" with pytest.raises(ValidationError): adapter.validate_python(test) def test_to_python_function(self) -> None: - test = copy.copy(self.base) + test = copy.deepcopy(self.base) test["Parameterisation"]["Electrolyte"]["Conductivity [S.m-1]"] = "2.0 * x" obj = adapter.validate_python(test) funct = obj.parameterisation.electrolyte.conductivity @@ -290,13 +291,13 @@ def test_to_python_function(self) -> None: assert pyfunct(2.0) == 4.0 def test_bad_input(self) -> None: - test = copy.copy(self.base) + test = copy.deepcopy(self.base) test["Parameterisation"]["Electrolyte"]["bad"] = "this shouldn't be here" with pytest.raises(ValidationError): adapter.validate_python(test) def test_validation_data(self) -> None: - test = copy.copy(self.base) + test = copy.deepcopy(self.base) test["Validation"] = { "Experiment 1": { "Time [s]": [0, 1000, 2000], @@ -314,39 +315,39 @@ def test_validation_data(self) -> None: def test_check_sto_limits_validator(self) -> None: warnings.filterwarnings("error") # Treat warnings as errors - test = copy.copy(self.base_non_blended) + test = copy.deepcopy(self.base_non_blended) test["Parameterisation"]["Cell"]["Upper voltage cut-off [V]"] = 4.3 test["Parameterisation"]["Cell"]["Lower voltage cut-off [V]"] = 2.5 adapter.validate_python(test) def test_check_sto_limits_validator_high_voltage(self) -> None: - test = copy.copy(self.base_non_blended) + test = copy.deepcopy(self.base_non_blended) test["Parameterisation"]["Cell"]["Upper voltage cut-off [V]"] = 4.0 with pytest.warns(UserWarning): adapter.validate_python(test) def test_check_sto_limits_validator_high_voltage_tolerance(self) -> None: warnings.filterwarnings("error") # Treat warnings as errors - test = copy.copy(self.base_non_blended) + test = copy.deepcopy(self.base_non_blended) test["Parameterisation"]["Cell"]["Upper voltage cut-off [V]"] = 4.0 BPX.Settings.tolerances["Voltage [V]"] = 0.25 adapter.validate_python(test) def test_check_sto_limits_validator_low_voltage(self) -> None: - test = copy.copy(self.base_non_blended) + test = copy.deepcopy(self.base_non_blended) test["Parameterisation"]["Cell"]["Lower voltage cut-off [V]"] = 3.0 with pytest.warns(UserWarning): adapter.validate_python(test) def test_check_sto_limits_validator_low_voltage_tolerance(self) -> None: warnings.filterwarnings("error") # Treat warnings as errors - test = copy.copy(self.base_non_blended) + test = copy.deepcopy(self.base_non_blended) test["Parameterisation"]["Cell"]["Lower voltage cut-off [V]"] = 3.0 BPX.Settings.tolerances["Voltage [V]"] = 0.35 adapter.validate_python(test) def test_user_defined(self) -> None: - test = copy.copy(self.base) + test = copy.deepcopy(self.base) test["Parameterisation"]["User-defined"] = { "a": 1.0, "b": 2.0, @@ -358,7 +359,7 @@ def test_user_defined(self) -> None: assert obj.parameterisation.user_defined.c == 3 def test_user_defined_table(self) -> None: - test = copy.copy(self.base) + test = copy.deepcopy(self.base) test["Parameterisation"]["User-defined"] = { "a": { "x": [1.0, 2.0], @@ -368,12 +369,12 @@ def test_user_defined_table(self) -> None: adapter.validate_python(test) def test_user_defined_function(self) -> None: - test = copy.copy(self.base) + test = copy.deepcopy(self.base) test["Parameterisation"]["User-defined"] = {"a": "2.0 * x"} adapter.validate_python(test) def test_bad_user_defined(self) -> None: - test = copy.copy(self.base) + test = copy.deepcopy(self.base) # bool not allowed type test["Parameterisation"]["User-defined"] = { "bad": True,