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):