Repository: conan-io/conan

Issue:
Define Conan hooks management
Following the feature of Conan Plugins #3555 we should define the following points:

- Installation of plugins: `conan config install` with custom plugins parameter or normal?
- Merge strategy for plugins with same name or same subfolders.
- Automatic activation/desactivation of plugins in *conan.conf*?
- What should be the strategy to for plugin versioning?
- Should we think about a way to detect compatibility of plugins in respect to the Conan client version?




Discussion:
Hi everyone!

Conan Plugins looks an interesting feature at first look, but could be more complex than usual. Years ago, before to use Docker, I had Vagrant + Ansible to create my C++ environment. Vagrant provides a good support for [plugins](https://www.vagrantup.com/docs/plugins/usage.html) where is possible to install directly by a simple command:

    vagrant plugin install <plugin_name>

But looking from Vagrant perspective now is necessary to review the plugin and maintain the plugin manager. Conan only supports official recipes on Conan center now, but following the same path for plugins, the community will require official plugins also.

On other hand we have pytest project, an awesome unit test python library that support dozen of [plugins](https://docs.pytest.org/en/latest/plugins.html) and doesn't have any plugin manager. Each plugin is a pip package that consumes pytest API. Do you want code coverage + pytest?

    pip install pytest-cov

Conan could provide a good API, including events (pre_download, pre_export...), but install plugins using Conan is not a good idea IMO. We already have pip to install any python stuff. Conan could manages it plugins by a config file but all stuff related to install, uninstall, update, version could be solved by pip.
I liked the idea of being able to support plugins that could be managed directly from git repos, more exactly, they could be just part of the ``conan config install`` and put them in a repo and get them installed together with the settings, profiles, etc. Also switching plugins on and off, seems a bit more convenient if they are listed in conan.conf, instead of found in the python installation (would almost enforce using a python virtualenv to maintain things clean, which is not done by every conan user)

Though I also understand the reasons, and why using python packages would be a good idea. Maybe supporting both? We will discuss this issue for 1.9, not to implement everything, but at least to have some idea how we want to manage them.
just a question using `config`. It needs a full git path to install. Is there any idea to include a plugin center? 
To be considered: is it possible to write plugins with other kind of extensions? Example: the optimized ``shutil.copytree`` that @sztomi was proposing.
It could be `.zip` or any other file compressed format. :thinking: 

In my opinion the installation and version management of plugins via conan itself adds possibly unnecessary complexity. In the end, conan only wants to be able to import the modules where the plugins are implemented. It's already possible to manage python packages via conan if one wants to; it's also already possible to import code from the repo; and it's just a matter of setting the right PYTHONPATH if people use another means of installing the modules (like pip or conda). So I would concentrate on just loading and running the plugins around the extension points.

To me, a more interesting question is how to tell conan what to load. I would prefer a repo-level config because I feel that is more portable than something in global configuration. In this case, I think this is the same for both OSS and non-OSS. Think about a contributor cloning a repo that contains a conanfile that depends on a plugin.

As for the shutil.copytree thing: I wouldn't mind having a different kind of plugin that applies a monkey-patch. 

```python
class CopytreePlugin(MonkeyPatchPlugin):
  function = shutil.copytree

  def __call__(self, *args, **kwargs):
    pass
```

The difficulty of doing this without upstream support is that we can't reliably apply a global monkeypatch in conan code.
>  I would prefer a repo-level config because I feel that is more portable than something in global configuration. In this case, I think this is the same for both OSS and non-OSS. Think about a contributor cloning a repo that contains a conanfile that depends on a plugin.

I'd say this is a more general issue, like being possible to define some remotes directly in the repo, which has been a demanded issue. Could be that the solution is something like the hierarchical system-user-project .gitconfig approach.
> Could be that the solution is something like the hierarchical system-user-project .gitconfig approach

I think that would be a worthwhile addition. Lots of the work we did on top of conan was about making stuff self-contained (except for the cache which we want shared normally).
Update on this: Finally we have considered this issue mostly referring to the renamed "plugins" to "hooks". We understand having real python plugins for Conan could make a lot of sense for more internal things such as the `shutil.copytree` suggestion above. In the other hand, hooks would be still useful to perform checks or basically additional tasks related to conan events/commands and they should be also considered part of the Conan client configuration

So, considering hooks management, my first approach on this issue is the following:

- Copy always files under _~/.conan/hooks_ folder if present in a `conan config install` avoiding git related stuff (in case you have hooks as submodules in a conan config git repo).
- Do not mess with a "merge" solution as hooks might be complex, overwrite files if present (dangerous, consider a `--force` or similar).
- Avoid automatic activation of hooks when they are installed in the **conan config install** command, they should be already activated in _conan.conf_ if user wants them activated by default.
- Best strategy to "version" hooks would be to have them separated from the config and cloning them directly in the conan hook folder *~/.conan/hooks*, that way users are able to update freely or checkout new versions in there.

Hooks will have a separate folder for each of them with the same naming of the python file entry point like: *~/.conan/hooks/attribute_checker/attribute_checker.py* However there might be folders with shared code between hooks in some other directories (that is why we cannot "activate" hooks automatically for every directory copied under "hooks" folder).


Resolving patch:
diff --git a/conans/client/conf/config_installer.py b/conans/client/conf/config_installer.py
--- a/conans/client/conf/config_installer.py
+++ b/conans/client/conf/config_installer.py
@@ -94,8 +94,12 @@ def _process_folder(folder, client_cache, output):
                 output.info("Installing profiles:")
                 profiles_path = client_cache.profiles_path
                 _handle_profiles(os.path.join(root, d), profiles_path, output)
-                break
-        dirs[:] = [d for d in dirs if d not in ("profiles", ".git")]
+            elif d == "hooks" and ".git" not in root:  # Avoid git hooks
+                output.info("Installing hooks:")
+                src_hooks_path = os.path.join(root, d)
+                dst_hooks_path = client_cache.hooks_path
+                _handle_hooks(src_hooks_path, dst_hooks_path, output)
+        dirs[:] = [d for d in dirs if d not in ("profiles", ".git", "hooks")]
 
 
 def _process_download(item, client_cache, output, tmp_folder, verify_ssl, requester):
@@ -182,3 +186,30 @@ def _process_config_install_item(item):
         verify_ssl = "true" in verify_ssl.lower()
         args = None if "none" in args.lower() else args
     return config_type, path_or_url, verify_ssl, args
+
+
+def _handle_hooks(src_hooks_path, dst_hooks_path, output):
+    """
+    Copies files to the hooks folder overwriting the files that are in the same path
+    (shutil.copytree fails on doing this), skips git related files (.git, .gitmodule...) and outputs
+    the copied files
+
+    :param src_hooks_path: Folder where the hooks come from
+    :param dst_hooks_path:  Folder where the hooks should finally go
+    :param output: Output to indicate the files copied
+    """
+    hooks_dirs = []
+    for root, dirs, files in walk(src_hooks_path):
+        if root == src_hooks_path:
+            hooks_dirs = dirs
+        else:
+            copied_files = False
+            relpath = os.path.relpath(root, src_hooks_path)
+            for f in files:
+                if ".git" not in f:
+                    dst = os.path.join(dst_hooks_path, relpath)
+                    mkdir(dst)
+                    shutil.copy(os.path.join(root, f), dst)
+                    copied_files = True
+            if copied_files and relpath in hooks_dirs:
+                output.info(" - %s" % relpath)
diff --git a/conans/client/hook_manager.py b/conans/client/hook_manager.py
--- a/conans/client/hook_manager.py
+++ b/conans/client/hook_manager.py
@@ -38,13 +38,13 @@ def __init__(self, hooks_folder, hook_names, output):
         self._hook_names = hook_names
         self.hooks = defaultdict(list)
         self.output = output
+        self._attribute_checker_path = os.path.join(self._hooks_folder, "attribute_checker.py")
 
     def create_default_hooks(self):
-        attribute_checker_path = os.path.join(self._hooks_folder, "attribute_checker.py")
-        save(attribute_checker_path, attribute_checker_hook)
+        save(self._attribute_checker_path, attribute_checker_hook)
 
     def execute(self, method_name, **kwargs):
-        if not os.path.exists(os.path.join(self._hooks_folder, "attribute_checker.py")):
+        if not os.path.exists(self._attribute_checker_path):
             self.create_default_hooks()
         if not self.hooks:
             self.load_hooks()
@@ -63,8 +63,9 @@ def load_hooks(self):
             self.load_hook(name)
 
     def load_hook(self, hook_name):
-        filename = "%s.py" % hook_name
-        hook_path = os.path.join(self._hooks_folder, filename)
+        if not hook_name.endswith(".py"):
+            hook_name = "%s.py" % hook_name
+        hook_path = os.path.normpath(os.path.join(self._hooks_folder, hook_name))
         try:
             hook = HookManager._load_module_from_file(hook_path)
             for method in valid_hook_methods:
@@ -72,9 +73,9 @@ def load_hook(self, hook_name):
                 if hook_method:
                     self.hooks[method].append((hook_name, hook_method))
         except NotFoundException:
-            self.output.warn("Hook '%s' not found in %s folder. Please remove hook "
-                             "from conan.conf or include it inside the hooks folder."
-                             % (filename, self._hooks_folder))
+            self.output.warn("Hook '%s' not found in %s folder. Please remove hook from conan.conf "
+                             "or include it inside the hooks folder." % (hook_name,
+                                                                         self._hooks_folder))
         except Exception as e:
             raise ConanException("Error loading hook '%s': %s" % (hook_path, str(e)))
 
@@ -110,9 +111,8 @@ def _load_module_from_file(hook_path):
         except Exception:
             trace = traceback.format_exc().split('\n')
             raise ConanException("Unable to load Hook in %s\n%s" % (hook_path,
-                                                                      '\n'.join(trace[3:])))
+                                                                    '\n'.join(trace[3:])))
         finally:
             sys.dont_write_bytecode = False
             sys.path.pop()
