From 04b43038cda8b57ba0aa37fd7589a3829f99639f Mon Sep 17 00:00:00 2001 From: Eneko Martin-Martinez <eneko.martin.martinez@gmail.com> Date: Fri, 2 Feb 2024 00:06:19 +0100 Subject: [PATCH] Suport newer versions --- docs/conf.py | 2 +- docs/installation.rst | 4 +-- docs/whats_new.rst | 27 +++++++++++++++ pysd/_version.py | 2 +- .../python/python_expressions_builder.py | 5 ++- pysd/py_backend/allocation.py | 6 +++- pysd/py_backend/model.py | 16 +++++---- requirements.txt | 2 +- tests/pytest_builders/pytest_python.py | 7 ++-- tests/pytest_pysd/pytest_errors.py | 24 ++++++++++---- tests/pytest_pysd/pytest_functions.py | 4 +-- tests/pytest_pysd/pytest_pysd.py | 32 ++++++++++++------ tests/pytest_pysd/pytest_select_submodel.py | 33 ++++++++++++------- .../pytest_translators/pytest_split_views.py | 10 ++++-- tests/pytest_translators/pytest_vensim.py | 9 ++--- .../pytest_types/external/pytest_external.py | 20 +++++++++-- .../statefuls/pytest_statefuls.py | 20 +++++------ 17 files changed, 152 insertions(+), 71 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 3b98ca8c..6fc3527a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -158,7 +158,7 @@ 'xarray': ('https://docs.xarray.dev/en/stable/', None), 'numpy': ('https://numpy.org/doc/stable/', None), 'scipy': ('https://docs.scipy.org/doc/scipy/', None), - 'pytest': ('https://docs.pytest.org/en/7.1.x/', None), + 'pytest': ('https://docs.pytest.org/en/8.0.x/', None), 'openpyxl': ('https://openpyxl.readthedocs.io/en/stable', None) } diff --git a/docs/installation.rst b/docs/installation.rst index 154199d5..3f509113 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -41,7 +41,7 @@ PySD requires **Python 3.9** or above. PySD builds on the core Python data analytics stack, and the following third party libraries: -* Numpy < 1.24 +* Numpy >= 1.23 * Scipy * Pandas (with Excel support: `pip install pandas[excel]`) * Parsimonious @@ -65,7 +65,7 @@ In order to plot model outputs as shown in :doc:`Getting started <../getting_sta * Matplotlib -To export data to netCDF (*.nc*) files: +To export data to netCDF (*.nc*) files or to serialize external objects: * netCDF4 diff --git a/docs/whats_new.rst b/docs/whats_new.rst index 8cfab142..40509b9f 100644 --- a/docs/whats_new.rst +++ b/docs/whats_new.rst @@ -1,5 +1,32 @@ What's New ========== +v3.13.3 (2024/02/02) +-------------------- +New Features +~~~~~~~~~~~~ + +Breaking changes +~~~~~~~~~~~~~~~~ + +Deprecations +~~~~~~~~~~~~ + +Bug fixes +~~~~~~~~~ + +Documentation +~~~~~~~~~~~~~ + +Performance +~~~~~~~~~~~ + +Internal Changes +~~~~~~~~~~~~~~~~ +- Support for Python 3.12. (`@enekomartinmartinez <https://github.com/enekomartinmartinez>`_) +- Support for :py:mod:`numpy` >= 1.24. (`@enekomartinmartinez <https://github.com/enekomartinmartinez>`_) +- Correct some warnings management in the tests. (`@enekomartinmartinez <https://github.com/enekomartinmartinez>`_) +- Fix :py:mod:`numpy` requirements to >= 1.23 to follow `NEP29 <https://numpy.org/neps/nep-0029-deprecation_policy.html>`_. (`@enekomartinmartinez <https://github.com/enekomartinmartinez>`_) + v3.13.2 (2024/01/09) -------------------- New Features diff --git a/pysd/_version.py b/pysd/_version.py index d8c1804a..49979b06 100644 --- a/pysd/_version.py +++ b/pysd/_version.py @@ -1 +1 @@ -__version__ = "3.13.2" +__version__ = "3.13.3" diff --git a/pysd/builders/python/python_expressions_builder.py b/pysd/builders/python/python_expressions_builder.py index bd11bfc7..fba09fbe 100644 --- a/pysd/builders/python/python_expressions_builder.py +++ b/pysd/builders/python/python_expressions_builder.py @@ -460,10 +460,9 @@ def build_not_implemented(self, arguments: dict) -> BuildAST: """ final_subscripts = self.reorder(arguments) warnings.warn( - "\n\nTrying to translate '" - + self.function.upper().replace("_", " ") + "Trying to translate '"+ self.function.upper().replace("_", " ") + "' which it is not implemented on PySD. The translated " - + "model will crash... " + + "model will crash..." ) self.section.imports.add("functions", "not_implemented_function") diff --git a/pysd/py_backend/allocation.py b/pysd/py_backend/allocation.py index 81272543..699e263d 100644 --- a/pysd/py_backend/allocation.py +++ b/pysd/py_backend/allocation.py @@ -106,7 +106,11 @@ def get_functions(cls, q0, pp, kind): interval = interval.union(i) # Full allocation function -> function to solve - def full_allocation(x): return np.sum([func(x) for func in functions]) + def full_allocation(x): + if isinstance(x, np.ndarray): + # Fix to solve issues in the newest numpy versions + x = x.squeeze()[()] + return np.sum([func(x) for func in functions]) def_intervals = [] for subinterval in interval: diff --git a/pysd/py_backend/model.py b/pysd/py_backend/model.py index 3f69a189..38828f82 100644 --- a/pysd/py_backend/model.py +++ b/pysd/py_backend/model.py @@ -516,6 +516,11 @@ def initialize_external_data(self, externals=None): -------- :func:`pysd.py_backend.model.Macro.serialize_externals` + Note + ---- + To load externals from a netCDF file you need to have installed + the optional dependency `netCDF4`. + """ if not externals: @@ -607,6 +612,11 @@ def serialize_externals(self, export_path="externals.nc", -------- :func:`pysd.py_backend.model.Macro.initialize_external_data` + Note + ---- + To run this function you need to have installed the optional + dependency `netCDF4`. + """ data = {} metadata = {} @@ -1125,12 +1135,6 @@ def _set_components(self, params, new): new_function = self._constant_component(value, dims) self._dependencies[func_name] = {} - # this won't handle other statefuls... - if '_integ_' + func_name in dir(self.components): - warnings.warn("Replacing the equation of stock " - "'{}' with params...".format(key), - stacklevel=2) - # copy attributes from the original object to proper working # of internal functions new_function.__name__ = func_name diff --git a/requirements.txt b/requirements.txt index 83131a0c..2e14f4ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy +numpy>=1.23 pandas[excel] parsimonious xarray>=2023.9 diff --git a/tests/pytest_builders/pytest_python.py b/tests/pytest_builders/pytest_python.py index 3fb6dcfa..67890d5a 100644 --- a/tests/pytest_builders/pytest_python.py +++ b/tests/pytest_builders/pytest_python.py @@ -1,6 +1,7 @@ -import pytest from pathlib import Path +import pytest + from pysd.builders.python.namespace import NamespaceManager from pysd.builders.python.subscripts import SubscriptManager from pysd.builders.python.python_model_builder import\ @@ -147,13 +148,13 @@ def test_referencebuilder_subscripts_warning(self, component, component.section.subscripts.mapping = { dim: [] for dim in subscripts} component.section.namespace.namespace = namespace - warning_message =\ + warn_message =\ f"The reference to '{origin_name}' in variable 'My Var' has "\ r"duplicated subscript ranges\. If mapping is used in one "\ r"of them, please, rewrite reference subscripts to avoid "\ r"duplicates\. Otherwise, the final model may crash\.\.\."\ - with pytest.warns(UserWarning, match=warning_message): + with pytest.warns(UserWarning, match=warn_message): ReferenceBuilder(reference_str, component) @pytest.mark.parametrize( diff --git a/tests/pytest_pysd/pytest_errors.py b/tests/pytest_pysd/pytest_errors.py index 58f4d79a..3ffe489a 100644 --- a/tests/pytest_pysd/pytest_errors.py +++ b/tests/pytest_pysd/pytest_errors.py @@ -1,6 +1,8 @@ -import pytest +import re import shutil +import pytest + from pysd import read_vensim, read_xmile, load @@ -92,13 +94,21 @@ def test_loading_error(loader, model_path, raise_type, error_message): ] ) def test_not_implemented_and_incomplete(model_path): - with pytest.warns(UserWarning) as ws: + with pytest.warns() as record: model = read_vensim(model_path) - assert "'incomplete var' has no equation specified"\ - in str(ws[0].message) - assert "Trying to translate 'MY FUNC' which it is not implemented"\ - " on PySD. The translated model will crash..."\ - in str(ws[1].message) + + warn_message = "'incomplete var' has no equation specified" + assert any([ + re.match(warn_message, str(warn.message)) + for warn in record + ]), f"Couldn't match warning:\n{warn_message}" + + warn_message = "Trying to translate 'MY FUNC' which it is "\ + "not implemented on PySD. The translated model will crash..." + assert any([ + re.match(warn_message, str(warn.message)) + for warn in record + ]), f"Couldn't match warning:\n{warn_message}" with pytest.warns(RuntimeWarning, match="Call to undefined function, calling dependencies " diff --git a/tests/pytest_pysd/pytest_functions.py b/tests/pytest_pysd/pytest_functions.py index 72059e7e..a89367c9 100644 --- a/tests/pytest_pysd/pytest_functions.py +++ b/tests/pytest_pysd/pytest_functions.py @@ -574,7 +574,7 @@ def test_get_time_value_errors(self, measure, relativeto, lambda: 0, relativeto, np.random.randint(-100, 100), measure) def test_vector_select(self): - warning_message =\ + warn_message =\ r"Vensim's help says that numerical_action=5 computes the "\ r"product of selection_array \^ expression_array\. But, in fact,"\ r" Vensim is computing the product of expression_array \^ "\ @@ -584,7 +584,7 @@ def test_vector_select(self): array = xr.DataArray([3, 10, 2], {'dim': ["A", "B", "C"]}) sarray = xr.DataArray([1, 0, 2], {'dim': ["A", "B", "C"]}) - with pytest.warns(UserWarning, match=warning_message): + with pytest.warns(UserWarning, match=warn_message): assert vector_select(sarray, array, ["dim"], np.nan, 5, 1)\ == 12 diff --git a/tests/pytest_pysd/pytest_pysd.py b/tests/pytest_pysd/pytest_pysd.py index cb5b85fa..e38cbc09 100644 --- a/tests/pytest_pysd/pytest_pysd.py +++ b/tests/pytest_pysd/pytest_pysd.py @@ -1,3 +1,4 @@ +import re from pathlib import Path import pytest @@ -140,13 +141,13 @@ def test_run_return_timestamps(self, model): # assert one timestamp is not returned because is not multiple of # the time step - warning_message =\ + warn_message =\ "The returning time stamp '%s' seems to not be a multiple "\ "of the time step. This value will not be saved in the output. "\ "Please, modify the returning timestamps or the integration "\ "time step to avoid this." # assert that return_timestamps works with float error - with pytest.warns(UserWarning, match=warning_message % 0.55): + with pytest.warns(UserWarning, match=warn_message % 0.55): stocks = model.run( time_step=0.1, return_timestamps=[0.3, 0.1, 0.55, 0.9]) assert 0.1 in stocks.index @@ -155,7 +156,7 @@ def test_run_return_timestamps(self, model): assert 0.55 not in stocks.index with pytest.warns(UserWarning, - match=warning_message % "(0.15|0.55|0.95)"): + match=warn_message % "(0.15|0.55|0.95)"): stocks = model.run( time_step=0.1, return_timestamps=[0.3, 0.15, 0.55, 0.95]) assert 0.15 not in stocks.index @@ -348,13 +349,19 @@ def test_set_component_with_real_name(self, model): @pytest.mark.parametrize("model_path", [test_model]) def test_set_components_warnings(self, model): """Addresses https://github.com/SDXorg/pysd/issues/80""" - warn_message = r"Replacing the equation of stock "\ - r"'Teacup Temperature' with params\.\.\." - with pytest.warns(UserWarning, match=warn_message): + warn_message = r"Replacing the value of Stateful variable with "\ + r"an expression\. To set initial conditions use "\ + r"`set_initial_condition` instead\.\.\." + with pytest.warns() as record: model.set_components( {"Teacup Temperature": 20, "Characteristic Time": 15} ) # set stock value using params + assert any([ + re.match(warn_message, str(warn.message)) + for warn in record + ]), f"Couldn't match warning:\n{warn_message}" + @pytest.mark.parametrize("model_path", [test_model]) def test_set_components_with_function(self, model): def test_func(): @@ -967,12 +974,17 @@ def test_set_initial_value_subscripted_value_with_numpy_error(self, model): @pytest.mark.parametrize("model_path", [test_model]) def test_replace_stateful(self, model): - warn_message = "Replacing the value of Stateful variable with "\ - "an expression. To set initial conditions use "\ - "`set_initial_condition` instead..." - with pytest.warns(UserWarning, match=warn_message): + warn_message = r"Replacing the value of Stateful variable with "\ + r"an expression\. To set initial conditions use "\ + r"`set_initial_condition` instead\.\.\." + with pytest.warns() as record: model.components.teacup_temperature = 3 + assert any([ + re.match(warn_message, str(warn.message)) + for warn in record + ]), f"Couldn't match warning:\n{warn_message}" + stocks = model.run() assert np.all(stocks["Teacup Temperature"] == 3) diff --git a/tests/pytest_pysd/pytest_select_submodel.py b/tests/pytest_pysd/pytest_select_submodel.py index f16dbea5..bcec7b0e 100644 --- a/tests/pytest_pysd/pytest_select_submodel.py +++ b/tests/pytest_pysd/pytest_select_submodel.py @@ -1,9 +1,9 @@ - -import pytest +import re import shutil from pathlib import Path -import numpy as np +import pytest +import numpy as np import pysd from pysd.translators.vensim.vensim_file import VensimFile @@ -60,7 +60,7 @@ class TestSubmodel: "Lookup table dependencies", "Stateful objects integrated with the selected variables" ] - warning = "Selecting submodel, "\ + warn_message = "Selecting submodel, "\ + "to run the full model again use model.reload()" common_vars = { 'initial_time', 'time_step', 'final_time', 'time', 'saveper', 'stock' @@ -141,11 +141,12 @@ def test_select_submodel(self, model, variables, modules, assert "stock" in model._doc["Py Name"].to_list() # select submodel - with pytest.warns(UserWarning) as record: + with pytest.warns() as record: model.select_submodel(vars=variables, modules=modules) # assert warning - assert str(record[0].message) == self.warning + record = [str(r.message) for r in record] + assert self.warn_message in record # assert stateful elements change assert len(model._dynamicstateful_elements) == 1 @@ -170,16 +171,20 @@ def test_select_submodel(self, model, variables, modules, # running the model without redefining dependencies will # produce nan values assert "Exogenous components for the following variables are"\ - + " necessary but not given:" in str(record[-1].message) + + " necessary but not given:" in record[-1] assert "Please, set them before running the model using "\ - + "set_components method..." in str(record[-1].message) + + "set_components method..." in record[-1] for var in dep_vars: - assert var in str(record[-1].message) + assert var in record[-1] assert np.any(np.isnan(model.run())) # redefine dependencies warn_message = "Replacing a variable by a constant value." - with pytest.warns(UserWarning, match=warn_message): + with pytest.warns(UserWarning) as record: out = model.run(params=dep_vars) + assert any([ + re.match(warn_message, str(warn.message)) + for warn in record + ]), f"Couldn't match warning:\n{warn_message}" assert not np.any(np.isnan(out)) # select submodel using contour values @@ -215,7 +220,7 @@ def test_select_submodel_copy(self, model, variables, modules, # assert warning record = [str(r.message) for r in record] - assert self.warning in record + assert self.warn_message in record # assert original stateful elements assert len(model._dynamicstateful_elements) == 2 @@ -263,8 +268,12 @@ def test_select_submodel_copy(self, model, variables, modules, assert np.any(np.isnan(model2.run())) # redefine dependencies warn_message = "Replacing a variable by a constant value." - with pytest.warns(UserWarning, match=warn_message): + with pytest.warns(UserWarning) as record: out = model2.run(params=dep_vars) + assert any([ + re.match(warn_message, str(warn.message)) + for warn in record + ]), f"Couldn't match warning:\n{warn_message}" assert not np.any(np.isnan(out)) # select submodel using contour values diff --git a/tests/pytest_translators/pytest_split_views.py b/tests/pytest_translators/pytest_split_views.py index 88b316cc..a68d8c29 100644 --- a/tests/pytest_translators/pytest_split_views.py +++ b/tests/pytest_translators/pytest_split_views.py @@ -273,7 +273,7 @@ def test_read_vensim_split_model_warnings(self, model_file, subview_sep, @pytest.mark.parametrize( - "model_path,subview_sep,warning_message", + "model_path,subview_sep,warn_message", [ ( # warning_noviews Path("test-models/samples/teacup/teacup.mdl"), @@ -306,6 +306,10 @@ def model(self, shared_tmpdir, model_path, _root): shutil.copy(_root.joinpath(model_path), file) return file - def test_split_view_warnings(self, model, subview_sep, warning_message): - with pytest.warns(UserWarning, match=warning_message): + def test_split_view_warnings(self, model, subview_sep, warn_message): + with pytest.warns() as record: pysd.read_vensim(model, split_views=True, subview_sep=subview_sep) + assert any([ + re.match(warn_message, str(warn.message)) + for warn in record + ]), f"Couldn't match warning:\n{warn_message}" diff --git a/tests/pytest_translators/pytest_vensim.py b/tests/pytest_translators/pytest_vensim.py index 7b2f46e4..6f3db342 100644 --- a/tests/pytest_translators/pytest_vensim.py +++ b/tests/pytest_translators/pytest_vensim.py @@ -1,9 +1,10 @@ -import pytest from pathlib import Path + +import pytest + from parsimonious import VisitationError import pysd - from pysd.translators.vensim.vensim_file import VensimFile from pysd.translators.vensim.vensim_element import Element @@ -113,9 +114,9 @@ def test_subscript_range_error(self, element, error_message): ) def test_complex_mapping(self, element, mapping): # parse the mapping - warning_message = r"Subscript mapping detected\. "\ + warn_message = r"Subscript mapping detected\. "\ r"This feature works only for simple cases\." - with pytest.warns(UserWarning, match=warning_message): + with pytest.warns(UserWarning, match=warn_message): out = element.parse() assert out.mapping == mapping diff --git a/tests/pytest_types/external/pytest_external.py b/tests/pytest_types/external/pytest_external.py index 8ec5a184..2afb92aa 100644 --- a/tests/pytest_types/external/pytest_external.py +++ b/tests/pytest_types/external/pytest_external.py @@ -1,7 +1,9 @@ import sys +import re +import importlib.util + import pytest -import importlib.util import numpy as np import xarray as xr @@ -1974,9 +1976,15 @@ def test_data_interp_h1dm_row(self, _root): final_coords=coords, py_name=py_name) - with pytest.warns(UserWarning, match="Not able to interpolate"): + with pytest.warns(UserWarning) as record: data.initialize() + warn_message = "Not able to interpolate" + assert any([ + re.match(warn_message, str(warn.message)) + for warn in record + ]), f"Couldn't match warning:\n{warn_message}" + assert all(np.isnan(data.data.values)) def test_data_interp_h1dm_row2(self, _root): @@ -2006,9 +2014,15 @@ def test_data_interp_h1dm_row2(self, _root): final_coords=coords, py_name=py_name) - with pytest.warns(UserWarning, match="Not able to interpolate"): + with pytest.warns(UserWarning) as record: data.initialize() + warn_message = "Not able to interpolate" + assert any([ + re.match(warn_message, str(warn.message)) + for warn in record + ]), f"Couldn't match warning:\n{warn_message}" + assert not any(np.isnan(data.data.loc[:, "B"].values)) assert not any(np.isnan(data.data.loc[:, "C"].values)) assert all(np.isnan(data.data.loc[:, "D"].values)) diff --git a/tests/pytest_types/statefuls/pytest_statefuls.py b/tests/pytest_types/statefuls/pytest_statefuls.py index c7060f64..8251d52e 100644 --- a/tests/pytest_types/statefuls/pytest_statefuls.py +++ b/tests/pytest_types/statefuls/pytest_statefuls.py @@ -178,26 +178,22 @@ def test_delay_order(self): tstep=lambda: 0.5, py_name='delay5') - warning_message = "Delay time very small, casting delay order "\ - "from 3 to 2" - with pytest.warns(UserWarning, match=warning_message): + warn_message = "Delay time very small, casting delay order from 3 to 2" + with pytest.warns(UserWarning, match=warn_message): delay1.initialize() - warning_message = "Delay time very small, casting delay order "\ - "from 3 to 2" - with pytest.warns(UserWarning, match=warning_message): + with pytest.warns(UserWarning, match=warn_message): delay2.initialize() - warning_message = r"Casting delay order from 1\.5 to 1" - with pytest.warns(UserWarning, match=warning_message): + warn_message = r"Casting delay order from 1\.5 to 1" + with pytest.warns(UserWarning, match=warn_message): delay3.initialize() - warning_message = r"Casting delay order from 1\.5 to 1" - with pytest.warns(UserWarning, match=warning_message): + with pytest.warns(UserWarning, match=warn_message): delay4.initialize() - warning_message = r"Casting delay order from 1\.500000 to 2" - with pytest.warns(UserWarning, match=warning_message): + warn_message = r"Casting delay order from 1\.500000 to 2" + with pytest.warns(UserWarning, match=warn_message): delay5.initialize() def test_forecast(self):