Repository: conan-io/conan

Issue:
conan 1.4.x fails when "scm" attribute in recipe is not a dict
Hi @lasote and @memsharded ,

at first of all thanks for implementing the first version of scm feature. Sadly I was unable to review it completely before the 1.4.0 release.

The current implementation fails (without changing our recipes) because we also have an "scm" attribute but which is an instance of our "private" SCM implementation.

The traceback is:

```
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/dist-packages/conan-1.4.2-py2.7.egg/conans/client/command.py", line 1182, in run
    method(args[0][1:])
  File "/usr/local/lib/python2.7/dist-packages/conan-1.4.2-py2.7.egg/conans/client/command.py", line 246, in create
    test_build_folder=args.test_build_folder)
  File "/usr/local/lib/python2.7/dist-packages/conan-1.4.2-py2.7.egg/conans/client/conan_api.py", line 77, in wrapper
    return f(*args, **kwargs)
  File "/usr/local/lib/python2.7/dist-packages/conan-1.4.2-py2.7.egg/conans/client/conan_api.py", line 310, in create
    self._user_io.out, self._client_cache)
  File "/usr/local/lib/python2.7/dist-packages/conan-1.4.2-py2.7.egg/conans/client/cmd/export.py", line 61, in cmd_export
    _export_conanfile(conanfile_path, output, client_cache, conanfile, conan_ref, keep_source)
  File "/usr/local/lib/python2.7/dist-packages/conan-1.4.2-py2.7.egg/conans/client/cmd/export.py", line 136, in _export_conanfile
    output, paths, conan_ref)
  File "/usr/local/lib/python2.7/dist-packages/conan-1.4.2-py2.7.egg/conans/client/cmd/export.py", line 107, in _capture_export_scm_data
    scm = get_scm(conanfile, src_path)
  File "/usr/local/lib/python2.7/dist-packages/conan-1.4.2-py2.7.egg/conans/client/source.py", line 18, in get_scm
    return SCM(data, src_folder)
  File "/usr/local/lib/python2.7/dist-packages/conan-1.4.2-py2.7.egg/conans/model/scm.py", line 13, in __init__
    self.type = data.get("type")
AttributeError: 'property' object has no attribute 'get'
```

The reason is that there is no dict instance check implemented in `get_scm()` but IMHO it has to. Something like the following should be enough:

```
def get_scm(conanfile, src_folder):
    data = getattr(conanfile, "scm", None)
    if data is not None and isinstance(data, dict):
        return SCM(data, src_folder)
    else:
        # not an instance of dict or None, skip SCM feature.
        pass
```

Would be nice if you can fix this as soon as possible.

Thanks in advance.
Best Aalmann


---
To help us debug your issue please explain:

- [x ] I've read the [CONTRIBUTING guide](https://raw.githubusercontent.com/conan-io/conan/develop/.github/CONTRIBUTING.md).
- [ x] I've specified the Conan version, operating system version and any tool that can be relevant.
- [ x] I've explained the steps to reproduce the error or the motivation/use case of the question/suggestion.


Discussion:

Patch:
diff --git a/conans/client/graph/graph.py b/conans/client/graph/graph.py
index a406e6e6f..2fb467e2d 100644
--- a/conans/client/graph/graph.py
+++ b/conans/client/graph/graph.py
@@ -141,6 +141,20 @@ class DepsGraph(object):
         open_nodes = nodes_by_level[1]
         return open_nodes
 
+    def full_closure(self, node):
+        closure = OrderedDict()
+        current = node.neighbors()
+        while current:
+            new_current = []
+            for n in current:
+                closure[n] = n
+            for n in current:
+                for neigh in n.public_neighbors():
+                    if neigh not in new_current and neigh not in closure:
+                        new_current.append(neigh)
+            current = new_current
+        return closure
+
     def closure(self, node):
         closure = OrderedDict()
         current = node.neighbors()
diff --git a/conans/client/installer.py b/conans/client/installer.py
index 12860cdf6..054a42858 100644
--- a/conans/client/installer.py
+++ b/conans/client/installer.py
@@ -414,12 +414,13 @@ class ConanInstaller(object):
     @staticmethod
     def _propagate_info(node, levels, deps_graph):
         # Get deps_cpp_info from upstream nodes
-        closure = deps_graph.closure(node)
+        closure = deps_graph.full_closure(node)
         node_order = []
         for level in levels:
-            for n in closure.values():
+            for n in closure:
                 if n in level:
                     node_order.append(n)
+
         conan_file = node.conanfile
         for n in node_order:
             conan_file.deps_cpp_info.update(n.conanfile.cpp_info, n.conan_ref.name)
