Skip to content

Commit

Permalink
Improve conan inspect output, it now understands set_name/`set_ve…
Browse files Browse the repository at this point in the history
…rsion` (#13716)

* Initial inspect sketch

* Different approaches

* Go for serialize(). Sort shown keys. Serialize user, name, version, default_options and options_description

* Add more info to inspect

* Make topics serialize as a list, helps inspect and graph in their json formatters

* Allow partial lockfiles

* Add remote python_require(_extend) test

* Fix tests

* Add requires serialization

* More tests, serialize ref in requirements

* No need to specify key
  • Loading branch information
AbrilRBS authored Apr 21, 2023
1 parent 8dac8b7 commit 944fdfc
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 32 deletions.
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()):
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
28 changes: 22 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,32 @@ 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",):
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)
result["package_type"] = str(self.package_type)
result["settings"] = self.settings.serialize()

settings = self.settings
if settings is not None:
result["settings"] = settings.serialize() if isinstance(settings, Settings) else list(settings)

result["options"] = self.options.serialize()
result["options_definitions"] = self.options.possible_values

if self.generators is not None:
result["generators"] = list(self.generators)
if self.license is not None:
result["license"] = list(self.license) if not isinstance(self.license, str) else self.license

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
9 changes: 9 additions & 0 deletions conans/model/requires.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ def __str__(self):
self.visible)
return "{}, Traits: {}".format(self.ref, traits)

def serialize(self):
serializable = ("ref", "run", "libs", "skip", "test", "force", "direct", "build",
"transitive_headers", "transitive_libs", "headers",
"package_id_mode", "visible")
return {attribute: str(getattr(self, attribute)) for attribute in serializable}

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 +560,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()]
147 changes: 139 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,132 @@ 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_empty_inspect():
conanfile = textwrap.dedent("""
from conan import ConanFile
class Pkg(ConanFile):
pass""")
tc = TestClient()
tc.save({"conanfile.py": conanfile})
tc.run("inspect . -f json")


def test_basic_new_inspect():
tc = TestClient()
tc.run("new basic")
tc.run("inspect . -f json")

tc.run("new cmake_lib -d name=pkg -d version=1.0 -f")
tc.run("inspect . -f json")


def test_requiremens_inspect():
tc = TestClient()
conanfile = textwrap.dedent("""
from conan import ConanFile
class Pkg(ConanFile):
requires = "zlib/1.2.13"
license = "MIT", "Apache"
""")
tc.save({"conanfile.py": conanfile})
tc.run("inspect .")
assert ['generators: []',
'label: ',
"license: ['MIT', 'Apache']",
'options:',
'options_definitions:',
'package_type: None',
"requires: [{'ref': 'zlib/1.2.13', 'run': 'False', 'libs': 'True', 'skip': "
"'False', 'test': 'False', 'force': 'False', 'direct': 'True', 'build': "
"'False', 'transitive_headers': 'None', '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"

0 comments on commit 944fdfc

Please sign in to comment.