-
         return loaded
diff --git a/conans/model/scm.py b/conans/model/scm.py
--- a/conans/model/scm.py
+++ b/conans/model/scm.py
@@ -71,7 +71,7 @@ def excluded_files(self):
         return self.repo.excluded_files()
 
     def checkout(self):
-        output= ""
+        output = ""
         if self._data.type == "git":
             output += self.repo.clone(url=self._data.url)
             output += self.repo.checkout(element=self._data.revision, submodule=self._data.submodule)
@@ -99,4 +99,3 @@ def get_qualified_remote_url(self):
 
     def is_local_repository(self):
         return self.repo.is_local_repository()
-


Test patch:
diff --git a/conans/test/functional/command/config_install_test.py b/conans/test/functional/command/config_install_test.py
--- a/conans/test/functional/command/config_install_test.py
+++ b/conans/test/functional/command/config_install_test.py
@@ -381,7 +381,7 @@ def test_git_checkout_is_possible(self):
         self.client.run('config install "%s/.git"' % folder)
         check_path = os.path.join(folder, ".git")
         self._check("git, %s, True, None" % check_path)
-        file_path = os.path.join(self.client.client_cache.conan_folder, "hooks", "cust", "cust.py")
+        file_path = os.path.join(self.client.client_cache.hooks_path, "cust", "cust.py")
         self.assertFalse(os.path.exists(file_path))
         # With checkout tag and reuse url
         self.client.run('config install --args="-b 0.0.1"')
