Repository: conan-io/conan

Issue:
[feature] conan list: info in packages when passing in a graph file
### What is your suggestion?

Hello!

When passing in a graph file into `conan list -g graph.json --format json`, there is no `info` field (containing e.g. settings) in the output list JSON, which you would otherwise get if you ran `conan list "*:*" --format json`.

As an example, assume the following `conanfile.py`:

``` python
from conan import ConanFile

class ConanRecipe(ConanFile):
    name = "pkg"
    version = "0.0"
    settings = "os", "compiler", "build_type", "arch"
```

Now run the following commands:

``` shell
conan create . --format json > graph.json
conan list -g graph.json --format json
conan list "pkg:*" --format json
```

The first `conan list` command does not contain package info, whereas the second one does. Could the `conan list` command be extended to include the info from the graph file, if provided? The info seems to be present in `graph.json`, so conan should be able to copy it over?

If this feature is added, could it also be included when generating a list file from `conan upload`? I.e.

``` shell
conan upload --list build-list.json -r my-remote --format json > upload-list.json
```

would also contain info in the packages in `upload-list.json`? I realize this second one could potentially require more to implement since there is no guarantee that `build-list.json` but it would be a nice addition nonetheless.

Also, the CLI of conan 2 is very good in my opinion, with the JSON formats that can be passed from command to command, well done! :)

### Have you read the CONTRIBUTING guide?

- [X] I've read the CONTRIBUTING guide


Discussion:
Hi @aander80 

Thanks for your suggestion. I am having a look at it, and I think it makes sense to expand the serialization to include the ``self.info`` information in serialized form too. I will send a exploratory PR shortly

> If this feature is added, could it also be included when generating a list file from conan upload? I.e.

I think this is already happening, could you please double check? The ``conan upload`` is already serializing the ``info``, and it is there in the output
You are correct that `conan upload` is serializing the `info`, I just assumed that it wasn't since I was not getting it in the output, but that was because my input list to `conan upload` did not have the `info`, thanks for pointing that out! :)
Sending a exploratory PR in #15553 

> Also, the CLI of conan 2 is very good in my opinion, with the JSON formats that can be passed from command to command, well done! :)

Thanks very much! We did a huge effort in Conan 2.0, and even if the migration effort is necessary, we strongly believe it is a much better version in many different aspects and it is worth it, thanks very much for appreciating it! 🙂 



Patch:
diff --git a/conan/api/model.py b/conan/api/model.py
index 14983fc1427..70416a2c57b 100644
--- a/conan/api/model.py
+++ b/conan/api/model.py
@@ -128,6 +128,7 @@ def load_graph(graphfile, graph_recipes=None, graph_binaries=None):
             if any(b == "*" or b == binary for b in binaries):
                 cache_list.add_refs([ref])  # Binary listed forces recipe listed
                 cache_list.add_prefs(ref, [pref])
+                cache_list.add_configurations({pref: node["info"]})
         return pkglist
 
 
diff --git a/conans/model/conan_file.py b/conans/model/conan_file.py
index c2d009575f8..554d0938afa 100644
--- a/conans/model/conan_file.py
+++ b/conans/model/conan_file.py
@@ -165,6 +165,8 @@ def serialize(self):
         result["cpp_info"] = self.cpp_info.serialize()
         result["conf_info"] = self.conf_info.serialize()
         result["label"] = self.display_name
+        if self.info is not None:
+            result["info"] = self.info.serialize()
         return result
 
     @property
diff --git a/conans/model/info.py b/conans/model/info.py
index a2b9ee85387..552364dea6d 100644
--- a/conans/model/info.py
+++ b/conans/model/info.py
@@ -168,7 +168,7 @@ def copy(self):
         return RequirementsInfo(data)
 
     def serialize(self):
-        return [str(r) for r in sorted(self._data.values())]
+        return [r.dumps() for r in self._data.values()]
 
     def __bool__(self):
         return bool(self._data)
@@ -246,6 +246,9 @@ def copy(self):
         refs = {r._ref: r.package_id_mode for r in self._refs} if self._refs else None
         return PythonRequiresInfo(refs, self._default_package_id_mode)
 
