Repository: pytorch/ignite

Issue:
Fix DeprecationWarning: distutils Version classes are deprecated
We need to fix `DeprecationWarning: distutils`:

```
  /home/docker/actions-runner/_work/ignite/ignite/tests/ignite/engine/test_create_supervised.py:587: DeprecationWarning: distutils Version classes are deprecated. Use packaging.version instead.
    @pytest.mark.skipif(LooseVersion(torch.__version__) < LooseVersion("1.6.0"), reason="Skip if < 1.6.0")
```
- In the CI and in the code

In addition to the code change we have to figure out if we need to add `packaging` as a dependency.
It is most probably brought with some other packages but we have to ensure that on a clean environment it is (or not) installed together with for example pytorch.

---

`packaging` is not installed with torch cpu on Ubuntu, checked ubuntu:latest docker container.
```
apt-get install -y python3 python3-pip
python3 --version
Python 3.8.10

pip3 install torch -f https://download.pytorch.org/whl/cpu/torch_stable.html
pip3 list | grep torch
torch             1.10.2+cp

python3 -c "import packaging"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'packaging'
```



Discussion:
Hello! I would like to fix this.
I am looking if `packaging` is a dependency in any other required package.
@DevPranjal thanks !
The only dependency ignite has is pytorch: https://github.com/pytorch/ignite/blob/750719f01585b068bb86cdbcc6c618439f555016/setup.py#L27-L29

