|
6 | 6 | """ |
7 | 7 |
|
8 | 8 | from collections.abc import Callable |
| 9 | +from typing import Any |
9 | 10 |
|
10 | 11 | import numpy as np |
11 | 12 | import pandas as pd |
@@ -560,6 +561,124 @@ def test_align_to_coords_preserves_type_errors() -> None: |
560 | 561 | align_to_coords(lambda x: x, {"x": [0, 1, 2]}, label="lower bound") |
561 | 562 |
|
562 | 563 |
|
| 564 | +def test_align_to_coords_does_not_relabel_coords_errors() -> None: |
| 565 | + """Coords-side TypeError carries its own message, not the value label.""" |
| 566 | + mi = pd.MultiIndex.from_product([[0, 1], ["a", "b"]], names=["i", "j"]) |
| 567 | + with pytest.raises(TypeError, match=r"MultiIndex.*must have \.name set"): |
| 568 | + align_to_coords(np.array([1, 2, 3, 4]), [mi], label="lower bound") |
| 569 | + |
| 570 | + |
| 571 | +class TestCoordsToDictRules: |
| 572 | + """ |
| 573 | + One test per row of the ``_coords_to_dict`` rules table. |
| 574 | +
|
| 575 | + Each test name states the rule it pins; the assertions show the |
| 576 | + expected outcome. Together they form the executable spec of how |
| 577 | + sequence-form ``coords`` entries are named. |
| 578 | + """ |
| 579 | + |
| 580 | + @staticmethod |
| 581 | + def _parse(coords: Any, dims: Any = None) -> dict: |
| 582 | + from linopy.common import _coords_to_dict |
| 583 | + |
| 584 | + return _coords_to_dict(coords, dims=dims) |
| 585 | + |
| 586 | + # -- container forms --------------------------------------------------- |
| 587 | + |
| 588 | + def test_mapping_is_returned_as_shallow_dict_copy(self) -> None: |
| 589 | + src = {"x": [0, 1, 2], "y": [10, 20]} |
| 590 | + result = self._parse(src) |
| 591 | + assert result == src |
| 592 | + assert result is not src |
| 593 | + |
| 594 | + def test_xarray_coordinates_keeps_only_dim_entries(self) -> None: |
| 595 | + midx = pd.MultiIndex.from_product([[0, 1], ["a", "b"]], names=["i", "j"]) |
| 596 | + coords = xr.Coordinates.from_pandas_multiindex(midx, "stacked") |
| 597 | + result = self._parse(coords) |
| 598 | + assert set(result) == {"stacked"} |
| 599 | + |
| 600 | + # -- pd.Index entries -------------------------------------------------- |
| 601 | + |
| 602 | + def test_named_pd_index_uses_its_name(self) -> None: |
| 603 | + result = self._parse([pd.Index([0, 1, 2], name="x")]) |
| 604 | + assert set(result) == {"x"} |
| 605 | + |
| 606 | + def test_unnamed_pd_index_with_dims_uses_dims(self) -> None: |
| 607 | + result = self._parse([pd.Index([0, 1, 2])], dims=["x"]) |
| 608 | + assert set(result) == {"x"} |
| 609 | + |
| 610 | + def test_unnamed_pd_index_without_dims_is_size_only(self) -> None: |
| 611 | + # Same as a bare sequence: contributes no dim name; xarray assigns |
| 612 | + # ``dim_0`` downstream. |
| 613 | + assert self._parse([pd.Index([0, 1, 2])]) == {} |
| 614 | + m = Model() |
| 615 | + v = m.add_variables(coords=[pd.Index([0, 1, 2])]) |
| 616 | + assert v.dims == ("dim_0",) |
| 617 | + |
| 618 | + # -- pd.MultiIndex entries -------------------------------------------- |
| 619 | + |
| 620 | + def test_named_multiindex_uses_its_name(self) -> None: |
| 621 | + mi = pd.MultiIndex.from_product([[0, 1], ["a", "b"]], names=["i", "j"]) |
| 622 | + mi.name = "multi" |
| 623 | + result = self._parse([mi]) |
| 624 | + assert set(result) == {"multi"} |
| 625 | + |
| 626 | + def test_unnamed_multiindex_with_dims_uses_dims(self) -> None: |
| 627 | + mi = pd.MultiIndex.from_product([[0, 1], ["a", "b"]], names=["i", "j"]) |
| 628 | + result = self._parse([mi], dims=["multi"]) |
| 629 | + assert set(result) == {"multi"} |
| 630 | + assert result["multi"].name == "multi" |
| 631 | + assert mi.name is None # caller's MultiIndex not mutated |
| 632 | + |
| 633 | + def test_unnamed_multiindex_without_dims_raises(self) -> None: |
| 634 | + mi = pd.MultiIndex.from_product([[0, 1], ["a", "b"]], names=["i", "j"]) |
| 635 | + with pytest.raises(TypeError, match=r"MultiIndex.*must have \.name set"): |
| 636 | + self._parse([mi]) |
| 637 | + |
| 638 | + # -- bare sequence entries -------------------------------------------- |
| 639 | + |
| 640 | + @pytest.mark.parametrize( |
| 641 | + "entry", |
| 642 | + [[0, 1, 2], (0, 1, 2), range(3), np.array([0, 1, 2])], |
| 643 | + ids=["list", "tuple", "range", "ndarray"], |
| 644 | + ) |
| 645 | + def test_bare_sequence_with_dims_uses_dims(self, entry: Any) -> None: |
| 646 | + result = self._parse([entry], dims=["x"]) |
| 647 | + assert set(result) == {"x"} |
| 648 | + |
| 649 | + @pytest.mark.parametrize( |
| 650 | + "entry", |
| 651 | + [[0, 1, 2], (0, 1, 2), range(3), np.array([0, 1, 2])], |
| 652 | + ids=["list", "tuple", "range", "ndarray"], |
| 653 | + ) |
| 654 | + def test_bare_sequence_without_dims_is_silently_skipped(self, entry: Any) -> None: |
| 655 | + assert self._parse([entry]) == {} |
| 656 | + |
| 657 | + @pytest.mark.parametrize( |
| 658 | + "entry", |
| 659 | + [[0, 1, 2], (0, 1, 2), range(3), np.array([0, 1, 2])], |
| 660 | + ids=["list", "tuple", "range", "ndarray"], |
| 661 | + ) |
| 662 | + def test_bare_sequence_without_dims_falls_through_to_xarray_dim_0( |
| 663 | + self, entry: Any |
| 664 | + ) -> None: |
| 665 | + m = Model() |
| 666 | + v = m.add_variables(coords=[entry]) |
| 667 | + assert v.dims == ("dim_0",) |
| 668 | + |
| 669 | + # -- unsupported entries ---------------------------------------------- |
| 670 | + |
| 671 | + def test_dataarray_entry_raises(self) -> None: |
| 672 | + with pytest.raises(TypeError, match=r"coords entries must be pd\.Index"): |
| 673 | + self._parse([DataArray([0, 1, 2], dims=["x"])]) |
| 674 | + |
| 675 | + def test_unknown_type_entry_raises(self) -> None: |
| 676 | + class Foo: ... |
| 677 | + |
| 678 | + with pytest.raises(TypeError, match=r"coords entries must be pd\.Index"): |
| 679 | + self._parse([Foo()]) |
| 680 | + |
| 681 | + |
563 | 682 | def test_best_int() -> None: |
564 | 683 | # Test for int8 |
565 | 684 | assert best_int(127) == np.int8 |
|
0 commit comments