diff --git a/conans/test/functional/command/info_test.py b/conans/test/functional/command/info_test.py
--- a/conans/test/functional/command/info_test.py
+++ b/conans/test/functional/command/info_test.py
@@ -38,9 +38,9 @@ def _create(self, number, version, deps=None, deps_dev=None, export=True):
             self.client.run("export . lasote/stable")
             expected_output = textwrap.dedent(
                 """\
-                [HOOK - attribute_checker] pre_export(): WARN: Conanfile doesn't have 'url'. It is recommended to add it as attribute
-                [HOOK - attribute_checker] pre_export(): WARN: Conanfile doesn't have 'license'. It is recommended to add it as attribute
-                [HOOK - attribute_checker] pre_export(): WARN: Conanfile doesn't have 'description'. It is recommended to add it as attribute
+                [HOOK - attribute_checker.py] pre_export(): WARN: Conanfile doesn't have 'url'. It is recommended to add it as attribute
+                [HOOK - attribute_checker.py] pre_export(): WARN: Conanfile doesn't have 'license'. It is recommended to add it as attribute
+                [HOOK - attribute_checker.py] pre_export(): WARN: Conanfile doesn't have 'description'. It is recommended to add it as attribute
                 """)
             self.assertIn(expected_output, self.client.user_io.out)
 
