Repository: conan-io/conan

Issue:
Circular reference on conan install --json if using multi configuration packages
While creating a multi configuration package (mainly for windows / Visual Studio + cmake) i want to use conan install --json to protocol the exact version installed of the package for versioning purposes. This is meant to be used to recreate the versioned environment at a later point in time.

Following https://github.com/memsharded/hello_multi_config I defined:
```
def package_info(self):
    self.cpp_info.release.libs = ["Conan_TestLibrary"]
    self.cpp_info.debug.libs = ["Conan_TestLibrary_d"]
```
after doing this conan install --json gives me the following error:

```
conan install Conan_TestLibrary/1.0.0@tlk/stable -g cmake --json test.json
Configuration:
[settings]
arch=x86_64
arch_build=x86_64
build_type=Release
compiler=Visual Studio
compiler.runtime=MD
compiler.version=15
os=Windows
os_build=Windows
[options]
[build_requires]
[env]

Conan_TestLibrary/1.0.0@tlk/stable: Installing package
Requirements
    Conan_TestLibrary/1.0.0@tlk/stable from local cache - Cache
Packages
    Conan_TestLibrary/1.0.0@tlk/stable:647cbb6309b429634f119fb1a6f066f9cca806ce - Cache

Conan_TestLibrary/1.0.0@tlk/stable: Already installed!
Conan_TestLibrary/1.0.0@tlk/stable: Generator cmake created conanbuildinfo.cmake
Conan_TestLibrary/1.0.0@tlk/stable: Generator txt created conanbuildinfo.txt
Traceback (most recent call last):
  File "c:\users\dirks\anaconda3\lib\site-packages\conans\client\command.py", line 1427, in run
    method(args[0][1:])
  File "c:\users\dirks\anaconda3\lib\site-packages\conans\client\command.py", line 399, in install
    self._outputer.json_output(info, args.json, cwd)
  File "c:\users\dirks\anaconda3\lib\site-packages\conans\client\conan_command_output.py", line 67, in json_output
    save(json_output, json.dumps(info, default=date_handler))
  File "c:\users\dirks\anaconda3\lib\json\__init__.py", line 238, in dumps
    **kw).encode(obj)
  File "c:\users\dirks\anaconda3\lib\json\encoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "c:\users\dirks\anaconda3\lib\json\encoder.py", line 257, in iterencode
    return _iterencode(o, 0)
ValueError: Circular reference detected

ERROR: Circular reference detected
```
Experimenting a bit with this it seems like accessing self.cpp_info.release.libs adds a config to self.cpp_info which is referencing a python object. Below is self.cpp_info.__dict__:
```
{
    'includedirs': ['include'], 
    'srcdirs': [], 
    'libdirs': ['lib'], 
    'resdirs': ['res'], 
    'bindirs': ['bin'], 
    'builddirs': [''], 
    'rootpaths': [], 
    'libs': [], 
    'defines': [], 
    'cflags': [], 
    'cppflags': [], 
    'sharedlinkflags': [], 
    'exelinkflags': [], 
    'rootpath': 'C:\\Users\\DirkS\\.conan\\data\\Conan_TestLibrary\\1.0.0\\tlk\\stable\\package\\647cbb6309b429634f119fb1a6f066f9cca806ce', 
    'sysroot': '', 
    '_include_paths': None, 
    '_lib_paths': None, 
    '_bin_paths': None, 
    '_build_paths': None, 
    '_res_paths': None, 
    '_src_paths': None, 
    'version': '1.0.0', 
    'description': ' ', 
    'public_deps': [], 
    'configs': {
        'release': <conans.model.build_info._CppInfo object at 0x0000028303264AC8>, 
        'debug': <conans.model.build_info._CppInfo object at 0x0000028303264EB8>
        }
    }
```
My current guess is that --json can not handle this object reference and thus fails. I expect this to be solvable by giving json.dumps a custom encoder.

I'm using Conan version 1.11.2 on Windows 10 64-bit

Steps to reproduce:
- define conan package,
- define package_info as shown above
- create package
- try to install package with --json

I'd apreciate any ideas to work around this issue for the time beeing. 

Edit: Added new line in code block, "{" was not showing.


Discussion:
Thanks for reporting, yes, looks like a bug. Let's try to solve it for the next release.

Resolving patch:
diff --git a/conans/client/conan_command_output.py b/conans/client/conan_command_output.py
--- a/conans/client/conan_command_output.py
+++ b/conans/client/conan_command_output.py
@@ -69,7 +69,10 @@ def json_output(self, info, json_output, cwd):
             json_output = os.path.join(cwd, json_output)
 
         def date_handler(obj):
