Skip to content

Commit

Permalink
Merge pull request #12 from khaeru/issue/4
Browse files Browse the repository at this point in the history
Increase test coverage to 100%
  • Loading branch information
khaeru committed Jan 17, 2021
2 parents 4e311c4 + 9791ab6 commit 6f9cce2
Show file tree
Hide file tree
Showing 15 changed files with 204 additions and 38 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ dist

# pytest, pytest-benchmark, pytest-cov
.benchmarks
.coverage
.coverage*
.pytest_cache
htmlcov

Expand Down
2 changes: 2 additions & 0 deletions genno/compat/_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# For testing only
HAS__TEST = False
3 changes: 2 additions & 1 deletion genno/compat/ixmp/reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,9 @@ def from_scenario(cls, scenario, **kwargs):
# Convert Series to list; protect list so that dask schedulers
# do not try to interpret its contents as further tasks
elements = dask.core.quote(elements.tolist())
except AttributeError:
except AttributeError: # pragma: no cover
# pd.DataFrame for a multidimensional set; store as-is
# TODO write tests for this downstream (in ixmp)
pass

rep.add(RENAME_DIMS.get(name, name), elements)
Expand Down
5 changes: 4 additions & 1 deletion genno/compat/pyam/computations.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ def as_pyam(
)

if len(replace) and not isinstance(next(iter(replace.values())), dict):
warn("Outdated replace_vars argument", DeprecationWarning)
warn(
"replace must be nested dict(), e.g. dict(variable={repr(replace)})",
DeprecationWarning,
)
replace = dict(variable=replace)

# - Convert to pd.DataFrame
Expand Down
4 changes: 2 additions & 2 deletions genno/core/attrseries.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ def squeeze(self, dim=None, *args, **kwargs):
continue
else:
raise ValueError(
"cannot select a dimension to squeeze out which has "
"length greater than one"
"cannot select a dimension to squeeze out which has length "
"greater than one"
)

to_drop.append(name)
Expand Down
34 changes: 19 additions & 15 deletions genno/core/computer.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,22 +234,22 @@ def add(self, data, *args, **kwargs):
# Add a single computation (without converting to Key)
return self.add_single(key, *computation, **kwargs)
else:
# Some other kind of import
raise ValueError(data)
# Some other kind of input
raise TypeError(data)

def add_queue(self, queue, max_tries=1, fail="raise"):
"""Add tasks from a list or `queue`.
Parameters
----------
queue : list of 2-tuple
The members of each tuple are the arguments (i.e. a list or tuple)
and keyword arguments (i.e. a dict) to :meth:`add`.
The members of each tuple are the arguments (i.e. a list or tuple) and
keyword arguments (i.e. a dict) to :meth:`add`.
max_tries : int, optional
Retry adding elements up to this many times.
fail : 'raise' or log level, optional
Action to take when a computation from `queue` cannot be added
after `max_tries`.
Action to take when a computation from `queue` cannot be added after
`max_tries`.
"""
# Elements to retry: list of (tries, args, kwargs)
retry = []
Expand Down Expand Up @@ -280,12 +280,16 @@ def _log(level):
# retry silently
retry.append((count + 1, (args, kwargs)))
else:
# More than *max_tries* failures; something
# More than *max_tries* failures; something has gone wrong
if fail == "raise":
_log(logging.ERROR)
raise
else:
_log(getattr(logging, fail.upper()))
_log(
getattr(logging, fail.upper())
if isinstance(fail, str)
else fail
)

return added

Expand Down Expand Up @@ -534,7 +538,7 @@ def aggregate(self, qty, tag, dims_or_groups, weights=None, keep=True, sums=Fals
return self.add(key, comp, strict=True, index=True, sums=sums)

def disaggregate(self, qty, new_dim, method="shares", args=[]):
"""Add a computation that disaggregates *qty* using *method*.
"""Add a computation that disaggregates `qty` using `method`.
Parameters
----------
Expand All @@ -543,12 +547,12 @@ def disaggregate(self, qty, new_dim, method="shares", args=[]):
new_dim: str
Name of the new dimension of the disaggregated variable.
method: callable or str
Disaggregation method. If a callable, then it is applied to *var*
with any extra *args*. If then a method named
'disaggregate_{method}' is used.
Disaggregation method. If a callable, then it is applied to `var` with any
extra `args`. If a string, then a method named 'disaggregate_{method}' is
used.
args: list, optional
Additional arguments to the *method*. The first element should be
the key for a quantity giving shares for disaggregation.
Additional arguments to the `method`. The first element should be the key
for a quantity giving shares for disaggregation.
Returns
-------
Expand All @@ -567,7 +571,7 @@ def disaggregate(self, qty, new_dim, method="shares", args=[]):
"No disaggregation method 'disaggregate_{}'".format(method)
)
if not callable(method):
raise ValueError(method)
raise TypeError(method)

