Repository: conan-io/conan
Pull request #16848 (2024-08-20 12:51:58)

Add `check_type` on `componet's` `get_property`
Changelog: Feature: Add `check_type` on `componet's` `get_property`
Docs: TODO

Close: #16042

Additional work will come in a follow up PR, this implements the basic feature.
- Add checks in the rest of `get_property`
- Add support for this feat on places like cmakedeps



memsharded: > Add checks in the rest of get_property

Probably not necessary to add a ``check_type=str`` for all of them, I'd add just those that are not strings



Patch:
diff --git a/conan/tools/cmake/cmakedeps/templates/targets.py b/conan/tools/cmake/cmakedeps/templates/targets.py
index f20b0bf44..66ecff223 100644
--- a/conan/tools/cmake/cmakedeps/templates/targets.py
+++ b/conan/tools/cmake/cmakedeps/templates/targets.py
@@ -26,7 +26,7 @@ class TargetsTemplate(CMakeDepsFileTemplate):
         target_pattern += "{}-Target-*.cmake".format(self.file_name)
 
         cmake_target_aliases = self.conanfile.cpp_info.\
-            get_property("cmake_target_aliases") or dict()
+            get_property("cmake_target_aliases", check_type=list) or dict()
 
         target = self.root_target_name
         cmake_target_aliases = {alias: target for alias in cmake_target_aliases}
@@ -36,7 +36,7 @@ class TargetsTemplate(CMakeDepsFileTemplate):
             if comp_name is not None:
                 aliases = \
                     self.conanfile.cpp_info.components[comp_name].\
-                    get_property("cmake_target_aliases") or dict()
+                    get_property("cmake_target_aliases", check_type=list) or dict()
 
                 target = self.get_component_alias(self.conanfile, comp_name)
                 cmake_component_target_aliases[comp_name] = {alias: target for alias in aliases}
diff --git a/conan/tools/gnu/pkgconfigdeps.py b/conan/tools/gnu/pkgconfigdeps.py
index 3e7436787..e7747dc86 100644
--- a/conan/tools/gnu/pkgconfigdeps.py
+++ b/conan/tools/gnu/pkgconfigdeps.py
@@ -301,7 +301,7 @@ class _PCGenerator:
 
     @staticmethod
     def _get_package_aliases(dep):
-        pkg_aliases = dep.cpp_info.get_property("pkg_config_aliases")
+        pkg_aliases = dep.cpp_info.get_property("pkg_config_aliases", check_type=list)
         return pkg_aliases or []
 
     def _get_component_aliases(self, dep, comp_name):
@@ -312,7 +312,7 @@ class _PCGenerator:
             raise ConanException("Component '{name}::{cname}' not found in '{name}' "
                                  "package requirement".format(name=dep.ref.name,
                                                               cname=comp_name))
-        comp_aliases = dep.cpp_info.components[comp_name].get_property("pkg_config_aliases")
+        comp_aliases = dep.cpp_info.components[comp_name].get_property("pkg_config_aliases", check_type=list)
         return comp_aliases or []
 
     def _get_package_name(self, dep):
diff --git a/conans/model/build_info.py b/conans/model/build_info.py
index fa50ed170..536da5ca8 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
@@ -334,11 +340,15 @@ class _Component:
             self._properties = {}
         self._properties[property_name] = value
 
-    def get_property(self, property_name):
+    def get_property(self, property_name, check_type=None):
         if self._properties is None:
             return None
         try:
-            return self._properties[property_name]
+            value = self._properties[property_name]
+            if check_type is not None and not isinstance(value, check_type):
+                raise ConanException(
+                    f'The expected type for {property_name} is "{check_type.__name__}", but "{type(value).__name__}" was found')
+            return value
         except KeyError:
             pass
 


Test patch:
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
+
diff --git a/test/integration/test_components_error.py b/test/integration/test_components_error.py
index 07943ddd9..fd78c4ea2 100644
--- a/test/integration/test_components_error.py
+++ b/test/integration/test_components_error.py
@@ -53,3 +53,18 @@ def test_component_error():
     arch = c.get_default_host_profile().settings['arch']
     assert 'list(APPEND t2_FIND_DEPENDENCY_NAMES )' in c.load(f"t3/t2-release-{arch}-data.cmake")
     assert not os.path.exists(os.path.join(c.current_folder, "t3/t1-config.cmake"))
+
+def test_verify_get_property_check_type():
+    c = TestClient(light=True)
+    conanfile = textwrap.dedent("""
+        from conan import ConanFile
+        class HelloConan(ConanFile):
+            name = "hello"
+            version = "0.1"
+            def package_info(self):
+                self.cpp_info.set_property("test_property", "foo")
+                self.cpp_info.get_property("test_property", check_type=list)
+        """)
+    c.save({"conanfile.py": conanfile})
+    c.run("create .", assert_error=True)
+    assert 'The expected type for test_property is "list", but "str" was found' in c.out