Repository: pyg-team/pytorch_geometric
Pull request #7629 (2023-06-22 06:31:49)

Make `Data.keys` a method rather than a property
Ensures consistency with `dict` and fixes https://github.com/pyg-team/pytorch_geometric/discussions/7626

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/7629?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) Report
> Merging [#7629](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/7629?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) (ac2864c) into [master](https://app.codecov.io/gh/pyg-team/pytorch_geometric/commit/ba54c72512859bdd4996f5e64ea5566d5d74db0a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) (ba54c72) will **decrease** coverage by `0.32%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##           master    #7629      +/-   ##
==========================================
- Coverage   91.90%   91.59%   -0.32%     
==========================================
  Files         452      452              
  Lines       25389    25385       -4     
==========================================
- Hits        23335    23251      -84     
- Misses       2054     2134      +80     
```


| [Impacted Files](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/7629?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) | Coverage Δ | |
|---|---|---|
| [torch\_geometric/data/data.py](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/7629?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team#diff-dG9yY2hfZ2VvbWV0cmljL2RhdGEvZGF0YS5weQ==) | `89.74% <100.00%> (-0.03%)` | :arrow_down: |
| [torch\_geometric/explain/explanation.py](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/7629?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team#diff-dG9yY2hfZ2VvbWV0cmljL2V4cGxhaW4vZXhwbGFuYXRpb24ucHk=) | `95.23% <100.00%> (ø)` | |
| [torch\_geometric/graphgym/loader.py](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/7629?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team#diff-dG9yY2hfZ2VvbWV0cmljL2dyYXBoZ3ltL2xvYWRlci5weQ==) | `40.64% <100.00%> (ø)` | |
| [torch\_geometric/loader/dense\_data\_loader.py](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/7629?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team#diff-dG9yY2hfZ2VvbWV0cmljL2xvYWRlci9kZW5zZV9kYXRhX2xvYWRlci5weQ==) | `100.00% <100.00%> (ø)` | |

... and [17 files with indirect coverage changes](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/7629/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team)

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team)


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aea479654..43d93d370 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -68,6 +68,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 
 ### Changed
 
+- Breaking Change: Made `Data.keys` a method rather than a property ([#7629](https://github.com/pyg-team/pytorch_geometric/pull/7629))
 - Added a `num_edges` parameter to the forward method of `HypergraphConv` ([#7560](https://github.com/pyg-team/pytorch_geometric/pull/7560))
 - Fixed `get_mesh_laplacian` for `normalization="sym"` ([#7544](https://github.com/pyg-team/pytorch_geometric/pull/7544))
 - Use `dim_size` to initialize output size of the `EquilibriumAggregation` layer ([#7530](https://github.com/pyg-team/pytorch_geometric/pull/7530))
diff --git a/docs/source/get_started/introduction.rst b/docs/source/get_started/introduction.rst
index 5616c32f5..c26629d77 100644
--- a/docs/source/get_started/introduction.rst
+++ b/docs/source/get_started/introduction.rst
@@ -86,7 +86,7 @@ Besides holding a number of node-level, edge-level or graph-level attributes, :c
 
 .. code-block:: python
 
-    print(data.keys)
+    print(data.keys())
     >>> ['x', 'edge_index']
 
     print(data['x'])
diff --git a/torch_geometric/data/data.py b/torch_geometric/data/data.py
index 06e2b371f..0ed967dd1 100644
--- a/torch_geometric/data/data.py
+++ b/torch_geometric/data/data.py
@@ -127,7 +127,6 @@ class BaseData:
 
     ###########################################################################
 
-    @property
     def keys(self) -> List[str]:
         r"""Returns a list of all graph attribute names."""
         out = []
@@ -137,12 +136,12 @@ class BaseData:
 
     def __len__(self) -> int:
         r"""Returns the number of graph attributes."""
-        return len(self.keys)
+        return len(self.keys())
 
     def __contains__(self, key: str) -> bool:
         r"""Returns :obj:`True` if the attribute :obj:`key` is present in the
         data."""
-        return key in self.keys
+        return key in self.keys()
 
     def __getstate__(self) -> Dict[str, Any]:
         return self.__dict__
@@ -738,7 +737,7 @@ class Data(BaseData, FeatureStore, GraphStore):
                     data[key][attr] = value.index_select(cat_dim, edge_ids[i])
 
         # Add global attributes.
-        exclude_keys = set(data.keys) | {
+        exclude_keys = set(data.keys()) | {
             'node_type', 'edge_type', 'edge_index', 'num_nodes', 'ptr'
         }
         for attr, value in self.items():
diff --git a/torch_geometric/explain/explanation.py b/torch_geometric/explain/explanation.py
index 5c2ec4118..57e4b98ed 100644
--- a/torch_geometric/explain/explanation.py
+++ b/torch_geometric/explain/explanation.py
@@ -15,7 +15,7 @@ class ExplanationMixin:
     @property
     def available_explanations(self) -> List[str]:
         """Returns the available explanation masks."""
-        return [key for key in self.keys if key.endswith('_mask')]
+        return [key for key in self.keys() if key.endswith('_mask')]
 
     def validate_masks(self, raise_on_error: bool = True) -> bool:
         r"""Validates the correctness of the :class:`Explanation` masks."""
diff --git a/torch_geometric/graphgym/loader.py b/torch_geometric/graphgym/loader.py
index b3cf6f3dd..0dfeb5505 100644
--- a/torch_geometric/graphgym/loader.py
+++ b/torch_geometric/graphgym/loader.py
@@ -222,11 +222,11 @@ def set_dataset_info(dataset):
 
     # count number of dataset splits
     cfg.share.num_splits = 1
-    for key in dataset._data.keys:
+    for key in dataset._data.keys():
         if 'val' in key:
             cfg.share.num_splits += 1
             break
-    for key in dataset._data.keys:
+    for key in dataset._data.keys():
         if 'test' in key:
             cfg.share.num_splits += 1
             break
diff --git a/torch_geometric/loader/dense_data_loader.py b/torch_geometric/loader/dense_data_loader.py
index 2488c0399..0ffc8a334 100644
--- a/torch_geometric/loader/dense_data_loader.py
+++ b/torch_geometric/loader/dense_data_loader.py
@@ -8,7 +8,7 @@ from torch_geometric.data import Batch, Data, Dataset
 
 def collate_fn(data_list: List[Data]) -> Batch:
     batch = Batch()
-    for key in data_list[0].keys:
+    for key in data_list[0].keys():
         batch[key] = default_collate([data[key] for data in data_list])
     return batch
 


Test patch:
diff --git a/test/data/test_data.py b/test/data/test_data.py
index b38578c7f..144c94fa1 100644
--- a/test/data/test_data.py
+++ b/test/data/test_data.py
@@ -30,7 +30,7 @@ def test_data():
     assert data.get('y', 2) == 2
     assert data.get('y', None) is None
 
-    assert sorted(data.keys) == ['edge_index', 'x']
+    assert sorted(data.keys()) == ['edge_index', 'x']
     assert len(data) == 2
     assert 'x' in data and 'edge_index' in data and 'pos' not in data
 
@@ -81,7 +81,7 @@ def test_data():
 
     dictionary = {'x': data.x, 'edge_index': data.edge_index}
     data = Data.from_dict(dictionary)
-    assert sorted(data.keys) == ['edge_index', 'x']
+    assert sorted(data.keys()) == ['edge_index', 'x']
 
     assert not data.has_isolated_nodes()
     assert not data.has_self_loops()
diff --git a/test/data/test_hetero_data.py b/test/data/test_hetero_data.py
index 5698ce7a0..75a37ee77 100644
--- a/test/data/test_hetero_data.py
+++ b/test/data/test_hetero_data.py
@@ -113,7 +113,7 @@ def test_hetero_data_functions():
     data['author', 'paper'].edge_index = edge_index_author_paper
     data['paper', 'paper'].edge_attr = edge_attr_paper_paper
     assert len(data) == 3
-    assert sorted(data.keys) == ['edge_attr', 'edge_index', 'x']
+    assert sorted(data.keys()) == ['edge_attr', 'edge_index', 'x']
     assert 'x' in data and 'edge_index' in data and 'edge_attr' in data
     assert data.num_nodes == 15
     assert data.num_edges == 110
@@ -145,7 +145,7 @@ def test_hetero_data_functions():
     data.y = 0
     assert data['y'] == 0 and data.y == 0
     assert len(data) == 4
-    assert sorted(data.keys) == ['edge_attr', 'edge_index', 'x', 'y']
+    assert sorted(data.keys()) == ['edge_attr', 'edge_index', 'x', 'y']
 
     del data['paper', 'author']
     node_types, edge_types = data.metadata()
diff --git a/test/data/test_temporal.py b/test/data/test_temporal.py
index 7c6acfc06..3d066ac28 100644
--- a/test/data/test_temporal.py
+++ b/test/data/test_temporal.py
@@ -32,8 +32,8 @@ def test_temporal_data():
     del data.edge_index
     assert data.edge_index.tolist() == [[0, 1, 2], [3, 4, 5]]
 
-    assert sorted(data.keys) == ['dst', 'msg', 'src', 't', 'y']
-    assert sorted(data.to_dict().keys()) == sorted(data.keys)
+    assert sorted(data.keys()) == ['dst', 'msg', 'src', 't', 'y']
+    assert sorted(data.to_dict().keys()) == sorted(data.keys())
 
     data_tuple = data.to_namedtuple()
     assert len(data_tuple) == 5
diff --git a/test/distributed/test_partition.py b/test/distributed/test_partition.py
index 272c2184f..ee98c93a2 100644
--- a/test/distributed/test_partition.py
+++ b/test/distributed/test_partition.py
@@ -1,14 +1,24 @@
 import os.path as osp
 
+import pytest
 import torch
 
 from torch_geometric.datasets import FakeDataset, FakeHeteroDataset
 from torch_geometric.distributed import Partitioner
-from torch_geometric.testing import withPackage
 from torch_geometric.typing import EdgeTypeStr
 
+try:
+    # TODO Using `pyg-lib` metis partitioning leads to some weird bugs in the
+    # CI. As such, we require `torch-sparse` for these tests for now.
+    rowptr = torch.tensor([0, 1])
+    col = torch.tensor([0])
+    torch.ops.torch_sparse.partition(rowptr, col, None, 1, True)
+    WITH_METIS = True
+except (AttributeError, RuntimeError):
+    WITH_METIS = False
 
-@withPackage('pyg_lib')
+
+@pytest.mark.skipif(not WITH_METIS, reason='Not compiled with METIS support')
 def test_partition_data(tmp_path):
     data = FakeDataset()[0]
     num_parts = 2
@@ -55,7 +65,7 @@ def test_partition_data(tmp_path):
                        node_feats1['feats']['x'])
 
 
-@withPackage('pyg_lib')
+@pytest.mark.skipif(not WITH_METIS, reason='Not compiled with METIS support')
 def test_partition_hetero_data(tmp_path):
     data = FakeHeteroDataset()[0]
     num_parts = 2
diff --git a/test/transforms/test_pad.py b/test/transforms/test_pad.py
index 69225a942..40c8223d9 100644
--- a/test/transforms/test_pad.py
+++ b/test/transforms/test_pad.py
@@ -29,13 +29,13 @@ def fake_hetero_data(node_types=2, edge_types=5) -> HeteroData:
 
 
 def _generate_homodata_node_attrs(data: Data) -> Generator[str, None, None]:
-    for attr in data.keys:
+    for attr in data.keys():
         if data.is_node_attr(attr):
             yield attr
 
 
 def _generate_homodata_edge_attrs(data: Data) -> Generator[str, None, None]:
-    for attr in data.keys:
+    for attr in data.keys():
         if data.is_edge_attr(attr):
             yield attr
 
@@ -75,10 +75,10 @@ def _check_homo_data_nodes(
 
     for attr in _generate_homodata_node_attrs(original):
         if attr in exclude_keys:
-            assert attr not in padded.keys
+            assert attr not in padded.keys()
             continue
 
-        assert attr in padded.keys
+        assert attr in padded.keys()
 
         if not isinstance(padded[attr], torch.Tensor):
             continue
@@ -133,10 +133,10 @@ def _check_homo_data_edges(
         if attr == 'edge_index':
             continue
         if attr in exclude_keys:
-            assert attr not in padded.keys
+            assert attr not in padded.keys()
             continue
 
-        assert attr in padded.keys
+        assert attr in padded.keys()
 
         if not isinstance(padded[attr], torch.Tensor):
             continue
@@ -474,7 +474,7 @@ def test_pad_data_non_tensor_attr():
 
     exclude_transform = Pad(max_num_nodes=101, exclude_keys=('batch_size', ))
     padded = exclude_transform(data)
-    assert 'batch_size' not in padded.keys
+    assert 'batch_size' not in padded.keys()
 
 
 @pytest.mark.parametrize('mask_pad_value', [True, False])