diff --git a/conans/client/source.py b/conans/client/source.py
index 8559983cf..7fdd38099 100644
--- a/conans/client/source.py
+++ b/conans/client/source.py
@@ -14,8 +14,11 @@ from conans.util.files import rmdir, set_dirty, is_dirty, clean_dirty, mkdir
 
 def get_scm(conanfile, src_folder):
     data = getattr(conanfile, "scm", None)
-    if data is not None:
+    if data is not None and isinstance(data, dict):
         return SCM(data, src_folder)
+    else:
+        # not an instance of dict or None, skip SCM feature.
+        pass
 
 
 def complete_recipe_sources(remote_manager, client_cache, registry, conanfile, conan_reference):


Test patch:
diff --git a/conans/test/functional/scm_test.py b/conans/test/functional/scm_test.py
index 0fc72c328..2420e5b44 100644
--- a/conans/test/functional/scm_test.py
+++ b/conans/test/functional/scm_test.py
@@ -40,6 +40,20 @@ class SCMTest(unittest.TestCase):
         self.client.runner("git add .", cwd=self.client.current_folder)
         self.client.runner('git commit -m  "commiting"', cwd=self.client.current_folder)
 
+    def test_scm_other_type_ignored(self):
+        conanfile = '''
+from conans import ConanFile, tools
+
+class ConanLib(ConanFile):
+    name = "lib"
+    version = "0.1"
+    scm = ["Other stuff"]
+
+'''
+        self.client.save({"conanfile.py": conanfile})
+        # nothing breaks
+        self.client.run("export . user/channel")
+
     def test_repeat_clone_changing_subfolder(self):
         tmp = '''
 from conans import ConanFile, tools
diff --git a/conans/test/integration/order_libs_test.py b/conans/test/integration/order_libs_test.py
index a2f0ec268..7f9d37845 100644
--- a/conans/test/integration/order_libs_test.py
+++ b/conans/test/integration/order_libs_test.py
@@ -10,6 +10,62 @@ class OrderLibsTest(unittest.TestCase):
     def setUp(self):
         self.client = TestClient()
 
+    def private_order_test(self):
+        # https://github.com/conan-io/conan/issues/3006
+        client = TestClient()
+        conanfile = """from conans import ConanFile
+class LibBConan(ConanFile):
+    def package_info(self):
+        self.cpp_info.libs = ["LibC"]
+"""
+        client.save({"conanfile.py": conanfile})
+        client.run("create . LibC/0.1@user/channel")
+
+        conanfile = """from conans import ConanFile
+class LibCConan(ConanFile):
+    requires = "LibC/0.1@user/channel"
+    def package_info(self):
+        self.cpp_info.libs = ["LibB"]
+"""
+        client.save({"conanfile.py": conanfile})
+        client.run("create . LibB/0.1@user/channel")
+
+        conanfile = """from conans import ConanFile
+class LibCConan(ConanFile):
+    requires = ("LibB/0.1@user/channel", "private"), "LibC/0.1@user/channel"
+"""
+        client.save({"conanfile.py": conanfile})
+        client.run("install . -g cmake")
+        conanbuildinfo = load(os.path.join(client.current_folder, "conanbuildinfo.cmake"))
+        self.assertIn("set(CONAN_LIBS LibB LibC ${CONAN_LIBS})", conanbuildinfo)
+        # Change private
+        conanfile = """from conans import ConanFile
+class LibCConan(ConanFile):
+    requires = "LibB/0.1@user/channel", ("LibC/0.1@user/channel", "private")
+"""
+        client.save({"conanfile.py": conanfile})
+        client.run("install . -g cmake")
+        conanbuildinfo = load(os.path.join(client.current_folder, "conanbuildinfo.cmake"))
+        self.assertIn("set(CONAN_LIBS LibB LibC ${CONAN_LIBS})", conanbuildinfo)
+        # Change order
+        conanfile = """from conans import ConanFile
+class LibCConan(ConanFile):
+    requires = ("LibC/0.1@user/channel", "private"), "LibB/0.1@user/channel"
+"""
+        client.save({"conanfile.py": conanfile})
+        client.run("install . -g cmake")
+        conanbuildinfo = load(os.path.join(client.current_folder, "conanbuildinfo.cmake"))
+        self.assertIn("set(CONAN_LIBS LibB LibC ${CONAN_LIBS})", conanbuildinfo)
+        # Change order
+        conanfile = """from conans import ConanFile
+class LibCConan(ConanFile):
+    requires = "LibC/0.1@user/channel", ("LibB/0.1@user/channel", "private")
+"""
+        client.save({"conanfile.py": conanfile})
+        client.run("install . -g cmake")
+        conanbuildinfo = load(os.path.join(client.current_folder, "conanbuildinfo.cmake"))
+        self.assertIn("set(CONAN_LIBS LibB LibC ${CONAN_LIBS})", conanbuildinfo)
+
     def _export(self, name, deps=None, export=True):
         def _libs():
             if name == "LibPNG":