Repository: pyg-team/pytorch_geometric
Pull request #9024 (2024-03-06 18:31:14)

feat(pyproject.toml): update & sort
- Add 'ipython' and 'matplotlib-inline' to dev dependencies
- Reorder 'torch_geometric[test]' in dev dependencies list

Introduced for developer install to pass tests on apple silicon


Notes here:

https://project-delphi.github.io/ml-blog/posts/developing-pytorch-geometric-on-m1/

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/9024?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 89.24%. Comparing base [(`af0f5f4`)](https://app.codecov.io/gh/pyg-team/pytorch_geometric/commit/af0f5f44d4dfa8accd898df3e8523d06b67940b0?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team) to head [(`b88aca6`)](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/9024?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team).


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #9024      +/-   ##
==========================================
- Coverage   89.86%   89.24%   -0.63%     
==========================================
  Files         468      468              
  Lines       30039    30034       -5     
==========================================
- Hits        26994    26803     -191     
- Misses       3045     3231     +186     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pyg-team/pytorch_geometric/pull/9024?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pyg-team).

project-delphi: @rusty1s 

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7fcdf137e..a931f86de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,6 +25,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 
 ### Fixed
 
+- Fixed `OSError` on read-only file systems within `MessagePassing` ([#9032](https://github.com/pyg-team/pytorch_geometric/pull/9032))
 - Fixed metaclass conflict in `Dataset` ([#8999](https://github.com/pyg-team/pytorch_geometric/pull/8999))
 - Fixed import errors on `MessagePassing` modules with nested inheritance ([#8973](https://github.com/pyg-team/pytorch_geometric/pull/8973))
 
diff --git a/README.md b/README.md
index a184e9a7a..7dcb95b85 100644
--- a/README.md
+++ b/README.md
@@ -20,10 +20,6 @@ In addition, it consists of easy-to-use mini-batch loaders for operating on many
 
 **[Click here to join our Slack community!][slack-url]**
 
-<p align="center">
-  <a href="https://www.eventbrite.com/e/webinar-pyg-25-distributed-training-of-gnns-tickets-850624951127"><img style="max-height: 400px" src="https://pbs.twimg.com/media/GHbKAITWwAA83mw?format=jpg&name=large" /></a>
-</p>
-
 <p align="center">
   <a href="https://medium.com/stanford-cs224w"><img style="max-width: 941px" src="https://data.pyg.org/img/cs224w_tutorials.png" /></a>
 </p>
diff --git a/docs/requirements.txt b/docs/requirements.txt
index d11fab31e..3d437463d 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,4 +1,3 @@
 https://download.pytorch.org/whl/cpu/torch-1.13.0%2Bcpu-cp38-cp38-linux_x86_64.whl
 numpy>=1.19.5
-nbsphinx
 git+https://github.com/pyg-team/pyg_sphinx_theme.git
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 9d362b90c..dcef5e18e 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -10,13 +10,6 @@ In addition, it consists of easy-to-use mini-batch loaders for operating on many
 
 .. slack_button::
 
-.. image:: https://pbs.twimg.com/media/GHbKAITWwAA83mw?format=jpg&name=large
-  :align: center
-  :width: 100%
-  :target: https://www.eventbrite.com/e/webinar-pyg-25-distributed-training-of-gnns-tickets-850624951127
-
-**Register for free** `here <https://www.eventbrite.com/e/webinar-pyg-25-distributed-training-of-gnns-tickets-850624951127>`_.
-
 .. toctree::
    :maxdepth: 1
    :caption: Install PyG
diff --git a/docs/source/install/installation.rst b/docs/source/install/installation.rst
index e649e0f47..aa7cfbee3 100644
--- a/docs/source/install/installation.rst
+++ b/docs/source/install/installation.rst
@@ -27,6 +27,12 @@ Given that you have :pytorch:`PyTorch` installed (:obj:`>=1.11.0`), simply run
 .. warning::
    Conda packages are currently not available for Windows and M1/M2/M3 macs.
 
+If :conda:`null` :obj:`conda` does not pick up the correct CUDA version of :pyg:`PyG`, you can enforce it as follows:
+
+.. code-block:: none
+
+   conda install pyg=*=*cu* -c pyg
+
 Installation via PyPi
 ---------------------
 
diff --git a/pyproject.toml b/pyproject.toml
index f26e88ec1..71f9d4593 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,67 +30,69 @@ classifiers=[
     "Programming Language :: Python :: 3 :: Only",
 ]
 dependencies=[
-    "tqdm",
-    "numpy",
-    "scipy",
+    "aiohttp",
     "fsspec",
     "jinja2",
-    "aiohttp",
-    "requests",
+    "numpy",
+    "psutil>=5.8.0",
     "pyparsing",
+    "requests",
     "scikit-learn",
-    "psutil>=5.8.0",
+    "scipy",
+    "tqdm",
 ]
 
 [project.optional-dependencies]
 graphgym=[
-    "yacs",
     "hydra-core",
     "protobuf<4.21",
     "pytorch-lightning",
+    "yacs",
 ]
 modelhub=[
     "huggingface_hub"
 ]
 benchmark=[
+    "matplotlib",
+    "networkx",
+    "pandas",
     "protobuf<4.21",
     "wandb",
-    "pandas",
-    "networkx",
-    "matplotlib",
 ]
 test=[
-    "pytest",
-    "pytest-cov",
     "onnx",
     "onnxruntime",
+    "pytest",
+    "pytest-cov",
 ]
 dev=[
-    "torch_geometric[test]",
+    "ipython",
+    "matplotlib-inline",
     "pre-commit",
+    "torch_geometric[test]",
 ]
 full = [
-    "torch_geometric[graphgym, modelhub]",
     "ase",
-    "h5py",
-    "numba",
-    "sympy",
-    "pandas",
     "captum<0.7.0",
-    "rdflib",
-    "trimesh",
-    "networkx",
     "graphviz",
-    "tabulate",
+    "h5py",
     "matplotlib",
+    "networkx",
+    "numba",
+    "opt_einsum",
+    "pandas",
+    "pgmpy",
     "pynndescent",
-    "torchmetrics",
-    "scikit-image",
     "pytorch-memlab",
-    "pgmpy",
-    "opt_einsum",
-    "statsmodels",
+    "rdflib",
     "rdkit",
+    "scikit-image",
+    "statsmodels",
+    "sympy",
+    "tabulate",
+    "torch_geometric[graphgym, modelhub]",
+    "torchmetrics",
+    "trimesh",
 ]
 
 [project.urls]
diff --git a/torch_geometric/edge_index.py b/torch_geometric/edge_index.py
index c9be09abd..90f77cb17 100644
--- a/torch_geometric/edge_index.py
+++ b/torch_geometric/edge_index.py
@@ -1823,7 +1823,7 @@ def matmul(
 
     transpose &= not input.is_undirected or input_value is not None
 
-    if not torch_geometric.typing.WITH_MKL:  # pragma: no cover
+    if torch_geometric.typing.NO_MKL:  # pragma: no cover
         sparse_input = input.to_sparse_coo(input_value)
     elif input.is_sorted_by_col:
         sparse_input = input.to_sparse_csc(input_value)
@@ -1833,7 +1833,7 @@ def matmul(
     if transpose:
         sparse_input = sparse_input.t()
 
-    if not torch_geometric.typing.WITH_MKL:  # pragma: no cover
+    if torch_geometric.typing.NO_MKL:  # pragma: no cover
         other = other.to_sparse_coo(other_value)
     elif other.is_sorted_by_col:
         other = other.to_sparse_csc(other_value)
diff --git a/torch_geometric/inspector.py b/torch_geometric/inspector.py
index fffcc211e..1ebadd80a 100644
--- a/torch_geometric/inspector.py
+++ b/torch_geometric/inspector.py
@@ -320,17 +320,6 @@ class Inspector:
 
     # Inspecting Method Bodies ################################################
 
-    @property
-    def can_read_source(self) -> bool:
-        r"""Returns :obj:`True` if able to read the source file of the
-        inspected class.
-        """
-        try:
-            inspect.getfile(self._cls)
-            return True
-        except Exception:
-            return False
-
     def get_source(self, cls: Optional[Type] = None) -> str:
         r"""Returns the source code of :obj:`cls`."""
         cls = cls or self._cls
diff --git a/torch_geometric/nn/conv/message_passing.py b/torch_geometric/nn/conv/message_passing.py
index 0b1a07e4d..68afb9e2d 100644
--- a/torch_geometric/nn/conv/message_passing.py
+++ b/torch_geometric/nn/conv/message_passing.py
@@ -166,7 +166,7 @@ class MessagePassing(torch.nn.Module):
         jinja_prefix = f'{self.__module__}_{self.__class__.__name__}'
         # Optimize `propagate()` via `*.jinja` templates:
         if not self.propagate.__module__.startswith(jinja_prefix):
-            if self.inspector.can_read_source:
+            try:
                 module = module_from_template(
                     module_name=f'{jinja_prefix}_propagate',
                     template_path=osp.join(root_dir, 'propagate.jinja'),
@@ -190,15 +190,14 @@ class MessagePassing(torch.nn.Module):
 
                 self.__class__.propagate = module.propagate
                 self.__class__.collect = module.collect
-            else:
+            except Exception:  # pragma: no cover
                 self.__class__._orig_propagate = self.__class__.propagate
                 self.__class__._jinja_propagate = self.__class__.propagate
 
         # Optimize `edge_updater()` via `*.jinja` templates (if implemented):
         if (self.inspector.implements('edge_update')
                 and not self.edge_updater.__module__.startswith(jinja_prefix)):
-            if self.inspector.can_read_source:
-
+            try:
                 module = module_from_template(
                     module_name=f'{jinja_prefix}_edge_updater',
                     template_path=osp.join(root_dir, 'edge_updater.jinja'),
@@ -216,7 +215,7 @@ class MessagePassing(torch.nn.Module):
 
                 self.__class__.edge_updater = module.edge_updater
                 self.__class__.edge_collect = module.edge_collect
-            else:
+            except Exception:  # pragma: no cover
                 self.__class__._orig_edge_updater = self.__class__.edge_updater
                 self.__class__._jinja_edge_updater = (
                     self.__class__.edge_updater)
diff --git a/torch_geometric/transforms/add_positional_encoding.py b/torch_geometric/transforms/add_positional_encoding.py
index f55c2030e..e4d43a032 100644
--- a/torch_geometric/transforms/add_positional_encoding.py
+++ b/torch_geometric/transforms/add_positional_encoding.py
@@ -153,10 +153,10 @@ class AddRandomWalkPE(BaseTransform):
             adj = torch.zeros((N, N), device=row.device)
             adj[row, col] = value
             loop_index = torch.arange(N, device=row.device)
-        elif torch_geometric.typing.WITH_MKL:
-            adj = to_torch_csr_tensor(data.edge_index, value, size=data.size())
-        else:
+        elif torch_geometric.typing.NO_MKL:  # pragma: no cover
             adj = to_torch_coo_tensor(data.edge_index, value, size=data.size())
+        else:
+            adj = to_torch_csr_tensor(data.edge_index, value, size=data.size())
 
         def get_pe(out: Tensor) -> Tensor:
             if is_torch_sparse_tensor(out):
diff --git a/torch_geometric/typing.py b/torch_geometric/typing.py
index 16a5fbd42..049c86bb8 100644
--- a/torch_geometric/typing.py
+++ b/torch_geometric/typing.py
@@ -17,7 +17,7 @@ WITH_PT112 = WITH_PT20 or int(torch.__version__.split('.')[1]) >= 12
 WITH_PT113 = WITH_PT20 or int(torch.__version__.split('.')[1]) >= 13
 
 WITH_WINDOWS = os.name == 'nt'
-WITH_MKL = 'USE_MKL=OFF' not in torch.__config__.show()
+NO_MKL = 'USE_MKL=OFF' in torch.__config__.show() or WITH_WINDOWS
 
 MAX_INT64 = torch.iinfo(torch.int64).max
 


Test patch:
diff --git a/test/distributed/test_dist_link_neighbor_loader.py b/test/distributed/test_dist_link_neighbor_loader.py
index 9fac58c76..bbdc23a36 100644
--- a/test/distributed/test_dist_link_neighbor_loader.py
+++ b/test/distributed/test_dist_link_neighbor_loader.py
@@ -15,7 +15,7 @@ from torch_geometric.distributed import (
     LocalGraphStore,
     Partitioner,
 )
-from torch_geometric.testing import onlyDistributedTest
+from torch_geometric.testing import onlyDistributedTest, withMETIS
 
 
 def create_dist_data(tmp_path: str, rank: int):
@@ -136,6 +136,7 @@ def dist_link_neighbor_loader_hetero(
     assert loader.channel.empty()
 
 
+@withMETIS
 @onlyDistributedTest
 @pytest.mark.parametrize('num_parts', [2])
 @pytest.mark.parametrize('num_workers', [0])
@@ -183,6 +184,7 @@ def test_dist_link_neighbor_loader_homo(
     w1.join()
 
 
+@withMETIS
 @onlyDistributedTest
 @pytest.mark.parametrize('num_parts', [2])
 @pytest.mark.parametrize('num_workers', [0])
diff --git a/test/distributed/test_dist_link_neighbor_sampler.py b/test/distributed/test_dist_link_neighbor_sampler.py
index 380ba545c..a55634ac4 100644
--- a/test/distributed/test_dist_link_neighbor_sampler.py
+++ b/test/distributed/test_dist_link_neighbor_sampler.py
@@ -19,7 +19,7 @@ from torch_geometric.distributed.event_loop import ConcurrentEventLoop
 from torch_geometric.distributed.rpc import init_rpc, shutdown_rpc
 from torch_geometric.sampler import EdgeSamplerInput, NeighborSampler
 from torch_geometric.sampler.neighbor_sampler import edge_sample
-from torch_geometric.testing import onlyDistributedTest
+from torch_geometric.testing import onlyDistributedTest, withMETIS
 from torch_geometric.typing import EdgeType
 
 
@@ -523,6 +523,7 @@ def test_dist_link_neighbor_sampler_edge_level_temporal(
     w1.join()
 
 
+@withMETIS
 @onlyDistributedTest
 @pytest.mark.parametrize('disjoint', [False, True])
 def test_dist_link_neighbor_sampler_hetero(tmp_path, disjoint):
@@ -565,6 +566,7 @@ def test_dist_link_neighbor_sampler_hetero(tmp_path, disjoint):
     w1.join()
 
 
+@withMETIS
 @onlyDistributedTest
 @pytest.mark.parametrize('seed_time', [None, [0, 0], [3, 3]])
 @pytest.mark.parametrize('temporal_strategy', ['uniform', 'last'])
@@ -619,6 +621,7 @@ def test_dist_link_neighbor_sampler_temporal_hetero(
     w1.join()
 
 
+@withMETIS
 @onlyDistributedTest
 @pytest.mark.parametrize('seed_time', [[0, 0], [3, 3]])
 @pytest.mark.parametrize('temporal_strategy', ['uniform', 'last'])
diff --git a/test/distributed/test_dist_neighbor_loader.py b/test/distributed/test_dist_neighbor_loader.py
index a73f158f7..a678cd293 100644
--- a/test/distributed/test_dist_neighbor_loader.py
+++ b/test/distributed/test_dist_neighbor_loader.py
@@ -14,7 +14,7 @@ from torch_geometric.distributed import (
     LocalGraphStore,
     Partitioner,
 )
-from torch_geometric.testing import onlyDistributedTest
+from torch_geometric.testing import onlyDistributedTest, withMETIS
 
 
 def create_dist_data(tmp_path: str, rank: int):
@@ -142,6 +142,7 @@ def dist_neighbor_loader_hetero(
     assert loader.channel.empty()
 
 
+@withMETIS
 @onlyDistributedTest
 @pytest.mark.parametrize('num_parts', [2])
 @pytest.mark.parametrize('num_workers', [0])
@@ -185,6 +186,7 @@ def test_dist_neighbor_loader_homo(
     w1.join()
 
 
+@withMETIS
 @onlyDistributedTest
 @pytest.mark.parametrize('num_parts', [2])
 @pytest.mark.parametrize('num_workers', [0])
diff --git a/test/distributed/test_dist_neighbor_sampler.py b/test/distributed/test_dist_neighbor_sampler.py
index 060fb5460..fbea1f23e 100644
--- a/test/distributed/test_dist_neighbor_sampler.py
+++ b/test/distributed/test_dist_neighbor_sampler.py
@@ -18,7 +18,7 @@ from torch_geometric.distributed.event_loop import ConcurrentEventLoop
 from torch_geometric.distributed.rpc import init_rpc, shutdown_rpc
 from torch_geometric.sampler import NeighborSampler, NodeSamplerInput
 from torch_geometric.sampler.neighbor_sampler import node_sample
-from torch_geometric.testing import onlyDistributedTest
+from torch_geometric.testing import onlyDistributedTest, withMETIS
 
 
 def create_data(rank: int, world_size: int, time_attr: Optional[str] = None):
@@ -474,6 +474,7 @@ def test_dist_neighbor_sampler_edge_level_temporal(
     w1.join()
 
 
+@withMETIS
 @onlyDistributedTest
 @pytest.mark.parametrize('disjoint', [False, True])
 def test_dist_neighbor_sampler_hetero(tmp_path, disjoint):
@@ -513,6 +514,7 @@ def test_dist_neighbor_sampler_hetero(tmp_path, disjoint):
     w1.join()
 
 
+@withMETIS
 @onlyDistributedTest
 @pytest.mark.parametrize('seed_time', [None, [0, 0], [2, 2]])
 @pytest.mark.parametrize('temporal_strategy', ['uniform', 'last'])
@@ -567,6 +569,7 @@ def test_dist_neighbor_sampler_temporal_hetero(
     w1.join()
 
 
+@withMETIS
 @onlyDistributedTest
 @pytest.mark.parametrize('seed_time', [[0, 0], [1, 2]])
 @pytest.mark.parametrize('temporal_strategy', ['uniform', 'last'])
diff --git a/test/distributed/test_partition.py b/test/distributed/test_partition.py
index 46e494124..cf4a6a711 100644
--- a/test/distributed/test_partition.py
+++ b/test/distributed/test_partition.py
@@ -8,10 +8,11 @@ from torch_geometric.distributed import (
     LocalGraphStore,
     Partitioner,
 )
-from torch_geometric.testing import onlyDistributedTest
+from torch_geometric.testing import onlyDistributedTest, withMETIS
 from torch_geometric.typing import EdgeTypeStr
 
 
+@withMETIS
 @onlyDistributedTest
 def test_partition_data(tmp_path):
     data = FakeDataset()[0]
@@ -58,6 +59,7 @@ def test_partition_data(tmp_path):
                        node_feats1['feats']['x'])
 
 
+@withMETIS
 @onlyDistributedTest
 def test_partition_hetero_data(tmp_path):
     data = FakeHeteroDataset()[0]
@@ -92,6 +94,7 @@ def test_partition_hetero_data(tmp_path):
         assert osp.exists(edge_feats_path)
 
 
+@withMETIS
 @onlyDistributedTest
 def test_partition_data_temporal(tmp_path):
     data = FakeDataset()[0]
@@ -112,6 +115,7 @@ def test_partition_data_temporal(tmp_path):
     assert torch.equal(data.time, node_feats1['time'])
 
 
+@withMETIS
 @onlyDistributedTest
 def test_partition_data_edge_level_temporal(tmp_path):
     data = FakeDataset(edge_dim=2)[0]
@@ -134,6 +138,7 @@ def test_partition_data_edge_level_temporal(tmp_path):
                        edge_feats1['edge_time'])
 
 
+@withMETIS
 @onlyDistributedTest
 def test_partition_hetero_data_temporal(tmp_path):
     data = FakeHeteroDataset()[0]
@@ -157,6 +162,7 @@ def test_partition_hetero_data_temporal(tmp_path):
         assert torch.equal(data[key].time, node_feats1[key]['time'])
 
 
+@withMETIS
 @onlyDistributedTest
 def test_partition_hetero_data_edge_level_temporal(tmp_path):
     data = FakeHeteroDataset(edge_dim=2)[0]
@@ -186,6 +192,7 @@ def test_partition_hetero_data_edge_level_temporal(tmp_path):
         )
 
 
+@withMETIS
 @onlyDistributedTest
 def test_from_partition_data(tmp_path):
     data = FakeDataset()[0]
@@ -220,6 +227,7 @@ def test_from_partition_data(tmp_path):
     assert torch.allclose(data.x[id2], x2)
 
 
+@withMETIS
 @onlyDistributedTest
 def test_from_partition_hetero_data(tmp_path):
     data = FakeHeteroDataset()[0]
@@ -247,6 +255,7 @@ def test_from_partition_hetero_data(tmp_path):
     assert node_types == set(data.node_types)
 
 
+@withMETIS
 @onlyDistributedTest
 def test_from_partition_temporal_data(tmp_path):
     data = FakeDataset()[0]
@@ -272,6 +281,7 @@ def test_from_partition_temporal_data(tmp_path):
     assert torch.equal(time2, data.time)
 
 
+@withMETIS
 @onlyDistributedTest
 def test_from_partition_edge_level_temporal_data(tmp_path):
     data = FakeDataset(edge_dim=2)[0]
@@ -299,6 +309,7 @@ def test_from_partition_edge_level_temporal_data(tmp_path):
     assert torch.equal(data.edge_time[edge_id2], time2)
 
 
+@withMETIS
 @onlyDistributedTest
 def test_from_partition_hetero_temporal_data(tmp_path):
     data = FakeHeteroDataset()[0]
@@ -331,6 +342,7 @@ def test_from_partition_hetero_temporal_data(tmp_path):
         assert torch.equal(times2[key], data[key].time)
 
 
+@withMETIS
 @onlyDistributedTest
 def test_from_partition_hetero_edge_level_temporal_data(tmp_path):
     data = FakeHeteroDataset(edge_dim=2)[0]
diff --git a/test/loader/test_cluster.py b/test/loader/test_cluster.py
index f5c50c8d6..9612dbc5d 100644
--- a/test/loader/test_cluster.py
+++ b/test/loader/test_cluster.py
@@ -3,21 +3,11 @@ import torch
 
 from torch_geometric.data import Data
 from torch_geometric.loader import ClusterData, ClusterLoader
-from torch_geometric.testing import onlyFullTest, onlyOnline
+from torch_geometric.testing import onlyFullTest, onlyOnline, withMETIS
 from torch_geometric.utils import sort_edge_index
 
-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
 
-
-@pytest.mark.skipif(not WITH_METIS, reason='Not compiled with METIS support')
+@withMETIS
 def test_cluster_gcn():
     adj = torch.tensor([
         [1, 1, 1, 0, 1, 0],
@@ -117,7 +107,7 @@ def test_cluster_gcn():
     assert torch.equal(out.edge_attr, tmp[1])
 
 
-@pytest.mark.skipif(not WITH_METIS, reason='Not compiled with METIS support')
+@withMETIS
 def test_keep_inter_cluster_edges():
     adj = torch.tensor([
         [1, 1, 1, 0, 1, 0],
@@ -159,10 +149,10 @@ def test_keep_inter_cluster_edges():
     assert data.edge_index.size(1) == data.edge_attr.size(0)
 
 
+@withMETIS
 @onlyOnline
 @onlyFullTest
 @pytest.mark.parametrize('sparse_format', ['csr', 'csc'])
-@pytest.mark.skipif(not WITH_METIS, reason='Not compiled with METIS support')
 def test_cluster_gcn_correctness(get_dataset, sparse_format):
     dataset = get_dataset('Cora')
     data = dataset[0].clone()
diff --git a/test/test_edge_index.py b/test/test_edge_index.py
index b6680b6d5..8c732d2c1 100644
--- a/test/test_edge_index.py
+++ b/test/test_edge_index.py
@@ -952,7 +952,7 @@ def test_spspmm(device, reduce, transpose, is_undirected):
         assert isinstance(out, EdgeIndex)
         assert out.is_sorted_by_row
         assert out._sparse_size == (3, 3)
-        if torch_geometric.typing.WITH_MKL:
+        if not torch_geometric.typing.NO_MKL:
             assert out._indptr is not None
         assert torch.allclose(out.to_dense(value), adj1_dense @ adj2_dense)
     else:
diff --git a/torch_geometric/testing/__init__.py b/torch_geometric/testing/__init__.py
index 7b6349d60..dca3d9978 100644
--- a/torch_geometric/testing/__init__.py
+++ b/torch_geometric/testing/__init__.py
@@ -19,6 +19,7 @@ from .decorators import (
     has_package,
     withPackage,
     withCUDA,
+    withMETIS,
     disableExtensions,
     withoutExtensions,
 )
@@ -47,6 +48,7 @@ __all__ = [
     'has_package',
     'withPackage',
     'withCUDA',
+    'withMETIS',
     'disableExtensions',
     'withoutExtensions',
     'assert_module',
diff --git a/torch_geometric/testing/decorators.py b/torch_geometric/testing/decorators.py
index f378b6bb9..f1018db86 100644
--- a/torch_geometric/testing/decorators.py
+++ b/torch_geometric/testing/decorators.py
@@ -8,7 +8,7 @@ from typing import Callable
 import torch
 from packaging.requirements import Requirement
 
-from torch_geometric.typing import WITH_PYG_LIB, WITH_TORCH_SPARSE
+from torch_geometric.typing import WITH_METIS, WITH_PYG_LIB, WITH_TORCH_SPARSE
 from torch_geometric.visualization.graph import has_graphviz
 
 
@@ -210,6 +210,27 @@ def withCUDA(func: Callable) -> Callable:
     return pytest.mark.parametrize('device', devices)(func)
 
 
+def withMETIS(func: Callable) -> Callable:
+    r"""A decorator to only test in case a valid METIS method is available."""
+    import pytest
+
+    with_metis = WITH_METIS
+
+    if with_metis:
+        try:  # Test that METIS can succesfully execute:
+            import pyg_lib
+            rowptr = torch.tensor([0, 2, 4, 6])
+            col = torch.tensor([1, 2, 0, 2, 1, 0])
+            pyg_lib.partition.metis(rowptr, col, num_partitions=2)
+        except Exception:
+            with_metis = False
+
+    return pytest.mark.skipif(
+        not with_metis,
+        reason="METIS not enabled",
+    )(func)
+
+
 def disableExtensions(func: Callable) -> Callable:
     r"""A decorator to temporarily disable the usage of the
     :obj:`torch_scatter`, :obj:`torch_sparse` and :obj:`pyg_lib` extension