diff --git a/conans/test/integration/hook_test.py b/conans/test/integration/hook_test.py
--- a/conans/test/integration/hook_test.py
+++ b/conans/test/integration/hook_test.py
@@ -156,22 +156,23 @@ def default_hook_test(self):
         self.assertTrue(client.client_cache.hooks_path.endswith("hooks"))
         client.save({"conanfile.py": conanfile_basic})
         client.run("export . danimtb/testing")
-        self.assertIn("[HOOK - attribute_checker] pre_export(): WARN: Conanfile doesn't have 'url'",
-                      client.out)
-        self.assertIn("[HOOK - attribute_checker] pre_export(): WARN: Conanfile doesn't have "
-                      "'description'", client.out)
-        self.assertIn("[HOOK - attribute_checker] pre_export(): WARN: Conanfile doesn't have "
-                      "'license'", client.out)
+        self.assertIn("[HOOK - attribute_checker.py] pre_export(): "
+                      "WARN: Conanfile doesn't have 'url'", client.out)
+        self.assertIn("[HOOK - attribute_checker.py] pre_export(): "
+                      "WARN: Conanfile doesn't have 'description'", client.out)
+        self.assertIn("[HOOK - attribute_checker.py] pre_export(): "
+                      "WARN: Conanfile doesn't have 'license'", client.out)
 
     def complete_hook_test(self):
         server = TestServer([], users={"danimtb": "pass"})
         client = TestClient(servers={"default": server}, users={"default": [("danimtb", "pass")]})
-        hook_path = os.path.join(client.client_cache.hooks_path, "complete_hook.py")
+        hook_path = os.path.join(client.client_cache.hooks_path, "complete_hook",
+                                 "complete_hook.py")
         client.save({hook_path: complete_hook, "conanfile.py": conanfile_basic})
         conanfile_path = os.path.join(client.current_folder, "conanfile.py")
         conanfile_cache_path = client.client_cache.conanfile(
             ConanFileReference("basic", "0.1", "danimtb", "testing"))
-        client.run("config set hooks.complete_hook")
+        client.run("config set hooks.complete_hook/complete_hook.py")
 
         client.run("source .")
         self._check_source(conanfile_path, client.out)
@@ -197,7 +198,7 @@ def complete_hook_test(self):
 
         client.run("remove * --force")
         client.run("create . danimtb/testing")
-        self._check_export(conanfile_path, conanfile_cache_path, client.out) # Export gets
+        self._check_export(conanfile_path, conanfile_cache_path, client.out)  # Export gets
         self._check_source(conanfile_cache_path, client.out, in_cache=True)
         self._check_build(conanfile_cache_path, client.out, in_cache=True)
         self._check_package(conanfile_cache_path, client.out, in_cache=True)
@@ -227,111 +228,173 @@ def complete_hook_test(self):
 
     def _check_source(self, conanfile_path, out, in_cache=False):
         reference = REFERENCE_CACHE if in_cache else REFERENCE_LOCAL