+    def serialize(self):
+        return [r.dumps() for r in self._refs or []]
+
     def __bool__(self):
         return bool(self._refs)
 
@@ -330,6 +333,32 @@ def clone(self):
         result.settings_target = self.settings_target.copy() if self.settings_target else None
         return result
 
+    def serialize(self):
+        result = {}
+        settings_dumps = self.settings.serialize()
+        if settings_dumps:
+            result["settings"] = settings_dumps
+        if self.settings_target is not None:
+            settings_target_dumps = self.settings_target.serialize()
+            if settings_target_dumps:
+                result["settings_target"] = settings_target_dumps
+        options_dumps = self.options.serialize()
+        if options_dumps:
+            result["options"] = options_dumps
+        requires_dumps = self.requires.serialize()
+        if requires_dumps:
+            result["requires"] = requires_dumps
+        python_requires_dumps = self.python_requires.serialize()
+        if python_requires_dumps:
+            result["python_requires"] = python_requires_dumps
+        build_requires_dumps = self.build_requires.serialize()
+        if build_requires_dumps:
+            result["build_requires"] = build_requires_dumps
+        conf_dumps = self.conf.serialize()
+        if conf_dumps:
+            result["conf"] = conf_dumps
+        return result
+
     def dumps(self):
         """
         Get all the information contained in settings, options, requires,


Test patch:
diff --git a/conans/test/integration/command/info/info_test.py b/conans/test/integration/command/info/info_test.py
index cdb17e8b3df..097a18da101 100644
--- a/conans/test/integration/command/info/info_test.py
+++ b/conans/test/integration/command/info/info_test.py
@@ -202,6 +202,8 @@ class pkg(ConanFile):
         info = json.loads(client.stdout)
         pyrequires = info["graph"]["nodes"]["0"]["python_requires"]
         tool = pyrequires["tool/0.1#4d670581ccb765839f2239cc8dff8fbd"]
+        info = info["graph"]["nodes"]["0"]["info"]
+        assert info["python_requires"] == ["tool/0.1.Z"]
         # lets make sure the path exists
         assert tool["recipe"] == "Cache"
         assert tool["remote"] is None
diff --git a/conans/test/integration/command/upload/upload_test.py b/conans/test/integration/command/upload/upload_test.py
index b5a3115b6ce..7a17a61aa37 100644
--- a/conans/test/integration/command/upload/upload_test.py
+++ b/conans/test/integration/command/upload/upload_test.py
@@ -1,3 +1,4 @@
+import json
 import os
 import platform
 import stat
@@ -534,3 +535,15 @@ def test_upload_list_only_recipe():
     c.run("list --graph=graph.json --format=json", redirect_stdout="installed.json")
     c.run("upload --list=installed.json --only-recipe -r=default -c")
     assert "conan_package.tgz" not in c.out
+
+
+def test_upload_json_output():
+    c = TestClient(default_server_user=True)
+    c.save({"conanfile.py": GenConanfile("liba", "0.1").with_settings("os")
+                                                       .with_shared_option(False)})
+    c.run("create . -s os=Linux")
+    c.run("upload * -r=default -c --format=json")
+    list_pkgs = json.loads(c.stdout)
+    revs = list_pkgs["default"]["liba/0.1"]["revisions"]["a565bd5defd3a99e157698fcc6e23b25"]
+    pkg = revs["packages"]["9e0f8140f0fe6b967392f8d5da9881e232e05ff8"]
+    assert pkg["info"] == {"settings": {"os": "Linux"}, "options": {"shared": "False"}}
diff --git a/conans/test/integration/command_v2/test_combined_pkglist_flows.py b/conans/test/integration/command_v2/test_combined_pkglist_flows.py
index f6b1cd28c36..115f9d1acdc 100644
--- a/conans/test/integration/command_v2/test_combined_pkglist_flows.py
+++ b/conans/test/integration/command_v2/test_combined_pkglist_flows.py
@@ -79,14 +79,20 @@ class TestCreateGraphToPkgList:
     def test_graph_pkg_list_only_built(self):
         c = TestClient()
         c.save({"zlib/conanfile.py": GenConanfile("zlib", "1.0"),
-                "app/conanfile.py": GenConanfile("app", "1.0").with_requires("zlib/1.0")})
+                "app/conanfile.py": GenConanfile("app", "1.0").with_requires("zlib/1.0")
+                                                              .with_settings("os")
+                                                              .with_shared_option(False)})
         c.run("create zlib")
-        c.run("create app --format=json", redirect_stdout="graph.json")
+        c.run("create app --format=json -s os=Windows", redirect_stdout="graph.json")
         c.run("list --graph=graph.json --graph-binaries=build --format=json")
         pkglist = json.loads(c.stdout)["Local Cache"]
         assert len(pkglist) == 1
-        assert len(pkglist["app/1.0"]["revisions"]
-                   ["0fa1ff1b90576bb782600e56df642e19"]["packages"]) == 1
+        pkgs = pkglist["app/1.0"]["revisions"]["8263c3c32802e14a2f03a0b1fcce0d95"]["packages"]
+        assert len(pkgs) == 1
+        pkg_app = pkgs["e0bcc80c3f095670b71e535c193114d0155426cb"]
+        assert pkg_app["info"]["requires"] == ["zlib/1.0.Z"]
+        assert pkg_app["info"]["settings"] == {'os': 'Windows'}
+        assert pkg_app["info"]["options"] == {'shared': 'False'}
 
     def test_graph_pkg_list_all_recipes_only(self):
         """