return self.add(key, tuple([method, qty] + args), strict=True)

Expand Down
16 changes: 10 additions & 6 deletions genno/core/quantity.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,16 @@ def __call__(self, data, *args, **kwargs):
result = cls.from_series(data)
elif self.CLASS == "AttrSeries":
result = cls(data, *args, **kwargs)
elif len(args) == len(kwargs) == 0:
# Single argument, possibly an xr.DataArray; convert to
# SparseDataArray
result = data._sda.convert()
else:
result = cls(data, *args, **kwargs)
try:
if len(args) == len(kwargs) == 0:
# Single argument, possibly an xr.DataArray; convert to
# SparseDataArray
result = data._sda.convert()
else: # pragma: no cover
result = cls(data, *args, **kwargs)
except AttributeError:
result = cls(data, *args, **kwargs)

if name:
result.name = name
Expand All @@ -79,5 +83,5 @@ def assert_quantity(*args):
for i, arg in enumerate(args):
if arg.__class__.__name__ != Quantity.CLASS:
raise TypeError(
f"arg #{i} ({repr(arg)}) is not Quantity; likely an incorrect " "key"
f"arg #{i+1} ({repr(arg)}) is not Quantity; likely an incorrect key"
)
3 changes: 2 additions & 1 deletion genno/tests/compat/test_pyam.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ def add_tm(df, name="Activity"):
key3 = c.convert_pyam(
ACT, "ya", replace_vars="activity variables", collapse=add_tm
).pop()
df3 = c.get(key3).as_pandas()
with pytest.deprecated_call():
df3 = c.get(key3).as_pandas()