-        self.assertIn("[HOOK - complete_hook] pre_source(): conanfile_path=%s" % conanfile_path, out)
-        self.assertIn("[HOOK - complete_hook] post_source(): conanfile_path=%s" % conanfile_path, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_source(): "
+                      "conanfile_path=%s" % conanfile_path, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_source(): "
+                      "conanfile_path=%s" % conanfile_path, out)
         if in_cache:
-            self.assertIn("[HOOK - complete_hook] pre_source(): reference=%s" % reference, out)
-            self.assertIn("[HOOK - complete_hook] post_source(): reference=%s" % reference, out)
+            self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_source(): "
+                          "reference=%s" % reference, out)
+            self.assertIn("[HOOK - complete_hook/complete_hook.py] post_source(): "
+                          "reference=%s" % reference, out)
 
     def _check_build(self, conanfile_path,  out, in_cache=False):
         reference = REFERENCE_CACHE if in_cache else REFERENCE_LOCAL
         if in_cache:
-            self.assertIn("[HOOK - complete_hook] pre_build(): reference=%s" % reference, out)
-            self.assertIn("[HOOK - complete_hook] pre_build(): package_id=%s" % PACKAGE_ID, out)
-            self.assertIn("[HOOK - complete_hook] post_build(): reference=%s" % reference, out)
-            self.assertIn("[HOOK - complete_hook] post_build(): package_id=%s" % PACKAGE_ID, out)
+            self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_build(): "
+                          "reference=%s" % reference, out)
+            self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_build(): "
+                          "package_id=%s" % PACKAGE_ID, out)
+            self.assertIn("[HOOK - complete_hook/complete_hook.py] post_build(): "
+                          "reference=%s" % reference, out)
+            self.assertIn("[HOOK - complete_hook/complete_hook.py] post_build(): "
+                          "package_id=%s" % PACKAGE_ID, out)
         else:
-            self.assertIn("[HOOK - complete_hook] pre_build(): conanfile_path=%s" % conanfile_path,
-                          out)
-            self.assertIn("[HOOK - complete_hook] post_build(): conanfile_path=%s" % conanfile_path,
-                          out)
+            self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_build(): "
+                          "conanfile_path=%s" % conanfile_path, out)
+            self.assertIn("[HOOK - complete_hook/complete_hook.py] post_build(): "
+                          "conanfile_path=%s" % conanfile_path, out)
 
     def _check_package(self, conanfile_path, out, in_cache=False):
         reference = REFERENCE_CACHE if in_cache else REFERENCE_LOCAL
-        self.assertIn("[HOOK - complete_hook] pre_package(): conanfile_path=%s" % conanfile_path,
-                      out)
-        self.assertIn("[HOOK - complete_hook] post_package(): conanfile_path=%s" % conanfile_path,
-                      out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_package(): "
+                      "conanfile_path=%s" % conanfile_path, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_package(): "
+                      "conanfile_path=%s" % conanfile_path, out)
         if in_cache:
-            self.assertIn("[HOOK - complete_hook] pre_package(): reference=%s" % reference, out)
-            self.assertIn("[HOOK - complete_hook] pre_package(): package_id=%s" % PACKAGE_ID, out)
-            self.assertIn("[HOOK - complete_hook] post_package(): reference=%s" % reference, out)
-            self.assertIn("[HOOK - complete_hook] post_package(): package_id=%s" % PACKAGE_ID, out)
+            self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_package(): "
+                          "reference=%s" % reference, out)
+            self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_package(): "
+                          "package_id=%s" % PACKAGE_ID, out)
+            self.assertIn("[HOOK - complete_hook/complete_hook.py] post_package(): "
+                          "reference=%s" % reference, out)
+            self.assertIn("[HOOK - complete_hook/complete_hook.py] post_package(): "
+                          "package_id=%s" % PACKAGE_ID, out)
 
     def _check_export(self, conanfile_path, conanfile_cache_path, out):
