Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Python 3.12, numpy >= 1.24 and pytest 8.0.x. #437

Merged
merged 6 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: '3.12'
- name: Install dependencies
run: |
pip install .
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ['3.9', '3.11']
python-version: ['3.9', '3.12']

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ sphinx:
build:
os: "ubuntu-22.04"
tools:
python: "3.11"
python: "3.12"

python:
install:
Expand Down
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
4 changes: 2 additions & 2 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
29 changes: 29 additions & 0 deletions docs/whats_new.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
What's New
==========
v3.13.3 (2024/02/02)
--------------------
New Features
~~~~~~~~~~~~

Breaking changes
~~~~~~~~~~~~~~~~

Deprecations
~~~~~~~~~~~~

Bug fixes
~~~~~~~~~

Documentation
~~~~~~~~~~~~~
- Improve documentation for :py:mod:`netCDF4` dependency. (`@enekomartinmartinez <https://github.com/enekomartinmartinez>`_)

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>`_)
- Custom error messages when :py:mod:`netCDF4` is missing (:issue:`435`). (`@enekomartinmartinez <https://github.com/enekomartinmartinez>`_)

v3.13.2 (2024/01/09)
--------------------
New Features
Expand Down
2 changes: 1 addition & 1 deletion pysd/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "3.13.2"
__version__ = "3.13.3"
5 changes: 2 additions & 3 deletions pysd/builders/python/python_expressions_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
6 changes: 5 additions & 1 deletion pysd/py_backend/allocation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
27 changes: 19 additions & 8 deletions pysd/py_backend/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -534,7 +539,11 @@ def initialize_external_data(self, externals=None):
if not externals.is_file():
raise FileNotFoundError(f"Invalid file path ({str(externals)})")

ds = xr.open_dataset(externals)
try:
ds = xr.open_dataset(externals)
except ValueError: # pragma: no cover
raise ModuleNotFoundError("No module named 'netCDF4'")


for ext in self._external_elements:
if ext.py_name in ds.data_vars.keys():
Expand Down Expand Up @@ -607,6 +616,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 = {}
Expand Down Expand Up @@ -679,7 +693,10 @@ def serialize_externals(self, export_path="externals.nc",
for key, values in metadata.items():
ds[key].attrs = values

ds.to_netcdf(export_path)
try:
ds.to_netcdf(export_path)
except KeyError: # pragma: no cover
raise ModuleNotFoundError("No module named 'netCDF4'")

def __include_for_serialization(self, ext, py_name_clean, data, metadata,
lookup_dims, data_dims):
Expand Down Expand Up @@ -1125,12 +1142,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
Expand Down
2 changes: 1 addition & 1 deletion pysd/py_backend/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ def make_flat_df(df, return_addresses, flatten=False):
if is_dataarray and values[0].size == 1:
# some elements are returned as 0-d arrays, convert
# them to float
values = [float(x) for x in values]
values = [x.squeeze().values[()] for x in values]
is_dataarray = False

if flatten and is_dataarray:
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
numpy<1.24
numpy>=1.23
pandas[excel]
parsimonious
xarray>=2023.9
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
],
install_requires=open('requirements.txt').read().strip().split('\n'),
package_data={
Expand Down
5 changes: 4 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,8 @@ def ignore_warns():
# warnings to be ignored in the integration tests
return [
"numpy.ndarray size changed, may indicate binary incompatibility.",
"Creating an ndarray from ragged nested sequences.*"
"Creating an ndarray from ragged nested sequences.*",
"datetime.datetime.* is deprecated and scheduled for removal in a "
"future version. Use timezone-aware objects to represent datetimes "
"in UTC.*",
]
4 changes: 3 additions & 1 deletion tests/pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@
python_files = pytest_*/**/*.py pytest_*/*.py
filterwarnings =
error
ignore:numpy.ndarray size changed, may indicate binary incompatibility.:RuntimeWarning
always:numpy.ndarray size changed, may indicate binary incompatibility.:RuntimeWarning
always::DeprecationWarning
always::PendingDeprecationWarning
7 changes: 4 additions & 3 deletions tests/pytest_builders/pytest_python.py
Original file line number Diff line number Diff line change
@@ -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\
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,7 @@
"file": "test_subscripted_round.mdl",
"warns": [
"Variable '.*' is defined with different .*types: '.*'",
"invalid value encountered in cast"
]
},
"subscripted_smooth": {
Expand Down
24 changes: 17 additions & 7 deletions tests/pytest_pysd/pytest_errors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import pytest
import re
import shutil

import pytest

from pysd import read_vensim, read_xmile, load


Expand Down Expand Up @@ -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 "
Expand Down
4 changes: 2 additions & 2 deletions tests/pytest_pysd/pytest_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 \^ "\
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion tests/pytest_pysd/pytest_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class EmptyHandler(OutputHandlerInterface):

@pytest.mark.parametrize("model_path", [test_model_look])
def test_invalid_output_file(self, model):
error_message = "expected str, bytes or os.PathLike object, not int"
error_message = ".* str.* os.PathLike object.*"
with pytest.raises(TypeError, match=error_message):
model.run(output_file=1234)

Expand Down
32 changes: 22 additions & 10 deletions tests/pytest_pysd/pytest_pysd.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
from pathlib import Path

import pytest
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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)

Expand Down