So, if pytorch requires `packaging` it would be good.
EDIT: -> no, it does not require `packaging`
@vfdev-5 Just a few facts I found:
- [This](https://github.com/pytorch/pytorch/pull/69823) approved PR in `pytorch` would include `packaging` to the requirements.
- Any system with `pip` installed would have the `packaging` module (from this [comment](https://github.com/pytorch/pytorch/pull/69823#pullrequestreview-831808434))

So, shall I add `packaging` to the requirements? We can remove it when Pytorch code has been refactored
Yes, let's add it now and remove once torch included it.
Thanks for investigations @DevPranjal !

Resolving patch:
diff --git a/ignite/distributed/comp_models/native.py b/ignite/distributed/comp_models/native.py
--- a/ignite/distributed/comp_models/native.py
+++ b/ignite/distributed/comp_models/native.py
@@ -2,12 +2,12 @@
 import re
 import subprocess
 import warnings
-from distutils.version import LooseVersion
 from typing import Any, Callable, cast, Dict, List, Mapping, Optional, Tuple, Union
 
 import torch
 import torch.distributed as dist
 import torch.multiprocessing as mp
+from packaging.version import Version
 
 from ignite.distributed.comp_models.base import ComputationModel
 
@@ -133,7 +133,7 @@ def _create_from_backend(
             # [W ProcessGroupNCCL.cpp:1569] Rank 0 using best-guess GPU 0 to perform barrier as devices used by
             # this process are currently unknown. This can potentially cause a hang if this rank to GPU mapping
             # is incorrect.Specify device_ids in barrier() to force use of a particular device.
-            if backend == dist.Backend.NCCL and LooseVersion(torch.__version__) >= LooseVersion("1.8.0"):
+            if backend == dist.Backend.NCCL and Version(torch.__version__) >= Version("1.8.0"):
                 device_ids = [torch.cuda.current_device()]
                 dist.barrier(device_ids=device_ids)
             else:
@@ -369,7 +369,7 @@ def spawn(  # type: ignore[override]
 
             start_processes = mp.spawn
             # start_method and start_processes in pytorch >= 1.5
-            if LooseVersion(torch.__version__) >= LooseVersion("1.5.0"):
+            if Version(torch.__version__) >= Version("1.5.0"):
                 import builtins
 
                 if "__IPYTHON__" in builtins.__dict__:
diff --git a/ignite/metrics/gan/fid.py b/ignite/metrics/gan/fid.py
--- a/ignite/metrics/gan/fid.py
+++ b/ignite/metrics/gan/fid.py
@@ -1,8 +1,8 @@
 import warnings
-from distutils.version import LooseVersion
 from typing import Callable, Optional, Sequence, Union
 
 import torch
+from packaging.version import Version
 
 from ignite.metrics.gan.utils import _BaseInceptionMetric, InceptionModel
 from ignite.metrics.metric import reinit__is_reduced, sync_all_reduce
@@ -153,7 +153,7 @@ def _online_update(features: torch.Tensor, total: torch.Tensor, sigma: torch.Ten
 
         total += features
 
-        if LooseVersion(torch.__version__) <= LooseVersion("1.7.0"):
+        if Version(torch.__version__) <= Version("1.7.0"):
             sigma += torch.ger(features, features)
         else:
             sigma += torch.outer(features, features)
@@ -163,7 +163,7 @@ def _get_covariance(self, sigma: torch.Tensor, total: torch.Tensor) -> torch.Ten
         Calculates covariance from mean and sum of products of variables
         """
 
-        if LooseVersion(torch.__version__) <= LooseVersion("1.7.0"):
+        if Version(torch.__version__) <= Version("1.7.0"):
             sub_matrix = torch.ger(total, total)
         else:
             sub_matrix = torch.outer(total, total)
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -24,9 +24,7 @@ def find_version(*file_paths):
 
 VERSION = find_version("ignite", "__init__.py")
 
-requirements = [
-    "torch>=1.3,<2",
-]
+requirements = ["torch>=1.3,<2", "packaging"]
 
 setup(
     # Metadata


Test patch:
diff --git a/tests/ignite/contrib/handlers/test_tqdm_logger.py b/tests/ignite/contrib/handlers/test_tqdm_logger.py
--- a/tests/ignite/contrib/handlers/test_tqdm_logger.py
+++ b/tests/ignite/contrib/handlers/test_tqdm_logger.py
@@ -2,12 +2,12 @@
 import sys
 import time
 from argparse import Namespace
-from distutils.version import LooseVersion
 from unittest.mock import patch
 
 import numpy as np
 import pytest
 import torch
+from packaging.version import Version
 
 from ignite.contrib.handlers import ProgressBar
 from ignite.engine import Engine, Events
@@ -21,7 +21,7 @@
 def get_tqdm_version():
     import tqdm
 
-    return LooseVersion(tqdm.__version__)
+    return Version(tqdm.__version__)
 
 
 def update_fn(engine, batch):
@@ -55,7 +55,7 @@ def test_pbar(capsys):
     err = captured.err.split("\r")
     err = list(map(lambda x: x.strip(), err))
     err = list(filter(None, err))
-    if get_tqdm_version() < LooseVersion("4.49.0"):
+    if get_tqdm_version() < Version("4.49.0"):
         expected = "Epoch [2/2]: [1/2]  50%|█████     , a=1 [00:00<00:00]"
     else:
         expected = "Epoch [2/2]: [1/2]  50%|█████     , a=1 [00:00<?]"
@@ -79,7 +79,7 @@ def test_pbar_file(tmp_path):
     file = open(str(file_path), "r")
     lines = file.readlines()
 
-    if get_tqdm_version() < LooseVersion("4.49.0"):
+    if get_tqdm_version() < Version("4.49.0"):
         expected = "Epoch [2/2]: [1/2]  50%|█████     , a=1 [00:00<00:00]\n"
     else:
         expected = "Epoch [2/2]: [1/2]  50%|█████     , a=1 [00:00<?]\n"
@@ -166,7 +166,7 @@ def step(engine, batch):
     err = list(map(lambda x: x.strip(), err))
     err = list(filter(None, err))
     actual = err[-1]
-    if get_tqdm_version() < LooseVersion("4.49.0"):
+    if get_tqdm_version() < Version("4.49.0"):
         expected = "Iteration: [1/2]  50%|█████     , batchloss=0.5 [00:00<00:00]"
     else:
         expected = "Iteration: [1/2]  50%|█████     , batchloss=0.5 [00:00<?]"
@@ -200,7 +200,7 @@ def step(engine, batch):
     err = list(map(lambda x: x.strip(), err))
     err = list(filter(None, err))
     actual = err[-1]
-    if get_tqdm_version() < LooseVersion("4.49.0"):
+    if get_tqdm_version() < Version("4.49.0"):
         expected = "Iteration: [1/2]  50%|█████     , batchloss=0.5, another batchloss=1.5 [00:00<00:00]"
     else:
         expected = "Iteration: [1/2]  50%|█████     , batchloss=0.5, another batchloss=1.5 [00:00<?]"
@@ -234,7 +234,7 @@ def step(engine, batch):
     err = list(map(lambda x: x.strip(), err))
     err = list(filter(None, err))
     actual = err[-1]
-    if get_tqdm_version() < LooseVersion("4.49.0"):
+    if get_tqdm_version() < Version("4.49.0"):
         expected = (
             "Iteration: [1/2]  50%|█████     , batchloss=0.5, alpha=3.9, beta=12.2, gamma_0=21, gamma_1=6 [00:00<00:00]"
         )
@@ -261,7 +261,7 @@ def test_pbar_no_metric_names(capsys):
     err = list(map(lambda x: x.strip(), err))
     err = list(filter(None, err))
     actual = err[-1]
-    if get_tqdm_version() < LooseVersion("4.49.0"):
+    if get_tqdm_version() < Version("4.49.0"):
         expected = "Epoch [2/2]: [1/2]  50%|█████      [00:00<00:00]"
     else:
         expected = "Epoch [2/2]: [1/2]  50%|█████      [00:00<?]"
@@ -282,7 +282,7 @@ def test_pbar_with_output(capsys):
     err = captured.err.split("\r")
     err = list(map(lambda x: x.strip(), err))
     err = list(filter(None, err))
-    if get_tqdm_version() < LooseVersion("4.49.0"):
+    if get_tqdm_version() < Version("4.49.0"):
         expected = "Epoch [2/2]: [1/2]  50%|█████     , a=1 [00:00<00:00]"
     else:
         expected = "Epoch [2/2]: [1/2]  50%|█████     , a=1 [00:00<?]"
@@ -311,7 +311,7 @@ def test_pbar_with_scalar_output(capsys):
     err = captured.err.split("\r")
     err = list(map(lambda x: x.strip(), err))
     err = list(filter(None, err))
-    if get_tqdm_version() < LooseVersion("4.49.0"):
+    if get_tqdm_version() < Version("4.49.0"):
         expected = "Epoch [2/2]: [1/2]  50%|█████     , output=1 [00:00<00:00]"
     else:
         expected = "Epoch [2/2]: [1/2]  50%|█████     , output=1 [00:00<?]"
@@ -332,7 +332,7 @@ def test_pbar_with_str_output(capsys):
     err = captured.err.split("\r")
     err = list(map(lambda x: x.strip(), err))
     err = list(filter(None, err))
-    if get_tqdm_version() < LooseVersion("4.49.0"):
+    if get_tqdm_version() < Version("4.49.0"):
         expected = "Epoch [2/2]: [1/2]  50%|█████     , output=red [00:00<00:00]"
     else:
         expected = "Epoch [2/2]: [1/2]  50%|█████     , output=red [00:00<?]"
@@ -444,7 +444,7 @@ def test_pbar_with_max_epochs_set_to_one(capsys):
     err = captured.err.split("\r")
     err = list(map(lambda x: x.strip(), err))
     err = list(filter(None, err))
-    if get_tqdm_version() < LooseVersion("4.49.0"):
+    if get_tqdm_version() < Version("4.49.0"):
         expected = "Iteration: [1/2]  50%|█████     , a=1 [00:00<00:00]"
     else:
         expected = "Iteration: [1/2]  50%|█████     , a=1 [00:00<?]"
diff --git a/tests/ignite/distributed/test_launcher.py b/tests/ignite/distributed/test_launcher.py
--- a/tests/ignite/distributed/test_launcher.py
+++ b/tests/ignite/distributed/test_launcher.py
@@ -1,11 +1,11 @@
 import os
 import subprocess
 import sys
-from distutils.version import LooseVersion
 from pathlib import Path
 
 import pytest
 import torch
+from packaging.version import Version
 
 import ignite.distributed as idist
 from ignite.distributed.utils import has_hvd_support, has_native_dist_support, has_xla_support
@@ -67,7 +67,7 @@ def _test_check_idist_parallel_torch_launch(init_method, fp, backend, nprocs):
     # torchrun --nproc_per_node=nprocs tests/ignite/distributed/check_idist_parallel.py --backend=backend
 
     cmd = []
-    if LooseVersion(torch.__version__) >= LooseVersion("1.10.0"):
+    if Version(torch.__version__) >= Version("1.10.0"):
         cmd += ["torchrun"]
     else:
         cmd += [
diff --git a/tests/ignite/engine/test_create_supervised.py b/tests/ignite/engine/test_create_supervised.py
--- a/tests/ignite/engine/test_create_supervised.py
+++ b/tests/ignite/engine/test_create_supervised.py
@@ -1,5 +1,4 @@
 import os
-from distutils.version import LooseVersion
 from importlib.util import find_spec
 from typing import Optional, Union
 from unittest import mock
@@ -7,6 +6,7 @@
 
 import pytest
 import torch
+from packaging.version import Version
 from pytest import approx
 from torch.nn import Linear
 from torch.nn.functional import mse_loss
@@ -118,13 +118,13 @@ def _():
                 assert not hasattr(state, "scaler")
 
     else:
-        if LooseVersion(torch.__version__) >= LooseVersion("1.7.0"):
+        if Version(torch.__version__) >= Version("1.7.0"):
             # This is broken in 1.6.0 but will be probably fixed with 1.7.0
             with pytest.raises(RuntimeError, match=r"Expected all tensors to be on the same device"):
                 trainer.run(data)
 
 
-@pytest.mark.skipif(LooseVersion(torch.__version__) < LooseVersion("1.6.0"), reason="Skip if < 1.6.0")
+@pytest.mark.skipif(Version(torch.__version__) < Version("1.6.0"), reason="Skip if < 1.6.0")
 def test_create_supervised_training_scalar_assignment():
 
     with mock.patch("ignite.engine._check_arg") as check_arg_mock:
@@ -234,7 +234,7 @@ def _test_create_supervised_evaluator(
         assert model.bias.item() == approx(0.0)
 
     else:
-        if LooseVersion(torch.__version__) >= LooseVersion("1.7.0"):
+        if Version(torch.__version__) >= Version("1.7.0"):
             # This is broken in 1.6.0 but will be probably fixed with 1.7.0
             with pytest.raises(RuntimeError, match=r"Expected all tensors to be on the same device"):
                 evaluator.run(data)
@@ -391,7 +391,7 @@ def test_create_supervised_trainer_amp_error(mock_torch_cuda_amp_module):
         _test_create_supervised_trainer(amp_mode="amp", scaler=True)
 
 
-@pytest.mark.skipif(LooseVersion(torch.__version__) < LooseVersion("1.5.0"), reason="Skip if < 1.5.0")
+@pytest.mark.skipif(Version(torch.__version__) < Version("1.5.0"), reason="Skip if < 1.5.0")
 def test_create_supervised_trainer_scaler_not_amp():
     scaler = torch.cuda.amp.GradScaler(enabled=torch.cuda.is_available())
 
@@ -418,7 +418,7 @@ def test_create_supervised_trainer_on_cuda():
     _test_create_mocked_supervised_trainer(model_device=model_device, trainer_device=trainer_device)
 
 
-@pytest.mark.skipif(LooseVersion(torch.__version__) < LooseVersion("1.6.0"), reason="Skip if < 1.6.0")
+@pytest.mark.skipif(Version(torch.__version__) < Version("1.6.0"), reason="Skip if < 1.6.0")
 @pytest.mark.skipif(not torch.cuda.is_available(), reason="Skip if no GPU")
 def test_create_supervised_trainer_on_cuda_amp():
     model_device = trainer_device = "cuda"
@@ -434,7 +434,7 @@ def test_create_supervised_trainer_on_cuda_amp():
     _test_create_mocked_supervised_trainer(model_device=model_device, trainer_device=trainer_device, amp_mode="amp")
 
 
-@pytest.mark.skipif(LooseVersion(torch.__version__) < LooseVersion("1.6.0"), reason="Skip if < 1.6.0")
+@pytest.mark.skipif(Version(torch.__version__) < Version("1.6.0"), reason="Skip if < 1.6.0")
 @pytest.mark.skipif(not torch.cuda.is_available(), reason="Skip if no GPU")
 def test_create_supervised_trainer_on_cuda_amp_scaler():
     model_device = trainer_device = "cuda"
@@ -545,7 +545,7 @@ def test_create_supervised_evaluator():
     _test_mocked_supervised_evaluator()
 
     # older versions didn't have the autocast method so we skip the test for older builds
-    if LooseVersion(torch.__version__) >= LooseVersion("1.6.0"):
+    if Version(torch.__version__) >= Version("1.6.0"):
         with mock.patch("torch.cuda.amp.autocast") as mock_torch_cuda_amp_module:
             _test_create_evaluation_step_amp(mock_torch_cuda_amp_module)
 
@@ -555,7 +555,7 @@ def test_create_supervised_evaluator_on_cpu():
     _test_mocked_supervised_evaluator(evaluator_device="cpu")
 
     # older versions didn't have the autocast method so we skip the test for older builds
-    if LooseVersion(torch.__version__) >= LooseVersion("1.6.0"):
+    if Version(torch.__version__) >= Version("1.6.0"):
         with mock.patch("torch.cuda.amp.autocast") as mock_torch_cuda_amp_module:
             _test_create_evaluation_step(mock_torch_cuda_amp_module, evaluator_device="cpu")
             _test_create_evaluation_step_amp(mock_torch_cuda_amp_module, evaluator_device="cpu")
@@ -566,7 +566,7 @@ def test_create_supervised_evaluator_traced_on_cpu():
     _test_mocked_supervised_evaluator(evaluator_device="cpu", trace=True)
 
     # older versions didn't have the autocast method so we skip the test for older builds
-    if LooseVersion(torch.__version__) >= LooseVersion("1.6.0"):
+    if Version(torch.__version__) >= Version("1.6.0"):
         with mock.patch("torch.cuda.amp.autocast") as mock_torch_cuda_amp_module:
             _test_create_evaluation_step(mock_torch_cuda_amp_module, evaluator_device="cpu", trace=True)
 
@@ -584,7 +584,7 @@ def test_create_supervised_evaluator_on_cuda_with_model_on_cpu():
     _test_mocked_supervised_evaluator(evaluator_device="cuda")
 
 
-@pytest.mark.skipif(LooseVersion(torch.__version__) < LooseVersion("1.6.0"), reason="Skip if < 1.6.0")
+@pytest.mark.skipif(Version(torch.__version__) < Version("1.6.0"), reason="Skip if < 1.6.0")
 @pytest.mark.skipif(not torch.cuda.is_available(), reason="Skip if no GPU")
 def test_create_supervised_evaluator_on_cuda_amp():
     model_device = evaluator_device = "cuda"
diff --git a/tests/ignite/handlers/test_checkpoint.py b/tests/ignite/handlers/test_checkpoint.py
--- a/tests/ignite/handlers/test_checkpoint.py
+++ b/tests/ignite/handlers/test_checkpoint.py
@@ -9,7 +9,7 @@
 import pytest
 import torch
 import torch.nn as nn
-from pkg_resources import parse_version
+from packaging.version import Version
 
 import ignite.distributed as idist
 from ignite.engine import Engine, Events, State
@@ -656,7 +656,7 @@ def _test_existance(atomic, _to_save, expected):
 
 
 @pytest.mark.skipif(
-    parse_version(torch.__version__) < parse_version("1.4.0"), reason="Zipfile serialization was introduced in 1.4.0"
+    Version(torch.__version__) < Version("1.4.0"), reason="Zipfile serialization was introduced in 1.4.0"
 )
 def test_disk_saver_zipfile_serialization_keyword(dirname):
     model = DummyModel()
diff --git a/tests/ignite/handlers/test_param_scheduler.py b/tests/ignite/handlers/test_param_scheduler.py
--- a/tests/ignite/handlers/test_param_scheduler.py
+++ b/tests/ignite/handlers/test_param_scheduler.py
@@ -23,10 +23,10 @@
 except ImportError:
     has_multiplicative_lr = False
 else:
-    from distutils.version import LooseVersion
+    from packaging.version import Version
 
     # https://github.com/pytorch/pytorch/issues/32756
-    has_multiplicative_lr = LooseVersion(torch.__version__) >= LooseVersion("1.5.0")
+    has_multiplicative_lr = Version(torch.__version__) >= Version("1.5.0")
 
 
 class FakeParamScheduler(ParamScheduler):
diff --git a/tests/ignite/test_utils.py b/tests/ignite/test_utils.py
--- a/tests/ignite/test_utils.py
+++ b/tests/ignite/test_utils.py
@@ -1,10 +1,10 @@
 import logging
 import sys
 from collections import namedtuple
-from distutils.version import LooseVersion
 
 import pytest
 import torch
+from packaging.version import Version
 
 from ignite.engine import Engine, Events
 from ignite.utils import convert_tensor, deprecated, hash_checkpoint, setup_logger, to_onehot
@@ -241,7 +241,7 @@ def test_smoke__utils():
     from ignite._utils import apply_to_tensor, apply_to_type, convert_tensor, to_onehot  # noqa: F401
 
 
-@pytest.mark.skipif(LooseVersion(torch.__version__) < LooseVersion("1.5.0"), reason="Skip if < 1.5.0")
+@pytest.mark.skipif(Version(torch.__version__) < Version("1.5.0"), reason="Skip if < 1.5.0")
 def test_hash_checkpoint(tmp_path):
     # download lightweight model
     from torchvision.models import squeezenet1_0