Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve conan inspect output, it now understands set_name/set_version #13716

Merged
merged 11 commits into from
Apr 21, 2023
7 changes: 7 additions & 0 deletions conan/api/subapi/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,10 @@ def test(conanfile):
with conanfile_exception_formatter(conanfile, "test"):
with chdir(conanfile.build_folder):
conanfile.test()

def inspect(self, conanfile_path, remotes, lockfile):
app = ConanApp(self._conan_api.cache_folder)
conanfile = app.loader.load_named(conanfile_path, name=None, version=None,
user=None, channel=None, remotes=remotes, graph_lock=lockfile)
return conanfile

41 changes: 23 additions & 18 deletions conan/cli/commands/inspect.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import inspect as python_inspect
import os

from conan.api.output import cli_out_write
from conan.cli.command import conan_command
from conan.cli.command import conan_command, OnceArgument
from conan.cli.formatters import default_json_formatter


def inspect_text_formatter(data):
for name, value in data.items():
for name, value in sorted(data.items(), key=lambda x: x[0]):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this lambda? Not very intuitive

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While playing with the output I realized it would be insertion-order dependant and implemented a sort on the items to have them show up alphabetically. Maybe we want to have a custom order and just have to be carefurl about when we insert the result in the serialization?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still not sure. sorted(mydict.items()) already sorts by the first element (key), no need for the lambda

if value is None:
continue
if isinstance(value, dict):
cli_out_write(f"{name}:")
for k, v in value.items():
cli_out_write(f" {k}: {v}")
else:
cli_out_write("{}: {}".format(name, value))
cli_out_write("{}: {}".format(name, str(value)))


@conan_command(group="Consumer", formatters={"text": inspect_text_formatter, "json": default_json_formatter})
Expand All @@ -24,20 +23,26 @@ def inspect(conan_api, parser, *args):
Inspect a conanfile.py to return its public fields.
"""
parser.add_argument("path", help="Path to a folder containing a recipe (conanfile.py)")
parser.add_argument("-r", "--remote", default=None, action="append",
help="Remote names. Accepts wildcards ('*' means all the remotes available)")
parser.add_argument("-l", "--lockfile", action=OnceArgument,
help="Path to a lockfile. Use --lockfile=\"\" to avoid automatic use of "
"existing 'conan.lock' file")
parser.add_argument("--lockfile-partial", action="store_true",
help="Do not raise an error if some dependency is not found in lockfile")

args = parser.parse_args(*args)

path = conan_api.local.get_conanfile_path(args.path, os.getcwd(), py=True)

conanfile = conan_api.graph.load_conanfile_class(path)
ret = {}

for name, value in python_inspect.getmembers(conanfile):
if name.startswith('_') or python_inspect.ismethod(value) \
or python_inspect.isfunction(value) or isinstance(value, property):
continue
ret[name] = value
if value is None:
continue

return ret
lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile,
conanfile_path=path,
cwd=os.getcwd(),
partial=args.lockfile_partial)
remotes = conan_api.remotes.list(args.remote) if args.remote else []
conanfile = conan_api.local.inspect(path, remotes=remotes, lockfile=lockfile)
result = conanfile.serialize()
# Some of the serialization info is not initialized so it's pointless to show it to the user
for item in ("cpp_info", "system_requires", "recipe_folder"):
if item in result:
del result[item]

return result
21 changes: 15 additions & 6 deletions conans/model/conan_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from conans.model.options import Options

from conans.model.requires import Requirements
from conans.model.settings import Settings


class ConanFile:
Expand Down Expand Up @@ -121,17 +122,25 @@ def __init__(self, display_name=""):
def serialize(self):
result = {}

for a in ("url", "license", "author", "description", "topics", "homepage", "build_policy",
"upload_policy",
"revision_mode", "provides", "deprecated", "win_bash", "win_bash_run"):
v = getattr(self, a)
for a in ("name", "user", "channel", "url", "license",
"author", "description", "homepage", "build_policy", "upload_policy",
"revision_mode", "provides", "deprecated", "win_bash", "win_bash_run",
"default_options", "options_description", "generators", "requires"):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Serialization of the requires field is a bit uncertain how is being serialized, as it is a Python Requirements object?

v = getattr(self, a, None)
if v is not None:
result[a] = v

if self.version is not None:
result["version"] = str(self.version)
if self.topics is not None:
result["topics"] = list(self.topics)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not happy about this, but some tests were failing when calling graph create for json formatter (test/integration/command/create_test.py:475). I do wonder why now and not with the old changes the also has the topics returned

result["package_type"] = str(self.package_type)
result["settings"] = self.settings.serialize()

settings = self.settings
result["settings"] = settings.serialize() if isinstance(settings, Settings) else settings
result["options"] = self.options.serialize()
result["options_definitions"] = self.options.possible_values

result["requires"] = self.requires.serialize()
if hasattr(self, "python_requires"):
result["python_requires"] = [r.repr_notime() for r in self.python_requires.all_refs()]
result["system_requires"] = self.system_requires
Expand Down
10 changes: 10 additions & 0 deletions conans/model/requires.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,13 @@ def __str__(self):
self.visible)
return "{}, Traits: {}".format(self.ref, traits)

def serialize(self):
return {attribute: getattr(self, attribute) for attribute in ("run", "libs", "skip",
AbrilRBS marked this conversation as resolved.
Show resolved Hide resolved
"test", "force", "direct",
"transitive_headers", "build",
"transitive_libs", "headers",
"package_id_mode", "visible")}

def copy_requirement(self):
return Requirement(self.ref, headers=self.headers, libs=self.libs, build=self.build,
run=self.run, visible=self.visible,
Expand Down Expand Up @@ -554,3 +561,6 @@ def tool_require(self, ref, raise_if_duplicated=True, package_id_mode=None, visi

def __repr__(self):
return repr(self._requires.values())

def serialize(self):
return [v.serialize() for v in self._requires.values()]
125 changes: 117 additions & 8 deletions conans/test/integration/command_v2/test_inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@ def test_basic_inspect():
t.save({"foo/conanfile.py": GenConanfile().with_name("foo").with_shared_option()})
t.run("inspect foo/conanfile.py")
lines = t.out.splitlines()
assert lines == ["default_options:",
" shared: False",

assert lines == ['default_options:',
' shared: False',
'generators: []',
'label: ',
'name: foo',
'no_copy_source: False',
"options:",
" shared: [True, False]",
'revision_mode: hash',
]
'options:',
' shared: False',
'options_definitions:',
" shared: ['True', 'False']",
'package_type: None',
'requires: []',
'revision_mode: hash']


def test_options_description():
Expand Down Expand Up @@ -51,3 +53,110 @@ def test_dot_and_folder_conanfile():
t.save({"foo/conanfile.py": GenConanfile().with_name("foo")}, clean_first=True)
t.run("inspect foo")
assert 'name: foo' in t.out


def test_inspect_understands_setname():
tc = TestClient()
conanfile = textwrap.dedent("""
from conan import ConanFile