diff --git a/conans/test/integration/configuration/conf/test_conf_package_id.py b/conans/test/integration/configuration/conf/test_conf_package_id.py
index 8505442109a..f6bd137f620 100644
--- a/conans/test/integration/configuration/conf/test_conf_package_id.py
+++ b/conans/test/integration/configuration/conf/test_conf_package_id.py
@@ -1,3 +1,4 @@
+import json
 import textwrap
 
 import pytest
@@ -32,3 +33,9 @@ def test_package_id(client):
     client.assert_listed_binary({"pkg/0.1": ("7501c16e6c5c93e534dd760829859340e2dc73bc", "Build")})
     client.run("create . --name=pkg --version=0.1 -pr=profile2")
     client.assert_listed_binary({"pkg/0.1": ("4eb2bd276f75d2df8fa0f4b58bd86014b7f51693", "Build")})
+
+
+def test_json_output(client):
+    client.run("create . --name=pkg --version=0.1 -c user.myconf:myitem=1 --format=json")
+    graph = json.loads(client.stdout)
+    assert graph["graph"]["nodes"]["1"]["info"]["conf"] == {'user.myconf:myitem': 1}
diff --git a/conans/test/integration/settings/per_package_settings_test.py b/conans/test/integration/settings/per_package_settings_test.py
index e6ec0b974a5..1eacb36b7d9 100644
--- a/conans/test/integration/settings/per_package_settings_test.py
+++ b/conans/test/integration/settings/per_package_settings_test.py
@@ -1,3 +1,4 @@
+import json
 import textwrap
 import unittest
 
@@ -119,10 +120,14 @@ def package_id(self):
     c.run("create . -s:b arch=x86_64 -s:h arch=armv7 --build-require")
     pkg_id = c.created_package_id("gcc/0.1")
 
-    c.run("install --tool-requires=gcc/0.1 -s:b arch=x86_64 -s arch=armv8 -s:h gcc*:arch=armv7")
+    c.run("install --tool-requires=gcc/0.1 -s:b arch=x86_64 -s arch=armv8 -s:h gcc*:arch=armv7"
+          " --format=json")
     # it will not fail due to armv8, but use the binary for armv7
     c.assert_listed_binary({"gcc/0.1": (pkg_id, "Cache")}, build=True)
 
+    graph = json.loads(c.stdout)
+    assert graph["graph"]["nodes"]["1"]["info"]["settings_target"] == {"arch": "armv7"}
+
 
 def test_per_package_settings_build():
     c = TestClient()