diff --git a/conans/client/cmd/copy.py b/conans/client/cmd/copy.py index 0ef69afc00a..f964cd40629 100644 --- a/conans/client/cmd/copy.py +++ b/conans/client/cmd/copy.py @@ -9,7 +9,7 @@ def _prepare_sources(cache, ref, remote_manager, loader, remotes): conan_file_path = cache.package_layout(ref).conanfile() - conanfile = loader.load_class(conan_file_path) + conanfile = loader.load_basic(conan_file_path) complete_recipe_sources(remote_manager, cache, conanfile, ref, remotes) return conanfile.short_paths diff --git a/conans/client/cmd/download.py b/conans/client/cmd/download.py index 9d651132b74..2a6f89dad2e 100644 --- a/conans/client/cmd/download.py +++ b/conans/client/cmd/download.py @@ -21,7 +21,7 @@ def download(app, ref, package_ids, remote, recipe, recorder, remotes): metadata.recipe.remote = remote.name conan_file_path = cache.package_layout(ref).conanfile() - conanfile = loader.load_class(conan_file_path) + conanfile = loader.load_basic(conan_file_path) # Download the sources too, don't be lazy complete_recipe_sources(remote_manager, cache, conanfile, ref, remotes) diff --git a/conans/client/cmd/uploader.py b/conans/client/cmd/uploader.py index a8ac9011baf..c198561636a 100644 --- a/conans/client/cmd/uploader.py +++ b/conans/client/cmd/uploader.py @@ -132,7 +132,7 @@ def _collect_packages_to_upload(self, refs, confirm, remotes, all_packages, quer if upload: try: conanfile_path = self._cache.package_layout(ref).conanfile() - conanfile = self._loader.load_class(conanfile_path) + conanfile = self._loader.load_basic(conanfile_path) except NotFoundException: raise NotFoundException(("There is no local conanfile exported as %s" % str(ref))) diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py index b42cfc3d788..a2911263483 100644 --- a/conans/client/conan_api.py +++ b/conans/client/conan_api.py @@ -261,17 +261,14 @@ def inspect(self, path, attributes, remote_name=None): ref = ConanFileReference.loads(path) except ConanException: conanfile_path = _get_conanfile_path(path, get_cwd(), py=True) - ref = os.path.basename(conanfile_path) - conanfile_class = self.app.loader.load_class(conanfile_path) + conanfile = self.app.loader.load_basic(conanfile_path) else: update = True if remote_name else False result = self.app.proxy.get_recipe(ref, update, update, remotes, ActionRecorder()) conanfile_path, _, _, ref = result - conanfile_class = self.app.loader.load_class(conanfile_path) - conanfile_class.name = ref.name - conanfile_class.version = ref.version - - conanfile = conanfile_class(self.app.out, None, repr(ref)) + conanfile = self.app.loader.load_basic(conanfile_path) + conanfile.name = ref.name + conanfile.version = ref.version result = OrderedDict() if not attributes: @@ -1054,8 +1051,7 @@ def export_alias(self, reference, target_reference): # Do not allow to override an existing package alias_conanfile_path = self.app.cache.package_layout(ref).conanfile() if os.path.exists(alias_conanfile_path): - conanfile_class = self.app.loader.load_class(alias_conanfile_path) - conanfile = conanfile_class(self.app.out, None, repr(ref)) + conanfile = self.app.loader.load_basic(alias_conanfile_path) if not getattr(conanfile, 'alias', None): raise ConanException("Reference '{}' is already a package, remove it before " "creating and alias with the same name".format(ref)) @@ -1162,7 +1158,7 @@ def editable_add(self, path, reference, layout, cwd): # Check the conanfile is there, and name/version matches ref = ConanFileReference.loads(reference, validate=True) - target_conanfile = self.app.graph_manager._loader.load_class(target_path) + target_conanfile = self.app.loader.load_basic(target_path) if (target_conanfile.name and target_conanfile.name != ref.name) or \ (target_conanfile.version and target_conanfile.version != ref.version): raise ConanException("Name and version from reference ({}) and target " diff --git a/conans/client/graph/graph_manager.py b/conans/client/graph/graph_manager.py index 55fe1814904..4bd02e9c4db 100644 --- a/conans/client/graph/graph_manager.py +++ b/conans/client/graph/graph_manager.py @@ -143,7 +143,7 @@ def _inject_require(conanfile, ref): if graph_info.root.name is None: # If the graph_info information is not there, better get what we can from # the conanfile - conanfile = self._loader.load_class(path) + conanfile = self._loader.load_basic(path) graph_info.root = ConanFileReference(graph_info.root.name or conanfile.name, graph_info.root.version or conanfile.version, graph_info.root.user, diff --git a/conans/client/loader.py b/conans/client/loader.py index f06e87e898f..87359b2809f 100644 --- a/conans/client/loader.py +++ b/conans/client/loader.py @@ -10,7 +10,8 @@ from conans.client.generators import registered_generators from conans.client.loader_txt import ConanFileTextLoader from conans.client.tools.files import chdir -from conans.errors import ConanException, NotFoundException, ConanInvalidConfiguration +from conans.errors import ConanException, NotFoundException, ConanInvalidConfiguration, \ + conanfile_exception_formatter from conans.model.conan_file import ConanFile from conans.model.conan_generator import Generator from conans.model.options import OptionsValues @@ -27,12 +28,15 @@ def __init__(self, runner, output, python_requires): self._output = output self._python_requires = python_requires sys.modules["conans"].python_requires = python_requires - self.cached_conanfiles = {} + self._cached_conanfile_classes = {} - def load_class(self, conanfile_path, lock_python_requires=None): - cached = self.cached_conanfiles.get(conanfile_path) + def load_basic(self, conanfile_path, lock_python_requires=None, user=None, channel=None, + display=""): + """ loads a conanfile basic object without evaluating anything + """ + cached = self._cached_conanfile_classes.get(conanfile_path) if cached and cached[1] == lock_python_requires: - return cached[0] + return cached[0](self._output, self._runner, display, user, channel) if lock_python_requires is not None: self._python_requires.locked_versions = {r.name: r for r in lock_python_requires} @@ -42,11 +46,11 @@ def load_class(self, conanfile_path, lock_python_requires=None): self._python_requires.valid = False self._python_requires.locked_versions = None - self.cached_conanfiles[conanfile_path] = (conanfile, lock_python_requires) + self._cached_conanfile_classes[conanfile_path] = (conanfile, lock_python_requires) conanfile.conan_data = self._load_data(conanfile_path) - return conanfile + return conanfile(self._output, self._runner, display, user, channel) except ConanException as e: raise ConanException("Error loading conanfile at '{}': {}".format(conanfile_path, e)) @@ -63,29 +67,49 @@ def _load_data(conanfile_path): return data or {} - def load_export(self, conanfile_path, name, version, user, channel, lock_python_requires=None): - conanfile = self.load_class(conanfile_path, lock_python_requires) + def load_named(self, conanfile_path, name, version, user, channel, lock_python_requires=None): + """ loads the basic conanfile object and evaluates its name and version + """ + conanfile = self.load_basic(conanfile_path, lock_python_requires, user, channel) + + if hasattr(conanfile, "set_name"): + if conanfile.name: + raise ConanException("Conanfile defined package 'name', set_name() redundant") + with conanfile_exception_formatter("conanfile.py", "set_name"): + conanfile.set_name() + if hasattr(conanfile, "set_version"): + if conanfile.version: + raise ConanException("Conanfile defined package 'version', set_version() redundant") + with conanfile_exception_formatter("conanfile.py", "set_version"): + conanfile.set_version() # Export does a check on existing name & version - if "name" in conanfile.__dict__: - if name and name != conanfile.name: - raise ConanException("Package recipe exported with name %s!=%s" - % (name, conanfile.name)) - elif not name: - raise ConanException("conanfile didn't specify name") - else: + if name: + if conanfile.name and name != conanfile.name: + raise ConanException("Package recipe with name %s!=%s" % (name, conanfile.name)) conanfile.name = name - if "version" in conanfile.__dict__: - if version and version != conanfile.version: - raise ConanException("Package recipe exported with version %s!=%s" + if version: + if conanfile.version and version != conanfile.version: + raise ConanException("Package recipe with version %s!=%s" % (version, conanfile.version)) - elif not version: - raise ConanException("conanfile didn't specify version") - else: conanfile.version = version + return conanfile + + def load_export(self, conanfile_path, name, version, user, channel, lock_python_requires=None): + """ loads the conanfile and evaluates its name, version, and enforce its existence + """ + conanfile = self.load_named(conanfile_path, name, version, user, channel, + lock_python_requires) + if not conanfile.name: + raise ConanException("conanfile didn't specify name") + if not conanfile.version: + raise ConanException("conanfile didn't specify version") + ref = ConanFileReference(conanfile.name, conanfile.version, user, channel) - return conanfile(self._output, self._runner, str(ref), ref.user, ref.channel) + conanfile.display_name = str(ref) + conanfile.output.scope = conanfile.display_name + return conanfile @staticmethod def _initialize_conanfile(conanfile, processed_profile): @@ -96,6 +120,7 @@ def _initialize_conanfile(conanfile, processed_profile): if package_settings_values: pkg_settings = package_settings_values.get(conanfile.name) if pkg_settings is None: + # FIXME: This seems broken for packages without user/channel ref = "%s/%s@%s/%s" % (conanfile.name, conanfile.version, conanfile._conan_user, conanfile._conan_channel) for pattern, settings in package_settings_values.items(): @@ -109,25 +134,20 @@ def _initialize_conanfile(conanfile, processed_profile): def load_consumer(self, conanfile_path, processed_profile, name=None, version=None, user=None, channel=None, test=None, lock_python_requires=None): - - conanfile_class = self.load_class(conanfile_path, lock_python_requires) - if name and conanfile_class.name and name != conanfile_class.name: - raise ConanException("Package recipe name %s!=%s" % (name, conanfile_class.name)) - if version and conanfile_class.version and version != conanfile_class.version: - raise ConanException("Package recipe version %s!=%s" - % (version, conanfile_class.version)) - conanfile_class.name = name or conanfile_class.name - conanfile_class.version = version or conanfile_class.version + """ loads a conanfile.py in user space. Might have name/version or not + """ + conanfile = self.load_named(conanfile_path, name, version, user, channel, + lock_python_requires) if test: - display_name = "%s (test package)" % str(test) + conanfile.display_name = "%s (test package)" % str(test) else: - ref = ConanFileReference(conanfile_class.name, conanfile_class.version, user, channel, + ref = ConanFileReference(conanfile.name, conanfile.version, user, channel, validate=False) if str(ref): - display_name = "%s (%s)" % (os.path.basename(conanfile_path), str(ref)) + conanfile.display_name = "%s (%s)" % (os.path.basename(conanfile_path), str(ref)) else: - display_name = os.path.basename(conanfile_path) - conanfile = conanfile_class(self._output, self._runner, display_name, user, channel) + conanfile.display_name = os.path.basename(conanfile_path) + conanfile.output.scope = conanfile.display_name conanfile.in_local_cache = False try: self._initialize_conanfile(conanfile, processed_profile) @@ -146,10 +166,14 @@ def load_consumer(self, conanfile_path, processed_profile, name=None, version=No raise ConanException("%s: %s" % (conanfile_path, str(e))) def load_conanfile(self, conanfile_path, processed_profile, ref, lock_python_requires=None): - conanfile_class = self.load_class(conanfile_path, lock_python_requires) - conanfile_class.name = ref.name - conanfile_class.version = ref.version - conanfile = conanfile_class(self._output, self._runner, str(ref), ref.user, ref.channel) + """ load a conanfile with a full reference, name, version, user and channel are obtained + from the reference, not evaluated. Main way to load from the cache + """ + conanfile = self.load_basic(conanfile_path, lock_python_requires, ref.user, ref.channel, + str(ref)) + conanfile.name = ref.name + conanfile.version = ref.version + if processed_profile.dev_reference and processed_profile.dev_reference == ref: conanfile.develop = True try: diff --git a/conans/test/functional/command/create_test.py b/conans/test/functional/command/create_test.py index fccb6d08fb4..c4dcbf33ba6 100644 --- a/conans/test/functional/command/create_test.py +++ b/conans/test/functional/command/create_test.py @@ -313,9 +313,9 @@ class TestConan(ConanFile): client.save({"conanfile.py": conanfile}) client.run("create . Hello/1.2@lasote/stable") client.run("create ./ Pkg/1.2@lasote/stable", assert_error=True) - self.assertIn("ERROR: Package recipe exported with name Pkg!=Hello", client.out) + self.assertIn("ERROR: Package recipe with name Pkg!=Hello", client.out) client.run("create . Hello/1.1@lasote/stable", assert_error=True) - self.assertIn("ERROR: Package recipe exported with version 1.1!=1.2", client.out) + self.assertIn("ERROR: Package recipe with version 1.1!=1.2", client.out) def create_user_channel_test(self): client = TestClient() diff --git a/conans/test/functional/command/export_pkg_test.py b/conans/test/functional/command/export_pkg_test.py index 0e8bc1fa907..5e0ef5e9c2b 100644 --- a/conans/test/functional/command/export_pkg_test.py +++ b/conans/test/functional/command/export_pkg_test.py @@ -415,10 +415,10 @@ def package(self): # Specify different name or version is not working client.run("export-pkg . lib/1.0@conan/stable -f", assert_error=True) - self.assertIn("ERROR: Package recipe exported with name lib!=Hello", client.out) + self.assertIn("ERROR: Package recipe with name lib!=Hello", client.out) client.run("export-pkg . Hello/1.1@conan/stable -f", assert_error=True) - self.assertIn("ERROR: Package recipe exported with version 1.1!=0.1", client.out) + self.assertIn("ERROR: Package recipe with version 1.1!=0.1", client.out) conanfile = """ from conans import ConanFile diff --git a/conans/test/functional/command/install_test.py b/conans/test/functional/command/install_test.py index c9e3ad03871..0a78b2517b8 100644 --- a/conans/test/functional/command/install_test.py +++ b/conans/test/functional/command/install_test.py @@ -70,7 +70,7 @@ def build(self): client.save({"conanfile.py": conanfile}) # passing the wrong package name raises client.run("install . Pkg/0.1@myuser/testing", assert_error=True) - self.assertIn("ERROR: Package recipe name Pkg!=Other", client.out) + self.assertIn("ERROR: Package recipe with name Pkg!=Other", client.out) # Partial reference works client.run("install . 0.1@myuser/testing") client.run("build .") @@ -85,7 +85,7 @@ def build(self): client.save({"conanfile.py": conanfile}) # passing the wrong package name raises client.run("install . Other/0.1@myuser/testing", assert_error=True) - self.assertIn("ERROR: Package recipe version 0.1!=0.2", client.out) + self.assertIn("ERROR: Package recipe with version 0.1!=0.2", client.out) # Partial reference works client.run("install . myuser/testing") client.run("build .") diff --git a/conans/test/functional/conanfile/get_name_version_test.py b/conans/test/functional/conanfile/get_name_version_test.py new file mode 100644 index 00000000000..b2ae58e1a17 --- /dev/null +++ b/conans/test/functional/conanfile/get_name_version_test.py @@ -0,0 +1,107 @@ +import textwrap +import unittest + +from parameterized import parameterized + +from conans.test.utils.tools import TestClient + + +class GetVersionNameTest(unittest.TestCase): + + @parameterized.expand([("", ), ("@user/channel", )]) + def set_version_name_test(self, user_channel): + client = TestClient() + conanfile = textwrap.dedent(""" + from conans import ConanFile + class Lib(ConanFile): + def set_name(self): + self.name = "pkg" + def set_version(self): + self.version = "2.1" + """) + client.save({"conanfile.py": conanfile}) + client.run("export . %s" % user_channel) + self.assertIn("pkg/2.1%s: A new conanfile.py version was exported" % user_channel, + client.out) + # installing it doesn't break + client.run("install pkg/2.1%s --build=missing" % (user_channel or "@")) + self.assertIn("pkg/2.1%s:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 - Build" % user_channel, + client.out) + client.run("install pkg/2.1%s --build=missing" % (user_channel or "@")) + self.assertIn("pkg/2.1%s:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 - Cache" % user_channel, + client.out) + + # Local flow should also work + client.run("install .") + self.assertIn("conanfile.py (pkg/2.1): Installing package", client.out) + + def set_version_name_file_test(self): + client = TestClient() + conanfile = textwrap.dedent(""" + from conans import ConanFile, load + class Lib(ConanFile): + def set_name(self): + self.name = load("name.txt") + def set_version(self): + self.version = load("version.txt") + """) + client.save({"conanfile.py": conanfile, + "name.txt": "pkg", + "version.txt": "2.1"}) + client.run("export . user/testing") + self.assertIn("pkg/2.1@user/testing: A new conanfile.py version was exported", client.out) + client.run("install pkg/2.1@user/testing --build=missing") + self.assertIn("pkg/2.1@user/testing:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 - Build", + client.out) + client.run("install pkg/2.1@user/testing") + self.assertIn("pkg/2.1@user/testing:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 - Cache", + client.out) + # Local flow should also work + client.run("install .") + self.assertIn("conanfile.py (pkg/2.1): Installing package", client.out) + + def set_version_name_errors_test(self): + client = TestClient() + conanfile = textwrap.dedent(""" + from conans import ConanFile + class Lib(ConanFile): + def set_name(self): + self.name = "pkg" + def set_version(self): + self.version = "2.1" + """) + client.save({"conanfile.py": conanfile}) + client.run("export . other/1.1@user/testing", assert_error=True) + self.assertIn("ERROR: Package recipe with name other!=pkg", client.out) + client.run("export . 1.1@user/testing", assert_error=True) + self.assertIn("ERROR: Package recipe with version 1.1!=2.1", client.out) + # These are checked but match and don't conflict + client.run("export . 2.1@user/testing") + client.run("export . pkg/2.1@user/testing") + + # Local flow should also fail + client.run("install . other/1.2@", assert_error=True) + self.assertIn("ERROR: Package recipe with name other!=pkg", client.out) + + def set_version_name_crash_test(self): + client = TestClient() + conanfile = textwrap.dedent(""" + from conans import ConanFile + class Lib(ConanFile): + def set_name(self): + self.name = error + """) + client.save({"conanfile.py": conanfile}) + client.run("export .", assert_error=True) + self.assertIn("ERROR: conanfile.py: Error in set_name() method, line 5", client.out) + self.assertIn("name 'error' is not defined", client.out) + conanfile = textwrap.dedent(""" + from conans import ConanFile + class Lib(ConanFile): + def set_version(self): + self.version = error + """) + client.save({"conanfile.py": conanfile}) + client.run("export .", assert_error=True) + self.assertIn("ERROR: conanfile.py: Error in set_version() method, line 5", client.out) + self.assertIn("name 'error' is not defined", client.out) diff --git a/conans/test/functional/python_requires/python_requires_test.py b/conans/test/functional/python_requires/python_requires_test.py index 65a85fce683..2cb1d776400 100644 --- a/conans/test/functional/python_requires/python_requires_test.py +++ b/conans/test/functional/python_requires/python_requires_test.py @@ -423,6 +423,67 @@ def build(self): self.assertTrue(os.path.exists(os.path.join(client.cache.package_layout(ref).export(), "other.txt"))) + def reuse_name_version_test(self): + client = TestClient() + conanfile = textwrap.dedent(""" + from conans import ConanFile + class BasePkg(ConanFile): + name = "Base" + version = "1.1" + """) + client.save({"conanfile.py": conanfile}) + client.run("export . lasote/testing") + + reuse = textwrap.dedent(""" + from conans import python_requires + base = python_requires("Base/1.1@lasote/testing") + class PkgTest(base.BasePkg): + pass + """) + client.save({"conanfile.py": reuse}) + client.run("create . Pkg/0.1@lasote/testing", assert_error=True) + self.assertIn("ERROR: Package recipe with name Pkg!=Base", client.out) + + reuse = textwrap.dedent(""" + from conans import python_requires + base = python_requires("Base/1.1@lasote/testing") + class PkgTest(base.BasePkg): + name = "Pkg" + version = "0.1" + """) + client.save({"conanfile.py": reuse}) + client.run("create . Pkg/0.1@lasote/testing") + self.assertIn("Pkg/0.1@lasote/testing: Created package ", client.out) + client.run("create . lasote/testing") + self.assertIn("Pkg/0.1@lasote/testing: Created package ", client.out) + + def reuse_set_name_set_version_test(self): + client = TestClient() + conanfile = textwrap.dedent(""" + from conans import ConanFile, load + class BasePkg(ConanFile): + def set_name(self): + self.name = load("name.txt") + def set_version(self): + self.version = load("version.txt") + """) + client.save({"conanfile.py": conanfile, + "name.txt": "Base", + "version.txt": "1.1"}) + client.run("export . user/testing") + + reuse = textwrap.dedent(""" + from conans import python_requires + base = python_requires("Base/1.1@user/testing") + class PkgTest(base.BasePkg): + pass + """) + client.save({"conanfile.py": reuse, + "name.txt": "Pkg", + "version.txt": "2.3"}) + client.run("create . user/testing") + self.assertIn("Pkg/2.3@user/testing: Created package", client.out) + def reuse_exports_conflict_test(self): conanfile = """from conans import ConanFile class Base(ConanFile): diff --git a/conans/test/integration/case_sensitive_test.py b/conans/test/integration/case_sensitive_test.py index 1ea1035bcb1..fb476b20410 100644 --- a/conans/test/integration/case_sensitive_test.py +++ b/conans/test/integration/case_sensitive_test.py @@ -69,7 +69,7 @@ def package_test(self): client.run("install Hello0/0.1@lasote/stable --build=missing") error = client.run("export-pkg . hello0/0.1@lasote/stable", assert_error=True) self.assertTrue(error) - self.assertIn("ERROR: Package recipe exported with name hello0!=Hello0", client.out) + self.assertIn("ERROR: Package recipe with name hello0!=Hello0", client.out) def copy_test(self): client = TestClient() diff --git a/conans/test/unittests/client/conanfile_loader_test.py b/conans/test/unittests/client/conanfile_loader_test.py index caa392bcc9a..8e309746b51 100644 --- a/conans/test/unittests/client/conanfile_loader_test.py +++ b/conans/test/unittests/client/conanfile_loader_test.py @@ -40,7 +40,7 @@ class BasePackage(ConanFile): """ save(conanfile_path, conanfile) save(os.path.join(tmp_dir, "base_recipe.py"), base_recipe) - conan_file = loader.load_class(conanfile_path) + conan_file = loader.load_basic(conanfile_path) self.assertEqual(conan_file.short_paths, True) result = loader.load_consumer(conanfile_path, diff --git a/conans/test/unittests/client/graph/version_ranges_graph_test.py b/conans/test/unittests/client/graph/version_ranges_graph_test.py index 08c66b5bf46..15c1edc1654 100644 --- a/conans/test/unittests/client/graph/version_ranges_graph_test.py +++ b/conans/test/unittests/client/graph/version_ranges_graph_test.py @@ -47,7 +47,7 @@ def setUp(self): self.retriever.save_recipe(say_ref, say_content) def build_graph(self, content, update=False): - self.loader.cached_conanfiles = {} + self.loader._cached_conanfile_classes = {} processed_profile = test_processed_profile() root_conan = self.retriever.root(str(content), processed_profile) deps_graph = self.builder.load_graph(root_conan, update, update, self.remotes, diff --git a/conans/test/unittests/model/transitive_reqs_test.py b/conans/test/unittests/model/transitive_reqs_test.py index d8744a8084f..5b75d267da5 100644 --- a/conans/test/unittests/model/transitive_reqs_test.py +++ b/conans/test/unittests/model/transitive_reqs_test.py @@ -90,7 +90,7 @@ def setUp(self): self.binaries_analyzer = GraphBinariesAnalyzer(cache, self.output, self.remote_manager) def build_graph(self, content, options="", settings=""): - self.loader.cached_conanfiles = {} + self.loader._cached_conanfile_classes = {} full_settings = Settings.loads(default_settings_yml) full_settings.values = Values.loads(settings) profile = Profile()