Repository: conan-io/conan

Issue:
conan remote remove fails with editable packages
Trying to update the metadata, it fails because the `PackageEditableLayout` has no `update_metadata()`. The main questions are:
- Should the editable packages still have the metadata methods available? 
or 
- Should we force always to retrieve the `PackageCacheLayout` sometimes, e.g from the `remote_registry.py`?

```
Traceback (most recent call last):                                                                                                                                                                                                                                             
  File "/home/luism/workspace/conan_sources/conans/client/command.py", line 1832, in run                                                                                                                                                                                       
    method(args[0][1:])                                                                                                                                                                                                                                                        
  File "/home/luism/workspace/conan_sources/conans/client/command.py", line 1423, in remote                                                                                                                                                                                    
    return self._conan.remote_remove(remote_name)                                                                                                                                                                                                                              
  File "/home/luism/workspace/conan_sources/conans/client/conan_api.py", line 77, in wrapper                                                                                                                                                                                   
    return f(*args, **kwargs)
  File "/home/luism/workspace/conan_sources/conans/client/conan_api.py", line 922, in remote_remove
    return self._cache.registry.remove(remote_name)
  File "/home/luism/workspace/conan_sources/conans/client/cache/remote_registry.py", line 301, in remove
    with self._cache.package_layout(ref).update_metadata() as metadata:
AttributeError: 'PackageEditableLayout' object has no attribute 'update_metadata'
```


Discussion:
@memsharded I would like to know what do you think about this.
I think that so far it doesn't make sense, as metadata are revisions (we don't have them while editable) and remote (same). If the package is in editable mode, I think doesn't make much sense to return PackageCacheLayout for it, so far the PacakgeEditableLayout have placeholder methods to provide better error messages.
Agree. But then what, how to solve this issue? Should the `remote remove` just ignore the editable layouts? When the package is no longer in "editable mode" it will have metadata with a wrong remote (removed).
I see, you are right.

I think there are 2 approaches:

- Explicit force to get the cache metadata, like: ``with self._cache.package_layout(ref, from-cache=True).update_metadata() as metadata:``
- Explicit disabling of ``editables``, like in the ``RemoteRegistry.remove`` make cache.editables = {} and at the end of the function restore it.

I think at the moment I prefer the later one, until we have more cases.

Resolving patch:
diff --git a/conans/client/cache/editable.py b/conans/client/cache/editable.py
--- a/conans/client/cache/editable.py
+++ b/conans/client/cache/editable.py
@@ -1,5 +1,6 @@
 import json
 import os
+from contextlib import contextmanager
 from os.path import join, normpath
 
 from conans.model.ref import ConanFileReference
@@ -49,3 +50,14 @@ def remove(self, ref):
 
     def override(self, workspace_edited):
         self._edited_refs = workspace_edited
+
+    @contextmanager
+    def disable_editables(self):
+        """
+        Temporary disable editables, if we want to make operations on the cache, as updating
+        remotes in packages metadata.
+        """
+        edited_refs = self._edited_refs
+        self._edited_refs = {}
+        yield
+        self._edited_refs = edited_refs
diff --git a/conans/client/cache/remote_registry.py b/conans/client/cache/remote_registry.py
--- a/conans/client/cache/remote_registry.py
+++ b/conans/client/cache/remote_registry.py
@@ -284,13 +284,14 @@ def add(self, remote_name, url, verify_ssl=True, insert=None, force=None):
         renamed = remotes.add(remote_name, url, verify_ssl, insert, force)
         remotes.save(self._filename)
         if renamed:
-            for ref in self._cache.all_refs():
-                with self._cache.package_layout(ref).update_metadata() as metadata:
-                    if metadata.recipe.remote == renamed:
-                        metadata.recipe.remote = remote_name
-                    for pkg_metadata in metadata.packages.values():
-                        if pkg_metadata.remote == renamed:
-                            pkg_metadata.remote = remote_name
+            with self._cache.editable_packages.disable_editables():
+                for ref in self._cache.all_refs():
+                    with self._cache.package_layout(ref).update_metadata() as metadata:
+                        if metadata.recipe.remote == renamed:
+                            metadata.recipe.remote = remote_name
+                        for pkg_metadata in metadata.packages.values():
+                            if pkg_metadata.remote == renamed:
+                                pkg_metadata.remote = remote_name
 
     def update(self, remote_name, url, verify_ssl=True, insert=None):
         self._validate_url(url)
@@ -301,52 +302,54 @@ def update(self, remote_name, url, verify_ssl=True, insert=None):
     def clear(self):
         remotes = self.load_remotes()
         remotes.clear()
-        for ref in self._cache.all_refs():
-            with self._cache.package_layout(ref).update_metadata() as metadata:
-                metadata.recipe.remote = None
-                for pkg_metadata in metadata.packages.values():
-                    pkg_metadata.remote = None
-        remotes.save(self._filename)
+        with self._cache.editable_packages.disable_editables():
+            for ref in self._cache.all_refs():
+                with self._cache.package_layout(ref).update_metadata() as metadata:
+                    metadata.recipe.remote = None
+                    for pkg_metadata in metadata.packages.values():
+                        pkg_metadata.remote = None
+            remotes.save(self._filename)
 
     def remove(self, remote_name):
         remotes = self.load_remotes()
         del remotes[remote_name]