-        self.assertIn("[HOOK - complete_hook] pre_export(): conanfile_path=%s" % conanfile_path, out)
-        self.assertIn("[HOOK - complete_hook] pre_export(): reference=%s" % REFERENCE_CACHE, out)
-        self.assertIn("[HOOK - complete_hook] post_export(): conanfile_path=%s" % conanfile_cache_path, out)
-        self.assertIn("[HOOK - complete_hook] post_export(): reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_export(): "
+                      "conanfile_path=%s" % conanfile_path, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_export(): "
+                      "reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_export(): "
+                      "conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_export(): "
+                      "reference=%s" % REFERENCE_CACHE, out)
 
     def _check_export_pkg(self, conanfile_cache_path, out):
-        self.assertIn("[HOOK - complete_hook] pre_package(): conanfile_path=%s" % conanfile_cache_path, out)
-        self.assertIn("[HOOK - complete_hook] pre_package(): reference=%s" % REFERENCE_CACHE, out)
-        self.assertIn("[HOOK - complete_hook] pre_package(): package_id=%s" % PACKAGE_ID, out)
-        self.assertIn("[HOOK - complete_hook] post_package(): conanfile_path=%s" % conanfile_cache_path, out)
-        self.assertIn("[HOOK - complete_hook] post_package(): reference=%s" % REFERENCE_CACHE, out)
-        self.assertIn("[HOOK - complete_hook] post_package(): package_id=%s" % PACKAGE_ID, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_package(): "
+                      "conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_package(): "
+                      "reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_package(): "
+                      "package_id=%s" % PACKAGE_ID, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_package(): "
+                      "conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_package(): "
+                      "reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_package(): "
+                      "package_id=%s" % PACKAGE_ID, out)
 
     def _check_upload(self, conanfile_cache_path, out):
-        self.assertIn("[HOOK - complete_hook] pre_upload(): conanfile_path=%s" % conanfile_cache_path, out)
-        self.assertIn("[HOOK - complete_hook] pre_upload(): reference=%s" % REFERENCE_CACHE, out)
-        self.assertIn("[HOOK - complete_hook] pre_upload(): remote.name=default", out)
-        self.assertIn("[HOOK - complete_hook] post_upload(): conanfile_path=%s" % conanfile_cache_path, out)
-        self.assertIn("[HOOK - complete_hook] post_upload(): reference=%s" % REFERENCE_CACHE, out)
-        self.assertIn("[HOOK - complete_hook] post_upload(): remote.name=default", out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_upload(): "
+                      "conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_upload(): "
+                      "reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_upload(): "
+                      "remote.name=default", out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_upload(): "
+                      "conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_upload(): "
+                      "reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_upload(): "
+                      "remote.name=default", out)
 
     def _check_upload_recipe(self, conanfile_cache_path, out):
-        self.assertIn("[HOOK - complete_hook] pre_upload_recipe(): conanfile_path=%s" % conanfile_cache_path, out)
-        self.assertIn("[HOOK - complete_hook] pre_upload_recipe(): reference=%s" % REFERENCE_CACHE, out)
-        self.assertIn("[HOOK - complete_hook] pre_upload_recipe(): remote.name=default", out)
-        self.assertIn("[HOOK - complete_hook] post_upload_recipe(): conanfile_path=%s" % conanfile_cache_path, out)
-        self.assertIn("[HOOK - complete_hook] post_upload_recipe(): reference=%s" % REFERENCE_CACHE, out)
-        self.assertIn("[HOOK - complete_hook] post_upload_recipe(): remote.name=default", out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_upload_recipe(): "
+                      "conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_upload_recipe(): "
+                      "reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_upload_recipe(): "
+                      "remote.name=default", out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_upload_recipe(): "
+                      "conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_upload_recipe(): "
+                      "reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_upload_recipe(): "
+                      "remote.name=default", out)
 
     def _check_upload_package(self, conanfile_cache_path, out):
