Repository: conan-io/conan
Pull request #16847 (2024-08-20 12:13:19)

Improve error when accessing cpp_info shorthand methods.
Changelog: Fix: Improve error when accessing `cpp_info` shorthand methods.
Docs: Omit

Fix: #16796


Patch:
diff --git a/conans/client/rest_client_local_recipe_index.py b/conans/client/rest_client_local_recipe_index.py
index 1da0de5fc..e806fe3fe 100644
--- a/conans/client/rest_client_local_recipe_index.py
+++ b/conans/client/rest_client_local_recipe_index.py
@@ -11,7 +11,8 @@ from conan.api.output import ConanOutput
 from conan.internal.cache.home_paths import HomePaths
 from conans.client.cmd.export import cmd_export
 from conans.client.loader import ConanFileLoader
-from conans.errors import ConanException, PackageNotFoundException, RecipeNotFoundException
+from conans.errors import ConanException, PackageNotFoundException, RecipeNotFoundException, \
+    ConanReferenceDoesNotExistInDB, NotFoundException
 from conans.model.conf import ConfDefinition
 from conans.model.recipe_ref import RecipeReference
 from conans.util.files import load, save, rmdir, copytree_compat
@@ -76,7 +77,12 @@ class RestApiClientLocalRecipesIndex:
         return self._copy_files(export_folder, dest_folder)
 
     def get_recipe_sources(self, ref, dest_folder):
-        export_sources = self._app.cache.recipe_layout(ref).export_sources()
+        try:
+            export_sources = self._app.cache.recipe_layout(ref).export_sources()
+        except ConanReferenceDoesNotExistInDB as e:
+            # This can happen when there a local-recipes-index is being queried for sources it
+            # doesn't contain
+            raise NotFoundException(str(e))
         return self._copy_files(export_sources, dest_folder)
 
     def get_package(self, pref, dest_folder, metadata, only_metadata):
diff --git a/conans/model/build_info.py b/conans/model/build_info.py
index fa50ed170..34a9d5c05 100644
--- a/conans/model/build_info.py
+++ b/conans/model/build_info.py
@@ -194,22 +194,28 @@ class _Component:
     @property
     def bindir(self):
         bindirs = self.bindirs
-        assert bindirs
-        assert len(bindirs) == 1
+        if not bindirs or len(bindirs) != 1:
+            raise ConanException(f"The bindir property is undefined because bindirs "
+                                 f"{'is empty' if not bindirs else 'has more than one element'}."
+                                 f" Consider using the bindirs property.")
         return bindirs[0]
 
     @property
     def libdir(self):
         libdirs = self.libdirs
-        assert libdirs
-        assert len(libdirs) == 1
+        if not libdirs or len(libdirs) != 1:
+            raise ConanException(f"The libdir property is undefined because libdirs "
+                                 f"{'is empty' if not libdirs else 'has more than one element'}."
+                                 f" Consider using the libdirs property.")
         return libdirs[0]
 
     @property
     def includedir(self):
         includedirs = self.includedirs
-        assert includedirs
-        assert len(includedirs) == 1
+        if not includedirs or len(includedirs) != 1:
+            raise ConanException(f"The includedir property is undefined because includedirs "
+                                 f"{'is empty' if not includedirs else 'has more than one element'}."
+                                 f" Consider using the includedirs property.")
         return includedirs[0]
 
     @property


Test patch:
diff --git a/test/functional/test_local_recipes_index.py b/test/functional/test_local_recipes_index.py
index fd01a7963..2a3df3a07 100644
--- a/test/functional/test_local_recipes_index.py
+++ b/test/functional/test_local_recipes_index.py
@@ -118,3 +118,35 @@ class TestInRepo:
         # Finally lets remove the remote, check that the clone is cleared
         c.run('remote remove local')
         assert "Removing temporary files for 'local' local-recipes-index remote" in c.out
+
+    def test_not_found(self):
+        """testing that the correct exception is raised when a recipe is not found
+        """
+        repo1_folder = temp_folder()
+        repo2_folder = temp_folder()
+        config_yml = textwrap.dedent("""\
+            versions:
+              "0.1":
+                folder: all
+            """)
+        conanfile = textwrap.dedent("""\
+            import os
+            from conan import ConanFile
+            from conan.tools.files import copy
+
+            class PkgRecipe(ConanFile):
+                name = "pkg"
+
+                def export_sources(self):
+                    copy(self, "*", src=self.recipe_folder, dst=self.export_sources_folder)
+            """)
+
+        save_files(repo2_folder, {"recipes/pkg/config.yml": config_yml,
+                                  "recipes/pkg/all/conanfile.py": conanfile,
+                                  "recipes/pkg/all/pkg.h": gen_function_h(name="pkg")})
+
+        c = TestClient()
+        c.run(f"remote add local1 '{repo1_folder}'")
+        c.run(f"remote add local2 '{repo2_folder}'")
+        c.run("install --requires=pkg/0.1 --build=missing")
+        assert "Install finished successfully" in c.out
diff --git a/test/integration/conanfile/conanfile_errors_test.py b/test/integration/conanfile/conanfile_errors_test.py
index 028c974ba..c10dc1c0b 100644
--- a/test/integration/conanfile/conanfile_errors_test.py
+++ b/test/integration/conanfile/conanfile_errors_test.py
@@ -192,3 +192,27 @@ def test_requirements_change_options():
     c.save({"conanfile.py": conanfile})
     c.run("create .", assert_error=True)
     assert "ERROR: hello/0.1: Dependencies options were defined incorrectly." in c.out
+
+
+@pytest.mark.parametrize("property_name", ["libdir", "bindir", "includedir"])
+@pytest.mark.parametrize("property_content", [[], ["mydir1", "mydir2"]])
+def test_shorthand_bad_interface(property_name, property_content):
+    c = TestClient(light=True)
+    conanfile = textwrap.dedent(f"""
+        from conan import ConanFile
+
+        class HelloConan(ConanFile):
+            name = "hello"
+            version = "0.1"
+
+            def package_info(self):
+                self.cpp_info.{property_name}s = {property_content}
+                self.output.info(self.cpp_info.{property_name})
+        """)
+    c.save({"conanfile.py": conanfile})
+    c.run("create .", assert_error=True)
+    if property_content:
+        assert f"The {property_name} property is undefined because {property_name}s has more than one element." in c.out
+    else:
+        assert f"The {property_name} property is undefined because {property_name}s is empty." in c.out
+