Repository: conan-io/conan

Issue:
[feature] Consistent patch handling in conan.tools
When using `apply_conandata_patches`:
- the `base_path` is set relative to source_folder, but only if the patches are versioned.
- the patch can be specified as a patch_string if the patches are not versioned, but trying to do so on versioned patches raises an exception


Discussion:

Resolving patch:
diff --git a/conan/tools/files/patches.py b/conan/tools/files/patches.py
--- a/conan/tools/files/patches.py
+++ b/conan/tools/files/patches.py
@@ -99,13 +99,17 @@ def apply_conandata_patches(conanfile):
     if isinstance(patches, dict):
         assert conanfile.version, "Can only be applied if conanfile.version is already defined"
         entries = patches.get(conanfile.version, [])
-        for it in entries:
-            if not "patch_file" in it:
-                raise ConanException("The 'conandata.yml' file needs a 'patch_file' entry for every"
-                                     " patch to be applied")
+    elif isinstance(patches, Iterable):
+        entries = patches
+    else:
+        return
+    for it in entries:
+        if "patch_file" in it:
             # The patch files are located in the root src
             patch_file = os.path.join(conanfile.folders.base_source, it.pop("patch_file"))
             patch(conanfile, patch_file=patch_file, **it)
-    elif isinstance(patches, Iterable):
-        for it in patches:
+        elif "patch_string" in it:
             patch(conanfile, **it)
+        else:
+            raise ConanException("The 'conandata.yml' file needs a 'patch_file' or 'patch_string'"
+                                 " entry for every patch to be applied")


Test patch:
diff --git a/conans/test/functional/tools/test_files.py b/conans/test/functional/tools/test_files.py
--- a/conans/test/functional/tools/test_files.py
+++ b/conans/test/functional/tools/test_files.py
@@ -23,7 +23,12 @@ def apply(self, strip=0, root=None, fuzz=False):
 def mock_patch_ng(monkeypatch):
     mock = MockPatchset()
 
+    def mock_fromstring(string):
+        mock.string = string
+        return mock
+
     monkeypatch.setattr(patch_ng, "fromfile", lambda _: mock)
+    monkeypatch.setattr(patch_ng, "fromstring", mock_fromstring)
     return mock
 
 
@@ -216,5 +221,64 @@ def build(self):
                  'conandata.yml': conandata_yml})
     client.run('create .', assert_error=True)
 
-    assert "The 'conandata.yml' file needs a 'patch_file' entry for every patch to be applied" \
-           in str(client.out)
+    assert "The 'conandata.yml' file needs a 'patch_file' or 'patch_string' entry for every patch" \
+           " to be applied" in str(client.out)
+
+def test_patch_string_entry(mock_patch_ng):
+    conanfile = textwrap.dedent("""
+        from conans import ConanFile
+        from conan.tools.files import apply_conandata_patches
+
+        class Pkg(ConanFile):
+            name = "mypkg"
+            version = "1.11.0"
+
+            def build(self):
+                apply_conandata_patches(self)
+        """)
+    conandata_yml = textwrap.dedent("""
+        patches:
+          "1.11.0":
+            - patch_string: mock patch data
+              patch_type: string
+    """)
+
+    client = TestClient()
+    client.save({'conanfile.py': conanfile,
+                 'conandata.yml': conandata_yml})
+    client.run('create .')
+
+    assert os.path.exists(mock_patch_ng.apply_args[0])
+    assert mock_patch_ng.apply_args[1:] == (0, False)
+    assert 'mock patch data' == mock_patch_ng.string.decode('utf-8')
+    assert 'mypkg/1.11.0: Apply patch (string)' in str(client.out)
+
+def test_relate_base_path_all_versions(mock_patch_ng):
+    conanfile = textwrap.dedent("""
+        from conans import ConanFile
+        from conan.tools.files import apply_conandata_patches
+
+        class Pkg(ConanFile):
+            name = "mypkg"
+            version = "1.0"
+
+            def layout(self):
+                self.folders.source = "source_subfolder"
+
+            def build(self):
+                apply_conandata_patches(self)
+        """)
+    conandata_yml = textwrap.dedent("""
+        patches:
+          - patch_file: "patches/0001-buildflatbuffers-cmake.patch"
+            base_path: "relative_dir"
+    """)
+
+    client = TestClient()
+    client.save({'conanfile.py': conanfile,
+                 'conandata.yml': conandata_yml})
+    client.run('create .')
+
+    assert mock_patch_ng.apply_args[0].endswith(os.path.join('source_subfolder', "relative_dir"))
+    assert mock_patch_ng.apply_args[1:] == (0, False)
+