Repository: DakaraProject/dakara-base

Issue:
Use `as_file` to get resources
[In `importlib.resources`](https://docs.python.org/3.11/library/importlib.resources.html), the `path` method has been deprecated since Python 3.11, the `as_file` method has been available since Python 3.9.

Discussion:

Solution patch:
diff --git a/src/dakara_base/config.py b/src/dakara_base/config.py
index c53c198..504ec69 100644
--- a/src/dakara_base/config.py
+++ b/src/dakara_base/config.py
@@ -33,8 +33,9 @@
 """
 
 import logging
-import shutil
 from collections import UserDict
+from importlib.resources import as_file, files
+from shutil import copyfile
 
 import coloredlogs
 import progressbar
@@ -42,12 +43,6 @@
 import yaml.parser
 from environs import Env, EnvError
 
-try:
-    from importlib.resources import path
-
-except ImportError:
-    from importlib_resources import path
-
 from dakara_base.directory import directories
 from dakara_base.exceptions import DakaraError
 from dakara_base.utils import strtobool
@@ -304,7 +299,7 @@ def create_config_file(resource, filename, force=False):
         force (bool): If True, config file in user directory is overwritten if
             it existed already. Otherwise, prompt the user.
     """
-    with path(resource, filename) as origin:
+    with as_file(files(resource).joinpath(filename)) as origin:
         # get the file
         destination = directories.user_config_path / filename
 
@@ -321,8 +316,8 @@ def create_config_file(resource, filename, force=False):
                 return
 
         # copy file
-        shutil.copyfile(origin, destination)
-        logger.info("Config created in '{}'".format(destination))
+        copyfile(origin, destination)
+        logger.info("Config created in '%s'", destination)
 
 
 class ConfigError(DakaraError):


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
index 28d34eb..9d2f46f 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -1,14 +1,8 @@
 import os
-from unittest import TestCase
-from unittest.mock import ANY, MagicMock, PropertyMock, patch
-
-try:
-    from importlib.resources import path
-
-except ImportError:
-    from importlib_resources import path
-
+from importlib.resources import as_file, files
 from pathlib import Path
+from unittest import TestCase
+from unittest.mock import PropertyMock, patch
 
 from environs import Env
 from platformdirs import PlatformDirs
@@ -165,7 +159,7 @@ def test_load_file_success(self):
 
         # call the method
         with self.assertLogs("dakara_base.config", "DEBUG") as logger:
-            with path("tests.resources", "config.yaml") as file:
+            with as_file(files("tests.resources").joinpath("config.yaml")) as file:
                 config.load_file(Path(file))
 
         # assert the result
@@ -196,7 +190,7 @@ def test_load_file_fail_parser_error(self, mocked_safe_load):
 
         # call the method
         with self.assertLogs("dakara_base.config", "DEBUG"):
-            with path("tests.resources", "config.yaml") as file:
+            with as_file(files("tests.resources").joinpath("config.yaml")) as file:
                 with self.assertRaisesRegex(
                     ConfigParseError, "Unable to parse config file"
                 ):
@@ -207,7 +201,7 @@ def test_config_env(self):
         config = Config("DAKARA")
 
         with self.assertLogs("dakara_base.config", "DEBUG"):
-            with path("tests.resources", "config.yaml") as file:
+            with as_file(files("tests.resources").joinpath("config.yaml")) as file:
                 config.load_file(Path(file))
 
         self.assertNotEqual(config.get("key").get("subkey"), "myvalue")
@@ -281,16 +275,7 @@ def test_configure_logger_no_level(self, mocked_set_level):
         mocked_set_level.assert_called_with("INFO")
 
 
-# fix required for Python 3.8 as you seemingly cannot use an invalid path as a
-# context manager
-def mock_context_manager(return_value):
-    mock = MagicMock()
-    mock.__enter__.return_value = return_value
-
-    return mock
-
-
-@patch("dakara_base.config.shutil.copyfile", autospec=True)
+@patch("dakara_base.config.copyfile", autospec=True)
 @patch.object(Path, "exists", autospec=True)
 @patch.object(Path, "mkdir", autospec=True)
 @patch.object(
@@ -299,16 +284,17 @@ def mock_context_manager(return_value):
     new_callable=PropertyMock(return_value=Path("path") / "to" / "directory"),
 )
 @patch(
-    "dakara_base.config.path",
-    return_value=mock_context_manager(Path("path") / "to" / "source"),
+    "dakara_base.config.as_file",
     autospec=True,
 )
+@patch("dakara_base.config.files", autospec=True)
 class CreateConfigFileTestCase(TestCase):
     """Test the config file creator."""
 
     def test_create_empty(
         self,
-        mocked_path,
+        mocked_files,
+        mocked_as_file,
         mocked_user_config_dir,
         mocked_mkdir,
         mocked_exists,
@@ -317,15 +303,21 @@ def test_create_empty(
         """Test create the config file in an empty directory."""
         # setup mocks
         mocked_exists.return_value = False
+        mocked_as_file.return_value.__enter__.return_value = (
+            Path("path") / "to" / "source"
+        )
 
         # call the function
         with self.assertLogs("dakara_base.config") as logger:
             create_config_file("module.resources", "config.yaml")
 
         # assert the call
-        mocked_path.assert_called_with("module.resources", "config.yaml")
-        mocked_mkdir.assert_called_with(ANY, parents=True, exist_ok=True)
-        mocked_exists.assert_called_with(ANY)
+        mocked_files.assert_called_with("module.resources")
+        mocked_files.return_value.joinpath.assert_called_with("config.yaml")
+        mocked_mkdir.assert_called_with(
+            Path("path/to/directory"), parents=True, exist_ok=True
+        )
+        mocked_exists.assert_called_with(Path("path/to/directory/config.yaml"))
         mocked_copyfile.assert_called_with(
             Path("path") / "to" / "source",
             Path("path") / "to" / "directory" / "config.yaml",
@@ -345,7 +337,8 @@ def test_create_empty(
     def test_create_existing_no(
         self,
         mocked_input,
-        mocked_path,
+        mocked_files,
+        mocked_as_file,
         mocked_user_config_dir,
         mocked_mkdir,
         mocked_exists,
@@ -355,6 +348,9 @@ def test_create_existing_no(
         # setup mocks
         mocked_exists.return_value = True
         mocked_input.return_value = "no"
+        mocked_as_file.return_value.__enter__.return_value = (
+            Path("path") / "to" / "source"
+        )
 
         # call the function
         create_config_file("module.resources", "config.yaml")
@@ -367,38 +363,23 @@ def test_create_existing_no(
             )
         )
 
-    @patch("dakara_base.config.input")
-    def test_create_existing_invalid_input(
-        self,
-        mocked_input,
-        mocked_path,
-        mocked_user_config_dir,
-        mocked_mkdir,
-        mocked_exists,
-        mocked_copyfile,
-    ):
-        """Test create the config file in a non empty directory with invalid input."""
-        # setup mocks
-        mocked_exists.return_value = True
-        mocked_input.return_value = ""
-
-        # call the function
-        create_config_file("module.resources", "config.yaml")
-
-        # assert the call
-        mocked_copyfile.assert_not_called()
-
     @patch("dakara_base.config.input")
     def test_create_existing_force(
         self,
         mocked_input,
-        mocked_path,
+        mocked_files,
+        mocked_as_file,
         mocked_user_config_dir,
         mocked_mkdir,
         mocked_exists,
         mocked_copyfile,
     ):
         """Test create the config file in a non empty directory with force overwrite."""
+        # setup mocks
+        mocked_as_file.return_value.__enter__.return_value = (
+            Path("path") / "to" / "source"
+        )
+
         # call the function
         create_config_file("module.resources", "config.yaml", force=True)