Repository: mkdocs/mkdocs

Issue:
No python version marker on importlib-metadata
### Discussed in https://github.com/mkdocs/mkdocs/discussions/2956

<div type='discussions-op-text'>

<sup>Originally posted by **pawamoy** August 31, 2022</sup>
The `importlib-metadata` dependency was bumped to `>=4.3` in https://github.com/mkdocs/mkdocs/commit/ec9b583b02376b909c10889fef780cdf2f25a23d.

We're now getting resolution conflicts with Flake8 because they set an upper bound on `importlib-metadata`: `<4.3`. From their answers in different issues and PRs (starting point here: https://github.com/PyCQA/flake8/issues/1503), it seems there has been quite some friction regarding this, and they likely won't change their upper bound.

Back to MkDocs: shouldn't the dependency have a marker like `python_version < '3.8'`?

Actual conflict we get (PDM output, URLs stripped):

```
Conflicts detected: 
  importlib-metadata>=4.3 (from <Candidate mkdocs 1.3.1)
  importlib-metadata; python_version < "3.8" (from <Candidate flake8-comprehensions 3.10.0)
  importlib-metadata; python_version < "3.8" (from <Candidate flake8-tidy-imports 4.8.0)
  importlib-metadata>=0.12; python_version < "3.8" (from <Candidate pytest 7.1.2)
  importlib-metadata>=3.6.0; python_version < "3.10" (from <Candidate pytest-randomly 3.12.0)
  importlib-metadata; python_version < "3.8" (from <Candidate wps-light 0.16.1)
  importlib-metadata<4.3,>=1.1.0; python_version < "3.8" (from <Candidate flake8 5.0.4)
```

We're using Python 3.8 so I believe adding the marker would help in our case at least. Unless the right marker to use is `python_version < '3.10'`.</div>

Discussion:
oprypin: Thanks for the research.

Usually the try-except imports apply nicely to stdlib modules that have 3rd party shims and you want to prioritize stdlib if it exists. And you do **have to** try stdlib first. The reason is that otherwise the behavior will be affected by the external state of whether this package is installed: Confusing error if it's not installed when it should be  and just arbitrarily using the (hopefully) equivalent implementation when it happens to be installed but was not requested.

Here, the stdlib module existing is not enough, so that approach can't be used. So I think we indeed have to use an explicit version check. 

```python
if sys.version_info >= (3, 10):
    from importlib.metadata import entry_points
else:
    from importlib_metadata import entry_points  # type: ignore
```
pawamoy: Thanks, I've applied your suggestion.

Patch:
diff --git a/mkdocs/plugins.py b/mkdocs/plugins.py
index 65631468..68a24e3b 100644
--- a/mkdocs/plugins.py
+++ b/mkdocs/plugins.py
@@ -5,10 +5,15 @@ Implements the plugin API for MkDocs.
 from __future__ import annotations
 
 import logging
+import sys
 from collections import OrderedDict
 from typing import Any, Callable, Dict, Optional, Sequence, Tuple, TypeVar, overload
 
-import importlib_metadata
+if sys.version_info >= (3, 10):
+    from importlib.metadata import entry_points
+else:
+    from importlib_metadata import entry_points
+
 import jinja2.environment
 
 from mkdocs.config.base import BaseConfigOption, Config
@@ -23,7 +28,7 @@ log = logging.getLogger('mkdocs.plugins')
 def get_plugins():
     """Return a dict of all installed Plugins as {name: EntryPoint}."""
 
-    plugins = importlib_metadata.entry_points(group='mkdocs.plugins')
+    plugins = entry_points(group='mkdocs.plugins')
 
     # Allow third-party plugins to override core plugins
     pluginmap = {}
diff --git a/mkdocs/utils/__init__.py b/mkdocs/utils/__init__.py
index 7a575135..c365f877 100644
--- a/mkdocs/utils/__init__.py
+++ b/mkdocs/utils/__init__.py
@@ -12,13 +12,18 @@ import os
 import posixpath
 import re
 import shutil
+import sys
 import warnings
 from collections import defaultdict
 from datetime import datetime, timezone
 from pathlib import PurePath
 from urllib.parse import urlsplit
 
-import importlib_metadata
+if sys.version_info >= (3, 10):
+    from importlib.metadata import entry_points
+else:
+    from importlib_metadata import entry_points
+
 import yaml
 from mergedeep import merge
 from yaml_env_tag import construct_env_tag
@@ -307,7 +312,7 @@ def get_themes():
     """Return a dict of all installed themes as {name: EntryPoint}."""
 
     themes = {}