-        self.assertIn("[HOOK - complete_hook] pre_upload_package(): conanfile_path=%s" % conanfile_cache_path, out)
-        self.assertIn("[HOOK - complete_hook] pre_upload_package(): reference=%s" % REFERENCE_CACHE, out)
-        self.assertIn("[HOOK - complete_hook] pre_upload_package(): package_id=%s" % PACKAGE_ID, out)
-        self.assertIn("[HOOK - complete_hook] pre_upload_package(): remote.name=default", out)
-        self.assertIn("[HOOK - complete_hook] post_upload_package(): conanfile_path=%s" % conanfile_cache_path, out)
-        self.assertIn("[HOOK - complete_hook] post_upload_package(): reference=%s" % REFERENCE_CACHE, out)
-        self.assertIn("[HOOK - complete_hook] post_upload_package(): package_id=%s" % PACKAGE_ID, out)
-        self.assertIn("[HOOK - complete_hook] post_upload_package(): remote.name=default", out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_upload_package(): "
+                      "conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_upload_package(): "
+                      "reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_upload_package(): "
+                      "package_id=%s" % PACKAGE_ID, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_upload_package(): "
+                      "remote.name=default", out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_upload_package(): "
+                      "conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_upload_package(): "
+                      "reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_upload_package(): "
+                      "package_id=%s" % PACKAGE_ID, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_upload_package(): "
+                      "remote.name=default", out)
 
     def _check_download(self, conanfile_cache_path, out):
-        self.assertIn("[HOOK - complete_hook] pre_download(): reference=%s" % REFERENCE_CACHE, out)
-        self.assertIn("[HOOK - complete_hook] pre_download(): remote.name=default", out)
-        self.assertIn("[HOOK - complete_hook] post_download(): conanfile_path=%s" % conanfile_cache_path, out)
-        self.assertIn("[HOOK - complete_hook] post_download(): reference=%s" % REFERENCE_CACHE, out)
-        self.assertIn("[HOOK - complete_hook] post_download(): remote.name=default", out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_download(): "
+                      "reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_download(): "
+                      "remote.name=default", out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_download(): "
+                      "conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_download(): "
+                      "reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_download(): "
+                      "remote.name=default", out)
 
     def _check_download_recipe(self, conanfile_cache_path, out):
-        self.assertIn("[HOOK - complete_hook] pre_download_recipe(): reference=%s" % REFERENCE_CACHE, out)
-        self.assertIn("[HOOK - complete_hook] pre_download_recipe(): remote.name=default", out)
-        self.assertIn("[HOOK - complete_hook] post_download_recipe(): conanfile_path=%s" % conanfile_cache_path, out)
-        self.assertIn("[HOOK - complete_hook] post_download_recipe(): reference=%s" % REFERENCE_CACHE, out)
-        self.assertIn("[HOOK - complete_hook] post_download_recipe(): remote.name=default", out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_download_recipe(): "
+                      "reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_download_recipe(): "
+                      "remote.name=default", out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_download_recipe(): "
+                      "conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_download_recipe(): "
+                      "reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_download_recipe(): "
+                      "remote.name=default", out)
 
     def _check_download_package(self, conanfile_cache_path, out):
