diff --git a/conans/client/cmd/export.py b/conans/client/cmd/export.py index f242fb0adee..2d9852b00b2 100644 --- a/conans/client/cmd/export.py +++ b/conans/client/cmd/export.py @@ -1,3 +1,4 @@ +import ast import os import shutil @@ -9,7 +10,7 @@ from conans.model.manifest import FileTreeManifest from conans.model.scm import SCM from conans.paths import CONAN_MANIFEST, CONANFILE -from conans.util.files import save, rmdir, is_dirty, set_dirty, mkdir +from conans.util.files import save, rmdir, is_dirty, set_dirty, mkdir, load from conans.util.log import logger from conans.search.search import search_recipes @@ -89,7 +90,36 @@ def _capture_export_scm_data(conanfile, conanfile_dir, destination_folder, outpu # Generate the scm_folder.txt file pointing to the src_path src_path = scm.get_repo_root() save(scm_src_file, src_path.replace("\\", "/")) - scm_data.replace_in_file(os.path.join(destination_folder, "conanfile.py")) + _replace_scm_data_in_conanfile(os.path.join(destination_folder, "conanfile.py"), + scm_data) + + +def _replace_scm_data_in_conanfile(conanfile_path, scm_data): + # Parsing and replacing the SCM field + content = load(conanfile_path) + lines = content.splitlines(True) + tree = ast.parse(content) + to_replace = [] + for item in tree.body: + if isinstance(item, ast.ClassDef): + statements = item.body + for i, stmt in enumerate(item.body): + if isinstance(stmt, ast.Assign) and len(stmt.targets) == 1: + if isinstance(stmt.targets[0], ast.Name) and stmt.targets[0].id == "scm": + try: + next_line = statements[i+1].lineno - 1 + except IndexError: + next_line = stmt.lineno + replace = [line for line in lines[(stmt.lineno-1):next_line] + if line.strip()] + to_replace.append("".join(replace).lstrip()) + break + if len(to_replace) != 1: + raise ConanException("The conanfile.py defines more than one class level 'scm' attribute") + + new_text = "scm = " + ",\n ".join(str(scm_data).split(",")) + "\n" + content = content.replace(to_replace[0], new_text) + save(conanfile_path, content) def _export_conanfile(conanfile_path, output, paths, conanfile, conan_ref, keep_source): diff --git a/conans/model/scm.py b/conans/model/scm.py index 84210fc259f..3e123102600 100644 --- a/conans/model/scm.py +++ b/conans/model/scm.py @@ -2,8 +2,6 @@ from conans.client.tools.scm import Git from conans.errors import ConanException -from conans.util.files import load, save -import re class SCMData(object): @@ -37,13 +35,6 @@ def __repr__(self): d = {k: v for k, v in d.items() if v} return json.dumps(d, sort_keys=True) - def replace_in_file(self, path): - content = load(path) - dumps = self.__repr__() - dumps = ",\n ".join(dumps.split(",")) - content = re.sub(r"scm\s*=\s*{[^}]*}", "scm = %s" % dumps, content) - save(path, content) - class SCM(object): def __init__(self, data, repo_folder): diff --git a/conans/test/functional/scm_test.py b/conans/test/functional/scm_test.py index 8916956f95a..11bd500764c 100644 --- a/conans/test/functional/scm_test.py +++ b/conans/test/functional/scm_test.py @@ -123,6 +123,12 @@ def test_auto_git(self): self.assertIn("Getting sources from folder: %s" % curdir, self.client.out) self.assertIn("My file is copied", self.client.out) + # check blank lines are respected in replacement + self.client.run("get lib/0.1@user/channel") + self.assertIn("""} + + def build(self):""", self.client.out) + # Export again but now with absolute reference, so no pointer file is created nor kept git = Git(curdir) self.client.save({"conanfile.py": base.format(url=curdir, revision=git.get_revision())}) diff --git a/conans/test/integration/python_build_test.py b/conans/test/integration/python_build_test.py index 8ad9fdb0f31..8038f1af8b1 100644 --- a/conans/test/integration/python_build_test.py +++ b/conans/test/integration/python_build_test.py @@ -1,5 +1,6 @@ import unittest -from conans.test.utils.tools import TestClient, TestServer +from conans.test.utils.tools import TestClient, TestServer,\ + create_local_git_repo from conans.paths import CONANFILE, BUILD_INFO from conans.util.files import load, save import os @@ -219,6 +220,47 @@ class PkgTest(base.MyConanfileBase): self.assertIn("Pkg/0.1@lasote/testing: Package installed 5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9", client.out) + def reuse_scm_test(self): + client = TestClient() + + conanfile = """from conans import ConanFile +scm = {"type" : "git", + "url" : "somerepo", + "revision" : "auto"} + +class MyConanfileBase(ConanFile): + scm = scm +""" + create_local_git_repo({"conanfile.py": conanfile}, branch="my_release", + folder=client.current_folder) + client.run("export . MyConanfileBase/1.1@lasote/testing") + client.run("get MyConanfileBase/1.1@lasote/testing") + # The global scm is left as-is + self.assertIn("""scm = {"type" : "git", + "url" : "somerepo", + "revision" : "auto"}""", client.out) + # but the class one is replaced + self.assertNotIn("scm = scm", client.out) + self.assertIn(' scm = {"revision":', client.out) + self.assertIn('"type": "git",', client.out) + self.assertIn('"url": "somerepo"', client.out) + + reuse = """from conans import python_requires +base = python_requires("MyConanfileBase/1.1@lasote/testing") +class PkgTest(base.MyConanfileBase): + scm = base.scm + other = 123 + def _my_method(self): + pass +""" + client.save({"conanfile.py": reuse}) + client.run("export . Pkg/0.1@lasote/testing") + client.run("get Pkg/0.1@lasote/testing") + self.assertNotIn("scm = base.scm", client.out) + self.assertIn('scm = {"revision":', client.out) + self.assertIn('"type": "git",', client.out) + self.assertIn('"url": "somerepo"', client.out) + class PythonBuildTest(unittest.TestCase):