Repository: conan-io/conan (python)

Issue:
[bug] raising ConanInvalidConfiguration from pre_validate hook propagates exception
### Describe the bug

With a hook like this:
```
def pre_validate(conanfile):
    if some_condition_external_to_the_recipe:
        raise ConanInvalidConfiguration("Raised from pre_validate")
```

The expectation is that this behaves _as if_ exception was raised from within the `validate()` method.
Instead, the exception is propagated all the way to the top 


The problem is here:
https://github.com/conan-io/conan/blob/cc258fb079f5c7449d72d74f64156d61b73d6546/conans/client/graph/compute_pid.py#L74-L85

The hooks should be invoked from within the try/except clause


### How to reproduce it

_No response_

Pull request:
[fix] Honor when raising ConanInvalidConfiguration in hooks
Hello!

The issue #18384 shows when a `ConanInvalidConfiguration` is raised from `pre_validate` it exits as a regular exception instead of marking as invalid package ID only.

The `HookManager` forwards any exception as `ConanException`, transforming `ConanInvalidConfiguration`: https://github.com/conan-io/conan/blob/2.17.0/conan/internal/hook_manager.py#L36

This PR change how the `HookManager` manages `ConanInvalidConfiguration`, forwarding it instead of converting to `ConanException`.

/cc @jcar87 

Changelog: Fix: Forward ConanInvalidConfiguration when raised in hooks
Docs: Omit

fixes #18384

- [x] Refer to the issue that supports this Pull Request.
- [x] If the issue has missing info, explain the purpose/use case/pain/need that covers this Pull Request.
- [x] I've read the [Contributing guide](https://github.com/conan-io/conan/blob/develop2/.github/CONTRIBUTING.md).
- [x] I've followed the PEP8 style guides for Python code.
- [ ] I've opened another PR in the Conan docs repo to the ``develop`` branch, documenting this one.


Patch:
diff --git a/conan/internal/graph/compute_pid.py b/conan/internal/graph/compute_pid.py
index 9bec5eb28..4c4c6abb4 100644
--- a/conan/internal/graph/compute_pid.py
+++ b/conan/internal/graph/compute_pid.py
@@ -73,15 +73,15 @@ def run_validate_package_id(conanfile, hook_manager=None):
 
     if hasattr(conanfile, "validate"):
         with conanfile_exception_formatter(conanfile, "validate"):
-            if hook_manager:
-                hook_manager.execute("pre_validate", conanfile=conanfile)
             with conanfile_remove_attr(conanfile, ['cpp_info'], "validate"):
                 try:
+                    if hook_manager:
+                        hook_manager.execute("pre_validate", conanfile=conanfile)
                     conanfile.validate()
+                    if hook_manager:
+                        hook_manager.execute("post_validate", conanfile=conanfile)
                 except ConanInvalidConfiguration as e:
                     conanfile.info.invalid = str(e)
-            if hook_manager:
-                hook_manager.execute("post_validate", conanfile=conanfile)
 
     # Once we are done, call package_id() to narrow and change possible values
     if hasattr(conanfile, "package_id"):
diff --git a/conan/internal/hook_manager.py b/conan/internal/hook_manager.py
index 19ad62ffd..7ef83bfc8 100644
--- a/conan/internal/hook_manager.py
+++ b/conan/internal/hook_manager.py
@@ -1,7 +1,7 @@
 import os
 
 from conan.internal.loader import load_python_file
-from conan.errors import ConanException
+from conan.errors import ConanException, ConanInvalidConfiguration
 
 valid_hook_methods = ["pre_export", "post_export",
                       "pre_validate", "post_validate",
@@ -32,6 +32,8 @@ class HookManager:
                 conanfile.display_name = "%s: [HOOK - %s] %s()" % (conanfile.display_name, name,
                                                                    method_name)
                 method(conanfile)
+            except ConanInvalidConfiguration as e:
+                raise
             except Exception as e:
                 raise ConanException("[HOOK - %s] %s(): %s" % (name, method_name, str(e)))
             finally:


Test patch:
diff --git a/test/integration/extensions/hooks/hook_test.py b/test/integration/extensions/hooks/hook_test.py
index a062e187c..9cdd7a888 100644
--- a/test/integration/extensions/hooks/hook_test.py
+++ b/test/integration/extensions/hooks/hook_test.py
@@ -1,5 +1,6 @@
 import os
 import textwrap
+import pytest
 
 from conan.test.assets.genconanfile import GenConanfile
 from conan.test.utils.tools import TestClient
@@ -200,3 +201,34 @@ class TestHooks:
         c.run("create . --name=foo --version=0.1.0")
         assert f"foo/0.1.0: [HOOK - testing/hook_complete.py] pre_validate(): Hello" in c.out
         assert f"foo/0.1.0: [HOOK - testing/hook_complete.py] post_validate(): Hello" in c.out
+
+
+@pytest.mark.parametrize("hook_name", ["pre_validate", "post_validate"])
+def test_validate_invalid_configuration(hook_name):
+    """ When raising ConanInvalidConfiguration in the pre_validate and post_validate hooks,
+        it should be forwarded and preserve the same exception type.
+    """
+    hook = textwrap.dedent(f"""
+        from conan.errors import ConanInvalidConfiguration
+        def {hook_name}(conanfile):
+            raise ConanInvalidConfiguration("Invalid configuration in {hook_name} hook")
+    """)
+
+    conanfile = textwrap.dedent(f"""
+        from conan import ConanFile
+        from conan.errors import ConanException
+
+        class Pkg(ConanFile):
+            def validate(self):
+                if "{hook_name}" == "pre_validate":
+                    raise ConanException("Should not reach this point")
+    """)
+
+    client = TestClient()
+    hook_path = os.path.join(client.paths.hooks_path, "custom_hooks", "hook_pre_validate.py")
+    client.save({"conanfile.py": conanfile,
+                 hook_path: hook})
+
+    client.run("build . ", assert_error=True)
+    assert f"ERROR: conanfile.py: Invalid ID: Invalid: Invalid configuration in {hook_name} hook" in client.out
+    assert "Should not reach this point" not in client.out
diff --git a/test/integration/graph/conflict_diamond_test.py b/test/integration/graph/conflict_diamond_test.py
index 6f24d54e9..2a0ae5a23 100644
--- a/test/integration/graph/conflict_diamond_test.py
+++ b/test/integration/graph/conflict_diamond_test.py
@@ -68,8 +68,8 @@ class TestConflictDiamondTest:
             c.assert_listed_require({f"math/{v}": "Cache"})
 
         c.run("install --requires=engine/1.0  --requires=ai/1.0", assert_error=True)
-        assert "Conflict between math/1.0.1 and math/1.0 in the graph"
-        assert "Conflict originates from ai/1.0"
+        assert "Conflict between math/1.0.1 and math/1.0 in the graph" in c.out
+        assert "Conflict originates from ai/1.0" in c.out
 
 
 @pytest.mark.parametrize("version_range", [True, False])