class Pkg(ConanFile):
settings = "os", "arch"
def set_name(self):
self.name = "foo"

def set_version(self):
self.version = "1.0"
""")

tc.save({"conanfile.py": conanfile})
tc.run("inspect .")
assert "foo" in tc.out
assert "1.0" in tc.out


def test_normal_inspect():
tc = TestClient()
tc.run("new basic -d name=pkg -d version=1.0")
tc.run("inspect .")
assert tc.out.splitlines() == ['description: A basic recipe',
'generators: []',
'homepage: <Your project homepage goes here>',
'label: ',
'license: <Your project license goes here>',
'name: pkg',
'options:',
'options_definitions:',
'package_type: None',
'requires: []',
'revision_mode: hash',
'version: 1.0']


def test_requiremens_inspect():
tc = TestClient()
conanfile = textwrap.dedent("""
from conan import ConanFile

class Pkg(ConanFile):
requires = "zlib/1.2.13"
""")
tc.save({"conanfile.py": conanfile})
tc.run("inspect .")
assert ['generators: []',
'label: ',
'options:',
'options_definitions:',
'package_type: None',
"requires: [{'ref': zlib/1.2.13, 'run': False, 'libs': True, 'skip': False, "
"'test': False, 'force': False, 'direct': True, 'transitive_headers': None, "
"'build': False, 'transitive_libs': None, 'headers': True, 'package_id_mode': "
"None, 'visible': True}]",
'revision_mode: hash'] == tc.out.splitlines()


def test_pythonrequires_remote():
tc = TestClient(default_server_user=True)
pyrequires = textwrap.dedent("""
from conan import ConanFile

class MyBase:
def set_name(self):
self.name = "my_company_package"

class PyReq(ConanFile):
name = "pyreq"
version = "1.0"
package_type = "python-require"
""")
tc.save({"pyreq/conanfile.py": pyrequires})
tc.run("create pyreq/")
tc.run("upload pyreq/1.0 -r default")
tc.run("search * -r default")
assert "pyreq/1.0" in tc.out
tc.run("remove * -c")
conanfile = textwrap.dedent("""
from conan import ConanFile

class Pkg(ConanFile):
python_requires = "pyreq/1.0"
python_requires_extend = "pyreq.MyBase"

def set_version(self):
self.version = "1.0"
""")
tc.save({"conanfile.py": conanfile})
tc.run("inspect . -r default")
assert "name: my_company_package" in tc.out
assert "version: 1.0" in tc.out


def test_serializable_inspect():
tc = TestClient()
tc.save({"conanfile.py": GenConanfile("a", "1.0")
.with_requires("b/2.0")
.with_setting("os")
.with_option("shared", [True, False])
.with_generator("CMakeDeps")})
tc.run("inspect . --format=json")
assert json.loads(tc.out)["name"] == "a"