# Values are the same; different names
exp = df2[df2.variable == "Activity|canning_plant|production"][
Expand Down
70 changes: 59 additions & 11 deletions genno/tests/core/test_computer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import logging

import pandas as pd
import pint
import pytest
Expand All @@ -15,12 +17,45 @@
from genno.testing import assert_qty_equal


def test_get():
"""Computer.get() using a default key."""
c = Computer()

# No default key is set
with pytest.raises(ValueError, match="no default reporting key set"):
c.get()

c.configure(default="foo")
c.add("foo", 42)

# Default key is used
assert c.get() == 42


def test_get_comp():
# Invalid name for a function returns None
assert Computer()._get_comp(42) is None


def test_require_compat():
c = Computer()
with pytest.raises(
ModuleNotFoundError,
match="No module named '_test', required by genno.compat._test",
):
c._require_compat("_test")


def test_add():
"""Adding computations that refer to missing keys raises KeyError."""
r = Computer()
r.add("a", 3)
r.add("d", 4)

# Invalid: value before key
with pytest.raises(TypeError):
r.add(42, "a")

# Adding an existing key with strict=True
with pytest.raises(KeyExistsError, match=r"key 'a' already exists"):
r.add("a", 5, strict=True)
Expand Down Expand Up @@ -72,7 +107,7 @@ def msg(*keys):
r.add("select", "bar", "a", bad_kwarg="foo", index=True)


def test_add_queue():
def test_add_queue(caplog):
r = Computer()
r.add("foo-0", (lambda x: x, 42))

Expand All @@ -98,6 +133,11 @@ def _product(a, b):
# correct result
assert r.get("foo-2") == 42 * 10 * 10

# Failures without raising an exception
r.add(queue, max_tries=3, fail=logging.INFO)
assert "Failed 3 times to add:" in caplog.messages
assert " with KeyExistsError('foo-2')" in caplog.messages


def test_apply():
# Reporter with two scalar values
Expand Down Expand Up @@ -145,6 +185,9 @@ def useless():

r.apply(useless)

# Also call via add()
r.add("apply", useless)

# Nothing added to the reporter
assert len(r.keys()) == N

Expand Down Expand Up @@ -181,6 +224,9 @@ def test_disaggregate():
with pytest.raises(ValueError):
r.disaggregate(foo, "d", method="baz")

with pytest.raises(TypeError):
r.disaggregate(foo, "d", method=None)


def test_file_io(tmp_path):
r = Computer()
Expand Down Expand Up @@ -275,22 +321,24 @@ def test_full_key():

def test_units(ureg):
"""Test handling of units within computations."""
r = Computer()
c = Computer()

assert isinstance(c.unit_registry, pint.UnitRegistry)

# Create some dummy data
dims = dict(coords=["a b c".split()], dims=["x"])
r.add("energy:x", Quantity(xr.DataArray([1.0, 3, 8], **dims), units="MJ"))
r.add("time", Quantity(xr.DataArray([5.0, 6, 8], **dims), units="hour"))
r.add("efficiency", Quantity(xr.DataArray([0.9, 0.8, 0.95], **dims)))
c.add("energy:x", Quantity(xr.DataArray([1.0, 3, 8], **dims), units="MJ"))
c.add("time", Quantity(xr.DataArray([5.0, 6, 8], **dims), units="hour"))
c.add("efficiency", Quantity(xr.DataArray([0.9, 0.8, 0.95], **dims)))

# Aggregation preserves units
r.add("energy", (computations.sum, "energy:x", None, ["x"]))
assert r.get("energy").attrs["_unit"] == ureg.parse_units("MJ")
c.add("energy", (computations.sum, "energy:x", None, ["x"]))
assert c.get("energy").attrs["_unit"] == ureg.parse_units("MJ")

# Units are derived for a ratio of two quantities
r.add("power", (computations.ratio, "energy:x", "time"))
assert r.get("power").attrs["_unit"] == ureg.parse_units("MJ/hour")
c.add("power", (computations.ratio, "energy:x", "time"))
assert c.get("power").attrs["_unit"] == ureg.parse_units("MJ/hour")

# Product of dimensioned and dimensionless quantities keeps the former
r.add("energy2", (computations.product, "energy:x", "efficiency"))
assert r.get("energy2").attrs["_unit"] == ureg.parse_units("MJ")
c.add("energy2", (computations.product, "energy:x", "efficiency"))
assert c.get("energy2").attrs["_unit"] == ureg.parse_units("MJ")
15 changes: 15 additions & 0 deletions genno/tests/core/test_describe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import pandas as pd
import pytest

from genno import Computer, Quantity


@pytest.mark.usefixtures("parametrize_quantity_class")
def test_describe():
c = Computer()
c.add("foo", Quantity(pd.Series([42, 43])))

if Quantity.CLASS == "SparseDataArray":
assert "\n".join(
["'foo':", "- <xarray.SparseDataArray (index: 2)>"]
) == c.describe("foo")
26 changes: 26 additions & 0 deletions genno/tests/core/test_quantity.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Tests for genno.quantity."""
import re

import numpy as np
import pandas as pd
import pytest
Expand All @@ -8,6 +10,7 @@

from genno import Quantity, Reporter, computations
from genno.core.attrseries import AttrSeries
from genno.core.quantity import assert_quantity
from genno.core.sparsedataarray import SparseDataArray
from genno.testing import assert_qty_allclose, assert_qty_equal

Expand Down Expand Up @@ -65,12 +68,25 @@ def _make_values():

yield scen

@pytest.mark.filterwarnings(
"ignore:.*default dtype for empty Series.*:DeprecationWarning"
)
def test_init(self):
"""Instantiated from a scalar object."""
Quantity(object())

def test_assert(self, a):
"""Test assertions about Quantity.
These are tests without `attr` property, in which case direct pd.Series
and xr.DataArray comparisons are possible.
"""
with pytest.raises(
TypeError,
match=re.escape("arg #2 ('foo') is not Quantity; likely an incorrect key"),
):
assert_quantity(a, "foo")

# Convert to pd.Series
b = a.to_series()

Expand Down Expand Up @@ -110,6 +126,10 @@ def test_assert_with_attrs(self, a):
a.attrs = {"bar": "foo"}
assert_qty_equal(a, b, check_attrs=False)

def test_to_dataframe(self, a):
"""Test Quantity.to_dataframe()."""
assert isinstance(a.to_dataframe(), pd.DataFrame)

def test_size(self, scen_with_big_data):
"""Stress-test reporting of large, sparse quantities."""
scen = scen_with_big_data
Expand Down Expand Up @@ -158,6 +178,12 @@ def test_squeeze(self, foo):
assert foo.sel(a="a1").squeeze().dims == ("b",)
assert foo.sel(a="a2", b="b1").squeeze().values == 2

with pytest.raises(
ValueError,
match="dimension to squeeze out which has length greater than one",
):
foo.squeeze(dim="b")

def test_sum(self, foo, bar):
# AttrSeries can be summed across all dimensions
result = foo.sum(dim=["a", "b"])
Expand Down
2 changes: 2 additions & 0 deletions genno/tests/test_computations.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ def test_broadcast_map(ureg, map_values, kwarg):
# Map a dimension name from the file to a different one in the quantity; ignore
# dimension "foo"
("input1.csv", dict(dims=dict(i="i", j_dim="j"))),
# Dimensions as a container, without mapping
("input0.csv", dict(dims=["i", "j"], units="km")),
pytest.param(
"load_file-invalid.csv",
dict(),
Expand Down
12 changes: 12 additions & 0 deletions genno/tests/test_core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from genno import configure
from genno.util import REPLACE_UNITS


def test_configure_units(caplog):
# Warning is logged on invalid definitions
configure(units=dict(define="0 = [0] * %"))
assert 'missing unary operator "*"' in caplog.messages

# Unit replacements are stored
configure(units=dict(replace={"foo": "bar"}))
assert REPLACE_UNITS.pop("foo") == "bar"

0 comments on commit 6f9cce2

Please sign in to comment.