-        self.assertIn("[HOOK - complete_hook] pre_download_package(): conanfile_path=%s" % conanfile_cache_path, out)
-        self.assertIn("[HOOK - complete_hook] pre_download_package(): reference=%s" % REFERENCE_CACHE, out)
-        self.assertIn("[HOOK - complete_hook] pre_download_package(): package_id=%s" % PACKAGE_ID, out)
-        self.assertIn("[HOOK - complete_hook] pre_download_package(): remote.name=default", out)
-        self.assertIn("[HOOK - complete_hook] post_download_package(): conanfile_path=%s" % conanfile_cache_path, out)
-        self.assertIn("[HOOK - complete_hook] post_download_package(): reference=%s" % REFERENCE_CACHE, out)
-        self.assertIn("[HOOK - complete_hook] post_download_package(): package_id=%s" % PACKAGE_ID, out)
-        self.assertIn("[HOOK - complete_hook] post_download_package(): remote.name=default", out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_download_package(): "
+                      "conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_download_package(): "
+                      "reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_download_package(): "
+                      "package_id=%s" % PACKAGE_ID, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] pre_download_package(): "
+                      "remote.name=default", out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_download_package(): "
+                      "conanfile_path=%s" % conanfile_cache_path, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_download_package(): "
+                      "reference=%s" % REFERENCE_CACHE, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_download_package(): "
+                      "package_id=%s" % PACKAGE_ID, out)
+        self.assertIn("[HOOK - complete_hook/complete_hook.py] post_download_package(): "
+                      "remote.name=default", out)
 
     def import_hook_test(self):
         client = TestClient()
-        hook_path = os.path.join(client.client_cache.hooks_path, "my_hook.py")
-        init_path = os.path.join(client.client_cache.hooks_path, "custom_module", "__init__.py")
-        custom_path = os.path.join(client.client_cache.hooks_path, "custom_module", "custom.py")
-        client.save({hook_path: complete_hook,
-                     init_path: "",
+        hook_path = os.path.join(client.client_cache.hooks_path, "my_hook", "my_hook.py")
+        init_path = os.path.join(client.client_cache.hooks_path, "my_hook", "custom_module",
+                                 "__init__.py")
+        custom_path = os.path.join(client.client_cache.hooks_path, "my_hook", "custom_module",
+                                   "custom.py")
+        client.save({init_path: "",
                      custom_path: custom_module,
                      hook_path: my_hook,
                      "conanfile.py": conanfile_basic})
-        client.run("config set hooks.my_hook")
+        client.run("config set hooks.my_hook/my_hook.py")
         client.run("export . danimtb/testing")
-        self.assertIn("[HOOK - my_hook] pre_export(): my_printer(): CUSTOM MODULE", client.out)
+        self.assertIn("[HOOK - my_hook/my_hook.py] pre_export(): my_printer(): CUSTOM MODULE",
+                      client.out)
diff --git a/conans/test/functional/old/hook_manager_test.py b/conans/test/unittests/client/hook_manager_test.py
similarity index 94%
rename from conans/test/functional/old/hook_manager_test.py
rename to conans/test/unittests/client/hook_manager_test.py
--- a/conans/test/functional/old/hook_manager_test.py
+++ b/conans/test/unittests/client/hook_manager_test.py
@@ -82,7 +82,7 @@ def check_output_test(self):
         methods = hook_manager.hooks.keys()
         for method in methods:
             hook_manager.execute(method)
-            self.assertIn("[HOOK - my_hook] %s(): %s()" % (method, method), output)
+            self.assertIn("[HOOK - my_hook.py] %s(): %s()" % (method, method), output)
 
     def no_error_with_no_method_test(self):
         hook_manager, output, hook_path = self._init()
@@ -110,4 +110,4 @@ def pre_build(output, **kwargs):
         try:
             hook_manager.execute("pre_build")
         except ConanException as e:
-            self.assertIn("[HOOK - my_hook] pre_build(): My custom exception", str(e))
+            self.assertIn("[HOOK - my_hook.py] pre_build(): My custom exception", str(e))
diff --git a/conans/test/utils/tools.py b/conans/test/utils/tools.py
--- a/conans/test/utils/tools.py
+++ b/conans/test/utils/tools.py
@@ -576,8 +576,7 @@ def _init_collaborators(self, user_io=None):
                                             get_request_timeout())
 
             self.hook_manager = HookManager(self.client_cache.hooks_path,
-                                            get_env("CONAN_HOOKS", list()),
-                                            self.user_io.out)
+                                            get_env("CONAN_HOOKS", list()), self.user_io.out)
 
             self.localdb, self.rest_api_client, self.remote_manager = Conan.instance_remote_manager(
                                                             self.requester, self.client_cache,