Repository: mne-tools/mne-python

Issue:
Use packaging.version for comparing versions
Because `distutils.Version` is deprecated, we have our own `_compare_version` function (https://github.com/mne-tools/mne-python/blob/main/mne/fixes.py#L32-L55). However, I was wondering (1) why this is in fixes and (2) why we do not use `packaging.version.Version` instead (which according to the `DeprecationWarning` is the recommended replacement). I'm happy to implement the changes (and probably move it back to `utils`?) if you think it makes sense. @hoechenberger WDYT?

Edit: Then we could also remove the [corresponding warning filter](https://github.com/mne-tools/mne-python/commit/523576f6bd805dfbf3e882933d779c5a2fd6d740#diff-d6b8de46e1e16287a7eed50a9cb2ed209e1d62ec5257722a2c0fce32dd2c726a).


Discussion:
Hello @cbrnr,
> However, I was wondering (1) why this is in fixes

because it "fixes" things for environments which don't have `setuptools` installed, which IIRC is currently not a dependency of MNE-Python.

> and (2) why we do not use `packaging.version.Version` instead (which according to the `DeprecationWarning` is the recommended replacement).

Because this would introduce a dependency on `packaging`, which seems less-widespread than `setuptools` :)

That said, I don't mind adding a few deps to make our code cleaner and leaner, but others may disagree
`setuptools` is bootstrapped with every Python installation, as is `pip`. So I'm not sure this is really a fix when everybody has it.
> `setuptools` is bootstrapped with every Python installation, as is `pip`. 

At least not in `conda` environments; haven't tried with `pyenv` & `venv`, though.
But yes, I understand that it's not part of the standard library, so if you compile Python yourself you don't have it.
I'd make `setuptools` a dependency. It's used everywhere and it's the [recommended replacement of distlib](https://docs.python.org/3/library/distutils.html).
> At least not in `conda` environments; haven't tried with `pyenv` & `venv`, though.

Sorry, I stand corrected, I just tested this and indeed `conda` and `mamba` do install `setuptools` and `pip` along with `python`.

I think it's just `conda-forge`'s build system that handles things differently, and requires one to explicitly list `setuptools` as a runtime dependency if it's required.
> I'd make `setuptools` a dependency. It's used everywhere and it's the [recommended replacement of distlib](https://docs.python.org/3/library/distutils.html).

No objection from my end. As soon as `distutils.Version` gets removed, we'll have to depend on `setuptools` anyway.
OK. But to summarize, we don't even need `setuptools` because the recommended package to do version checks is `packaging`, so I'd add that as a dependency. And then remove `_compare_version` from `fixes`.
> But to summarize, we don't even need `setuptools` because the recommended package to do version checks is `packaging`,

Yeah well no, I still think `setuptools` is the better choice in case we want to pick a dependency that "everybody already has installed"

Like I said I personally don't care, but other MNE devs will 😅
Let's wait for others to chime in, but here's a quote from the `pkg_resources` [documentation](https://setuptools.pypa.io/en/latest/pkg_resources.html) (which is part of `setuptools`) to direct the discussion:

 > Use of pkg_resources is discouraged in favor of importlib.resources, importlib.metadata, and their backports (resources, metadata). Please consider using those libraries instead of pkg_resources.

So I'm voting for `packaging`.
no opinion

> Message ID: ***@***.***>
>

> So I'm voting for `packaging`.

Ok, convinced. +1 for `packaging` if you believe this is a good solution
We could also vendor it if we don't want an additional dependency.
+1 for packaging

> We could also vendor it if we don't want an additional dependency.

That might indeed make sense. But let's wait until we settle #10198 to do this (or not).
> Then we could also remove the [corresponding warning filter](https://github.com/mne-tools/mne-python/commit/523576f6bd805dfbf3e882933d779c5a2fd6d740#diff-d6b8de46e1e16287a7eed50a9cb2ed209e1d62ec5257722a2c0fce32dd2c726a).

That warning filter is not for our code, it was added because a dependency (nilearn) hadn't switched off of LooseVersion yet (reported [here](https://github.com/nilearn/nilearn/issues/3108) and fixed upstream 6 days ago).  Whatever change you end up making, please add a comment to that line indicating why the warning filter exists (e.g., `# present in nilearn v 0.8.1, fixed in nilearn main`).  IMO all our warning filters should have this, so we know when to bother checking whether they're out of date

I'm also +1 for using `packaging` but I don't see the point of vendoring it. It's developed by pypa and "blessed" as the recommended replacement for this use case, and thus seems likely to be used widely / required by many other libraries / be present on the user's system already.




Yes, we should require a comment for every new filter warning! Maybe someone has the time to check which of these filters are not even necessary anymore?

I'd also try to vendor as few packages as possible, especially when we're talking about core packages provided by pypa.
> I'd also try to vendor as few packages as possible, especially when we're talking about core packages provided by pypa.

+1

Resolving patch:
diff --git a/mne/conftest.py b/mne/conftest.py
--- a/mne/conftest.py
+++ b/mne/conftest.py
@@ -125,6 +125,7 @@ def pytest_configure(config):
     ignore:.*Found the following unknown channel type.*:RuntimeWarning
     ignore:.*np\.MachAr.*:DeprecationWarning
     ignore:.*Passing unrecognized arguments to super.*:DeprecationWarning
+    # present in nilearn v 0.8.1, fixed in nilearn main
     ignore:.*distutils Version classes are deprecated.*:DeprecationWarning
     ignore:.*pandas\.Int64Index is deprecated.*:FutureWarning
     always::ResourceWarning
diff --git a/mne/fixes.py b/mne/fixes.py
--- a/mne/fixes.py
+++ b/mne/fixes.py
@@ -47,11 +47,7 @@ def _compare_version(version_a, operator, version_b):
     bool
         The result of the version comparison.
     """
-    try:
-        from pkg_resources import parse_version as parse
-    except ImportError:
-        from distutils.version import LooseVersion as parse
-
+    from packaging.version import parse
     with warnings.catch_warnings(record=True):
         warnings.simplefilter('ignore')
         return eval(f'parse("{version_a}") {operator} parse("{version_b}")')


Test patch: