diff --git a/conan/api/subapi/local.py b/conan/api/subapi/local.py index ed45ce646a3..160a44a4f2d 100644 --- a/conan/api/subapi/local.py +++ b/conan/api/subapi/local.py @@ -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 + diff --git a/conan/cli/commands/inspect.py b/conan/cli/commands/inspect.py index 3b2262c57a5..31e1980e638 100644 --- a/conan/cli/commands/inspect.py +++ b/conan/cli/commands/inspect.py @@ -1,13 +1,12 @@ -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): @@ -15,7 +14,7 @@ def inspect_text_formatter(data): 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}) @@ -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 diff --git a/conans/model/conan_file.py b/conans/model/conan_file.py index 802738098af..2c19df67002 100644 --- a/conans/model/conan_file.py +++ b/conans/model/conan_file.py @@ -11,6 +11,7 @@ from conans.model.options import Options from conans.model.requires import Requirements +from conans.model.settings import Settings class ConanFile: @@ -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 diff --git a/conans/model/requires.py b/conans/model/requires.py index d2222ffc76b..dc832e0ffb1 100644 --- a/conans/model/requires.py +++ b/conans/model/requires.py @@ -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, @@ -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()] diff --git a/conans/test/integration/command_v2/test_inspect.py b/conans/test/integration/command_v2/test_inspect.py index 3299fcceed5..2a75455cdbe 100644 --- a/conans/test/integration/command_v2/test_inspect.py +++ b/conans/test/integration/command_v2/test_inspect.py @@ -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(): @@ -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: ', + 'label: ', + 'license: ', + '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"