+        with self._cache.editable_packages.disable_editables():
+            for ref in self._cache.all_refs():
+                with self._cache.package_layout(ref).update_metadata() as metadata:
+                    if metadata.recipe.remote == remote_name:
+                        metadata.recipe.remote = None
+                    for pkg_metadata in metadata.packages.values():
+                        if pkg_metadata.remote == remote_name:
+                            pkg_metadata.remote = None
 
-        for ref in self._cache.all_refs():
-            with self._cache.package_layout(ref).update_metadata() as metadata:
-                if metadata.recipe.remote == remote_name:
-                    metadata.recipe.remote = None
-                for pkg_metadata in metadata.packages.values():
-                    if pkg_metadata.remote == remote_name:
-                        pkg_metadata.remote = None
-
-        remotes.save(self._filename)
+            remotes.save(self._filename)
 
     def define(self, remotes):
         # For definition from conan config install
-        for ref in self._cache.all_refs():
-            with self._cache.package_layout(ref).update_metadata() as metadata:
-                if metadata.recipe.remote not in remotes:
-                    metadata.recipe.remote = None
-                for pkg_metadata in metadata.packages.values():
-                    if pkg_metadata.remote not in remotes:
-                        pkg_metadata.remote = None
+        with self._cache.editable_packages.disable_editables():
+            for ref in self._cache.all_refs():
+                with self._cache.package_layout(ref).update_metadata() as metadata:
+                    if metadata.recipe.remote not in remotes:
+                        metadata.recipe.remote = None
+                    for pkg_metadata in metadata.packages.values():
+                        if pkg_metadata.remote not in remotes:
+                            pkg_metadata.remote = None
 
-        remotes.save(self._filename)
+            remotes.save(self._filename)
 
     def rename(self, remote_name, new_remote_name):
         remotes = self.load_remotes()
         remotes.rename(remote_name, new_remote_name)
+        with self._cache.editable_packages.disable_editables():
+            for ref in self._cache.all_refs():
+                with self._cache.package_layout(ref).update_metadata() as metadata:
+                    if metadata.recipe.remote == remote_name:
+                        metadata.recipe.remote = new_remote_name
+                    for pkg_metadata in metadata.packages.values():
+                        if pkg_metadata.remote == remote_name:
+                            pkg_metadata.remote = new_remote_name
 
-        for ref in self._cache.all_refs():
-            with self._cache.package_layout(ref).update_metadata() as metadata:
-                if metadata.recipe.remote == remote_name:
-                    metadata.recipe.remote = new_remote_name
-                for pkg_metadata in metadata.packages.values():
-                    if pkg_metadata.remote == remote_name:
-                        pkg_metadata.remote = new_remote_name
-
-        remotes.save(self._filename)
+            remotes.save(self._filename)
 
     @property
     def refs_list(self):


Test patch:
diff --git a/conans/test/functional/command/remote_test.py b/conans/test/functional/command/remote_test.py
--- a/conans/test/functional/command/remote_test.py
+++ b/conans/test/functional/command/remote_test.py
@@ -392,3 +392,43 @@ def test_invalid_url(self):
                       self.client.user_io.out)
         self.client.run("remote list")
         self.assertIn("pepe.org", self.client.out)
+
+    def test_metadata_editable_packages(self):
+        """
+        Check that 'conan remote' commands work with editable packages
+        """
+        self.client.save({"conanfile.py": """from conans import ConanFile
+class Conan(ConanFile):
+    pass"""})
+        self.client.run("create . pkg/1.1@lasote/stable")
+        self.client.run("upload pkg/1.1@lasote/stable --all -c --remote remote1")
+        self.client.run("remove -f pkg/1.1@lasote/stable")
+        self.client.run("install pkg/1.1@lasote/stable")
+        self.assertIn("pkg/1.1@lasote/stable: Package installed", self.client.out)
+        self.client.run("remote list_ref")
+        self.assertIn("pkg/1.1@lasote/stable: remote1", self.client.out)
+        self.client.run("editable add . pkg/1.1@lasote/stable")
+        # Check add --force, update and rename
+        self.client.run("remote add remote2 %s --force" % self.servers["remote1"].fake_url)
+        self.client.run("remote update remote2 %sfake" % self.servers["remote1"].fake_url)
+        self.client.run("remote rename remote2 remote-fake")
+        self.client.run("editable remove pkg/1.1@lasote/stable")
+        # Check associated remote has changed name
+        self.client.run("remote list_ref")
+        self.assertIn("pkg/1.1@lasote/stable: remote-fake", self.client.out)
+        # Check remove
+        self.client.run("editable add . pkg/1.1@lasote/stable")
+        self.client.run("remote remove remote-fake")
+        self.client.run("remote list")
+        self.assertIn("remote0: %s" % self.servers["remote0"].fake_url, self.client.out)
+        self.assertNotIn("remote-fake", self.client.out)
+        # Check clean
+        self.client.run("editable remove pkg/1.1@lasote/stable")
+        self.client.run("remove -f pkg/1.1@lasote/stable")
+        self.client.run("remote add remote1 %s" % self.servers["remote1"].fake_url)
+        self.client.run("install pkg/1.1@lasote/stable")
+        self.client.run("editable add . pkg/1.1@lasote/stable")
+        self.client.run("remote clean")
+        self.client.run("remote list")
+        self.assertNotIn("remote1", self.client.out)
+        self.assertNotIn("remote0", self.client.out)