-    eps = set(importlib_metadata.entry_points(group='mkdocs.themes'))
+    eps = set(entry_points(group='mkdocs.themes'))
     builtins = {ep.name for ep in eps if ep.dist.name == 'mkdocs'}
 
     for theme in eps:
diff --git a/requirements/project-min.txt b/requirements/project-min.txt
index 6627b262..ce88eeae 100644
--- a/requirements/project-min.txt
+++ b/requirements/project-min.txt
@@ -6,7 +6,7 @@ PyYAML==5.1
 watchdog==2.0.0
 ghp-import==1.0
 pyyaml_env_tag==0.1
-importlib_metadata==4.3
+importlib_metadata==4.3; python_version < "3.10"
 packaging==20.5
 mergedeep==1.3.4
 babel==2.9.0
diff --git a/requirements/project.txt b/requirements/project.txt
index 37b8039d..b952b7ba 100644
--- a/requirements/project.txt
+++ b/requirements/project.txt
@@ -6,7 +6,7 @@ PyYAML>=5.1
 watchdog>=2.0.0
 ghp-import>=1.0
 pyyaml_env_tag>=0.1
-importlib_metadata>=4.3
+importlib_metadata>=4.3; python_version < "3.10"
 packaging>=20.5
 mergedeep>=1.3.4
 babel>=2.9.0
diff --git a/setup.py b/setup.py
index 4a008851..905d3518 100755
--- a/setup.py
+++ b/setup.py
@@ -70,7 +70,7 @@ setup(
         'watchdog>=2.0',
         'ghp-import>=1.0',
         'pyyaml_env_tag>=0.1',
-        'importlib_metadata>=4.3',
+        'importlib_metadata>=4.3; python_version < "3.10"',
         'packaging>=20.5',
         'mergedeep>=1.3.4'
     ],


Test patch:
diff --git a/mkdocs/tests/plugin_tests.py b/mkdocs/tests/plugin_tests.py
index 81260f43..631cd30d 100644
--- a/mkdocs/tests/plugin_tests.py
+++ b/mkdocs/tests/plugin_tests.py
@@ -239,7 +239,7 @@ MockEntryPoint = mock.Mock()
 MockEntryPoint.configure_mock(**{'name': 'sample', 'load.return_value': DummyPlugin})
 
 
-@mock.patch('importlib_metadata.entry_points', return_value=[MockEntryPoint])
+@mock.patch('mkdocs.plugins.entry_points', return_value=[MockEntryPoint])
 class TestPluginConfig(unittest.TestCase):
     def test_plugin_config_without_options(self, mock_class):
 
diff --git a/mkdocs/tests/utils/utils_tests.py b/mkdocs/tests/utils/utils_tests.py
index a59ce5de..7f1f8f83 100644
--- a/mkdocs/tests/utils/utils_tests.py
+++ b/mkdocs/tests/utils/utils_tests.py
@@ -293,7 +293,7 @@ class UtilsTests(unittest.TestCase):
 
         self.assertEqual(sorted(utils.get_theme_names()), ['mkdocs', 'readthedocs'])
 
-    @mock.patch('importlib_metadata.entry_points', autospec=True)
+    @mock.patch('mkdocs.utils.entry_points', autospec=True)
     def test_get_theme_dir(self, mock_iter):
 
         path = 'some/path'
@@ -312,7 +312,7 @@ class UtilsTests(unittest.TestCase):
         with self.assertRaises(KeyError):
             utils.get_theme_dir('nonexistanttheme')
 
-    @mock.patch('importlib_metadata.entry_points', autospec=True)
+    @mock.patch('mkdocs.utils.entry_points', autospec=True)
     def test_get_theme_dir_importerror(self, mock_iter):
 
         theme = mock.Mock()
@@ -325,7 +325,7 @@ class UtilsTests(unittest.TestCase):
         with self.assertRaises(ImportError):
             utils.get_theme_dir(theme.name)
 
-    @mock.patch('importlib_metadata.entry_points', autospec=True)
+    @mock.patch('mkdocs.utils.entry_points', autospec=True)
     def test_get_themes_warning(self, mock_iter):
 
         theme1 = mock.Mock()
@@ -342,7 +342,7 @@ class UtilsTests(unittest.TestCase):
 
         self.assertEqual(sorted(utils.get_theme_names()), sorted(['mkdocs2']))
 
-    @mock.patch('importlib_metadata.entry_points', autospec=True)
+    @mock.patch('mkdocs.utils.entry_points', autospec=True)
     def test_get_themes_error(self, mock_iter):
 
         theme1 = mock.Mock()