-            return obj.isoformat() if hasattr(obj, 'isoformat') else obj
+            if hasattr(obj, 'isoformat'):
+                return obj.isoformat()
+            else:
+                raise TypeError("Unserializable object {} of type {}".format(obj, type(obj)))
 
         save(json_output, json.dumps(info, default=date_handler))
         self.user_io.out.writeln("")
diff --git a/conans/client/recorder/action_recorder.py b/conans/client/recorder/action_recorder.py
--- a/conans/client/recorder/action_recorder.py
+++ b/conans/client/recorder/action_recorder.py
@@ -22,6 +22,23 @@
 INSTALL_ERROR_BUILDING = "building"
 
 
+def _cpp_info_to_dict(cpp_info):
+    doc = {}
+    for it, value in vars(cpp_info).items():
+        if it.startswith("_") or not value:
+            continue
+
+        if it == "configs":
+            configs_data = {}
+            for cfg_name, cfg_cpp_info in value.items():
+                configs_data[cfg_name] = _cpp_info_to_dict(cfg_cpp_info)
+            doc["configs"] = configs_data
+            continue
+
+        doc[it] = value
+    return doc
+
+
 class Action(namedtuple("Action", "type, doc, time")):
 
     def __new__(cls, the_type, doc=None):
@@ -97,12 +114,7 @@ def package_cpp_info(self, pref, cpp_info):
         assert isinstance(pref, PackageReference)
         pref = pref.copy_clear_rev()
         # assert isinstance(cpp_info, CppInfo)
-        doc = {}
-        for it, value in vars(cpp_info).items():
-            if it.startswith("_") or not value:
-                continue
-            doc[it] = value
-        self._inst_packages_info[pref]['cpp_info'] = doc
+        self._inst_packages_info[pref]['cpp_info'] = _cpp_info_to_dict(cpp_info)
 
     @property
     def install_errored(self):


Test patch:
diff --git a/conans/test/functional/command/json_output_test.py b/conans/test/functional/command/json_output_test.py
--- a/conans/test/functional/command/json_output_test.py
+++ b/conans/test/functional/command/json_output_test.py
@@ -1,7 +1,9 @@
 import json
 import os
+import textwrap
 import unittest
 
+from conans.model.build_info import DEFAULT_LIB
 from conans.test.utils.cpp_test_files import cpp_hello_conan_files
 from conans.test.utils.tools import TestClient, TestServer
 from conans.util.files import load, save
@@ -169,3 +171,43 @@ def build_requirements(self):
         self.assertFalse(my_json["installed"][2]["recipe"]["downloaded"])
         self.assertFalse(my_json["installed"][2]["packages"][0]["downloaded"])
         self.assertFalse(my_json["installed"][2]["packages"][1]["downloaded"])
+
+    def test_json_create_multiconfig(self):
+        conanfile = textwrap.dedent("""
+            from conans import ConanFile
+            
+            class Lib(ConanFile):
+                def package_info(self):
+                    self.cpp_info.release.libs = ["hello"]
+                    self.cpp_info.debug.libs = ["hello_d"]
+                    
+                    self.cpp_info.debug.libdirs = ["lib-debug"]
+                    
+            """)
+        self.client.save({'conanfile.py': conanfile})
+        self.client.run("create . name/version@user/channel --json=myfile.json")
+        my_json = load(os.path.join(self.client.current_folder, "myfile.json"))
+        my_json = json.loads(my_json)
+
+        # Nodes with cpp_info
+        cpp_info = my_json["installed"][0]["packages"][0]["cpp_info"]
+        cpp_info_debug = cpp_info["configs"]["debug"]
+        cpp_info_release = cpp_info["configs"]["release"]
+
+        # Each node should have its own information
+        self.assertFalse("libs" in cpp_info)
+        self.assertEqual(cpp_info_debug["libs"], ["hello_d"])
+        self.assertEqual(cpp_info_release["libs"], ["hello"])
+        self.assertEqual(cpp_info_debug["libdirs"], ["lib-debug"])
+        self.assertEqual(cpp_info_release["libdirs"], [DEFAULT_LIB])
+
+        # FIXME: There are _empty_ nodes
+        self.assertEqual(cpp_info_debug["builddirs"], [""])
+        self.assertEqual(cpp_info_release["builddirs"], [""])
+
+        # FIXME: Default information is duplicated in all the nodes
+        dupe_nodes = ["rootpath", "includedirs", "resdirs",
+                      "bindirs", "builddirs", "filter_empty"]
+        for dupe in dupe_nodes:
+            self.assertEqual(cpp_info[dupe], cpp_info_debug[dupe])
+            self.assertEqual(cpp_info[dupe], cpp_info_release[dupe])