Repository: conan-io/conan

Issue:
conan install unexpectedly reports invalid version range
i have this requirement in my `conanfile.py`:
`foo/[>16.5.0 <17.0.0]@bar/stable`

`conan install` aborts with this error:
`ERROR: version range expression '>16.5.0<17.0.0' is not valid`

note: using the deprecated comma-based syntax works fine

* Python 3.6 on CentOS 7.5
* Conan 1.11.2
* node-semver 0.6.1

Discussion:

Patch:
diff --git a/conans/client/client_cache.py b/conans/client/client_cache.py
index 9912370a4..8518adc24 100644
--- a/conans/client/client_cache.py
+++ b/conans/client/client_cache.py
@@ -16,7 +16,8 @@ from conans.model.package_metadata import PackageMetadata
 from conans.model.profile import Profile
 from conans.model.ref import ConanFileReference
 from conans.model.settings import Settings
-from conans.paths import CONAN_MANIFEST, PUT_HEADERS, SimplePaths, check_ref_case
+from conans.paths import CONAN_MANIFEST, PUT_HEADERS
+from conans.paths.simple_paths import SimplePaths
 from conans.unicode import get_cwd
 from conans.util.files import list_folder_subdirs, load, normalize, save
 from conans.util.locks import Lock, NoLock, ReadLock, SimpleLock, WriteLock
@@ -28,6 +29,9 @@ REGISTRY = "registry.txt"
 REGISTRY_JSON = "registry.json"
 PROFILES_FOLDER = "profiles"
 HOOKS_FOLDER = "hooks"
+LAYOUTS_FOLDER = 'layouts'
+
+DEFAULT_LAYOUT_FILE = "default"
 
 # Client certificates
 CLIENT_CERT = "client.crt"
@@ -161,6 +165,10 @@ class ClientCache(SimplePaths):
             return join(self.conan_folder, PROFILES_FOLDER,
                         self.conan_config.default_profile)
 
+    @property
+    def default_editable_path(self):
+        return os.path.join(self.conan_folder, LAYOUTS_FOLDER, DEFAULT_LAYOUT_FILE)
+
     @property
     def hooks_path(self):
         """
@@ -246,7 +254,6 @@ class ClientCache(SimplePaths):
         """conan_id = sha(zip file)"""
         assert isinstance(conan_reference, ConanFileReference)
         export_folder = self.export(conan_reference)
-        check_ref_case(conan_reference, export_folder, self.store)
         return FileTreeManifest.load(export_folder)
 
     def load_package_manifest(self, package_reference):
@@ -313,6 +320,17 @@ class ClientCache(SimplePaths):
         readed_digest = FileTreeManifest.load(package_folder)
         return readed_digest.summary_hash
 
+    def install_as_editable(self, conan_reference, target_path):
+        linked_folder_sentinel = self._build_path_to_linked_folder_sentinel(conan_reference)
+        save(linked_folder_sentinel, content=target_path)
+
+    def remove_editable(self, conan_reference):
+        if self.installed_as_editable(conan_reference):
+            linked_folder_sentinel = self._build_path_to_linked_folder_sentinel(conan_reference)
+            os.remove(linked_folder_sentinel)
+            return True
+        return False
+
 
 def _mix_settings_with_env(settings):
     """Reads CONAN_ENV_XXXX variables from environment
diff --git a/conans/client/command.py b/conans/client/command.py
index 05f7757f0..67c654851 100644
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -11,15 +11,16 @@ from conans import __version__ as client_version
 from conans.client.cmd.uploader import UPLOAD_POLICY_FORCE, \
     UPLOAD_POLICY_NO_OVERWRITE, UPLOAD_POLICY_NO_OVERWRITE_RECIPE, UPLOAD_POLICY_SKIP
 from conans.client.conan_api import (Conan, default_manifest_folder)
+from conans.client.conan_api import _get_conanfile_path
 from conans.client.conan_command_output import CommandOutputer
 from conans.client.output import Color
 from conans.client.printer import Printer
-from conans.client.tools.files import save
 from conans.errors import ConanException, ConanInvalidConfiguration, NoRemoteAvailable
 from conans.model.ref import ConanFileReference
 from conans.unicode import get_cwd
 from conans.util.config_parser import get_bool_from_text
 from conans.util.files import exception_message_safe
+from conans.util.files import save
 from conans.util.log import logger
 
 # Exit codes for conan command:
@@ -1332,12 +1333,46 @@ class Command(object):
 
         self._conan.export_alias(args.reference, args.target)
 
+    def link(self, *args):
+        """ Links a conan reference (e.g lib/1.0@conan/stable) with a local folder path.
+
+        """
+        parser = argparse.ArgumentParser(description=self.link.__doc__,
+                                         prog="conan link")
+        parser.add_argument('target', help='Path to the package folder in the user workspace',
+                            nargs='?',)
+        parser.add_argument('reference', help='Reference to link. e.g.: mylib/1.X@user/channel')
+        parser.add_argument("--remove", action='store_true', default=False,
+                            help='Remove linked reference (target not required)')
+
+        args = parser.parse_args(*args)
+        self._warn_python2()
+
+        # Args sanity check
+        if args.remove and args.target:
+            raise ConanException("Do not provide the 'target' argument for removal")
+
+        if not args.remove and not args.target:
+            raise ConanException("Argument 'target' is required to link a reference")
+
+        if not args.remove:
+            self._conan.link(args.target, args.reference, cwd=os.getcwd())
+            self._outputer.writeln("Reference '{}' linked to directory "
+                                   "'{}'".format(args.reference, os.path.dirname(args.target)))
+        else:
+            ret = self._conan.unlink(args.reference)
+            if ret:
+                self._outputer.writeln("Removed linkage for reference '{}'".format(args.reference))
+            else:
+                self._user_io.out.warn("Reference '{}' was not installed "
+                                       "as editable".format(args.reference))
+
     def _show_help(self):
         """Prints a summary of all commands
         """
         grps = [("Consumer commands", ("install", "config", "get", "info", "search")),
                 ("Creator commands", ("new", "create", "upload", "export", "export-pkg", "test")),
-                ("Package development commands", ("source", "build", "package")),
+                ("Package development commands", ("source", "build", "package", "link")),
                 ("Misc commands", ("profile", "remote", "user", "imports", "copy", "remove",
                                    "alias", "download", "inspect", "help"))]
 
diff --git a/conans/client/conan_api.py b/conans/client/conan_api.py
index d8ff587cf..86f4d1f54 100644
--- a/conans/client/conan_api.py
+++ b/conans/client/conan_api.py
@@ -49,7 +49,7 @@ from conans.model.graph_info import GraphInfo, GRAPH_INFO_FILE
 from conans.model.ref import ConanFileReference, PackageReference, check_valid_ref
 from conans.model.version import Version
 from conans.model.workspace import Workspace
-from conans.paths import BUILD_INFO, CONANINFO, get_conan_user_home
+from conans.paths import BUILD_INFO, CONANINFO, get_conan_user_home, CONAN_PACKAGE_LAYOUT_FILE
 from conans.tools import set_global_instances
 from conans.unicode import get_cwd
 from conans.util.env_reader import get_env
@@ -933,6 +933,26 @@ class ConanAPIV1(object):
     def get_remote_by_name(self, remote_name):
         return self._client_cache.registry.remotes.get(remote_name)
 
+    @api_method
+    def link(self, target_path, target_reference, cwd):
+        # Retrieve conanfile.py from target_path
+        target_path = _get_conanfile_path(path=target_path, cwd=cwd, py=True)
+
+        ref = ConanFileReference.loads(target_reference, validate=True)
+        target_conanfile = self._graph_manager._loader.load_class(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 "
+                                 "conanfile.py ({}/{}) must match".
+                                 format(ref, target_conanfile.name, target_conanfile.version))
+
+        self._client_cache.install_as_editable(ref, os.path.dirname(target_path))
+
+    @api_method
+    def unlink(self, reference):
+        ref = ConanFileReference.loads(reference, validate=True)
+        return self._client_cache.remove_editable(ref)
+
 
 Conan = ConanAPIV1
 
diff --git a/conans/client/conan_command_output.py b/conans/client/conan_command_output.py
index b55f1d5bc..abf64ff3c 100644
--- a/conans/client/conan_command_output.py
+++ b/conans/client/conan_command_output.py
@@ -7,6 +7,7 @@ from conans.model.ref import ConanFileReference, PackageReference
 from conans.search.binary_html_table import html_binary_graph
 from conans.unicode import get_cwd
 from conans.util.files import save
+from conans.client.graph.graph import RECIPE_EDITABLE
 
 
 class CommandOutputer(object):
@@ -73,7 +74,7 @@ class CommandOutputer(object):
         ret = {}
         for node in sorted(deps_graph.nodes):
             ref = node.conan_ref
-            if node.recipe not in (RECIPE_CONSUMER, RECIPE_VIRTUAL):
+            if node.recipe not in (RECIPE_CONSUMER, RECIPE_VIRTUAL, RECIPE_EDITABLE):
                 manifest = self.client_cache.load_manifest(ref)
                 ret[ref] = manifest.time_str
         return ret
diff --git a/conans/client/graph/graph.py b/conans/client/graph/graph.py
index eb6add3a2..cc168b890 100644
--- a/conans/client/graph/graph.py
+++ b/conans/client/graph/graph.py
@@ -12,6 +12,7 @@ RECIPE_NOT_IN_REMOTE = "Not in remote"
 RECIPE_UPDATEABLE = "Update available"  # The update of the recipe is available (only in conan info)
 RECIPE_NO_REMOTE = "No remote"
 RECIPE_WORKSPACE = "Workspace"
+RECIPE_EDITABLE = "Editable"
 RECIPE_CONSUMER = "Consumer"  # A conanfile from the user
 RECIPE_VIRTUAL = "Virtual"  # A virtual conanfile (dynamic in memory conanfile)
 
@@ -22,6 +23,7 @@ BINARY_BUILD = "Build"
 BINARY_MISSING = "Missing"
 BINARY_SKIP = "Skip"
 BINARY_WORKSPACE = "Workspace"
+BINARY_EDITABLE = "Editable"
 
 
 class Node(object):
@@ -99,6 +101,10 @@ class Node(object):
         if self.conan_ref.revision is not None and other.conan_ref.revision is None:
             return -1
 
+        if self.recipe in (RECIPE_CONSUMER, RECIPE_VIRTUAL):
+            return 1
+        if other.recipe in (RECIPE_CONSUMER, RECIPE_VIRTUAL):
+            return -1
         if self.conan_ref < other.conan_ref:
             return -1
 
diff --git a/conans/client/graph/graph_binaries.py b/conans/client/graph/graph_binaries.py
index 36e8b7c22..933d757fe 100644
--- a/conans/client/graph/graph_binaries.py
+++ b/conans/client/graph/graph_binaries.py
@@ -2,7 +2,9 @@ import os
 
 from conans.client.graph.graph import (BINARY_BUILD, BINARY_CACHE, BINARY_DOWNLOAD, BINARY_MISSING,
                                        BINARY_SKIP, BINARY_UPDATE, BINARY_WORKSPACE,
+                                       RECIPE_EDITABLE, BINARY_EDITABLE,
                                        RECIPE_CONSUMER, RECIPE_VIRTUAL)
+from conans.client.output import ScopedOutput
 from conans.errors import NoRemoteAvailable, NotFoundException
 from conans.model.info import ConanInfo
 from conans.model.manifest import FileTreeManifest
@@ -74,6 +76,11 @@ class GraphBinariesAnalyzer(object):
         evaluated_references[package_ref] = node
 
         output = conanfile.output
+
+        if node.recipe == RECIPE_EDITABLE:
+            node.binary = BINARY_EDITABLE
+            return
+
         if build_mode.forced(conanfile, conan_ref):
             output.warn('Forced build from source')
             node.binary = BINARY_BUILD
@@ -91,7 +98,8 @@ class GraphBinariesAnalyzer(object):
         with self._client_cache.package_lock(package_ref):
             if is_dirty(package_folder):
                 output.warn("Package is corrupted, removing folder: %s" % package_folder)
-                rmdir(package_folder)
+                assert node.recipe != RECIPE_EDITABLE, "Editable package cannot be dirty"
+                rmdir(package_folder)  # Do not remove if it is EDITABLE
 
         if remote_name:
             remote = self._registry.remotes.get(remote_name)
@@ -116,6 +124,7 @@ class GraphBinariesAnalyzer(object):
             if not node.binary:
                 node.binary = BINARY_CACHE
                 package_hash = ConanInfo.load_from_package(package_folder).recipe_hash
+
         else:  # Binary does NOT exist locally
             if not revisions_enabled and not node.revision_pinned:
                 # Do not search for packages for the specific resolved recipe revision but all
diff --git a/conans/client/graph/proxy.py b/conans/client/graph/proxy.py
index 5b0382fa9..7277a231a 100644
--- a/conans/client/graph/proxy.py
+++ b/conans/client/graph/proxy.py
@@ -4,7 +4,7 @@ from requests.exceptions import RequestException
 
 from conans.client.graph.graph import (RECIPE_DOWNLOADED, RECIPE_INCACHE, RECIPE_NEWER,
                                        RECIPE_NOT_IN_REMOTE, RECIPE_NO_REMOTE, RECIPE_UPDATEABLE,
-                                       RECIPE_UPDATED)
+                                       RECIPE_UPDATED, RECIPE_EDITABLE)
 from conans.client.output import ScopedOutput
 from conans.client.recorder.action_recorder import INSTALL_ERROR_MISSING, INSTALL_ERROR_NETWORK
 from conans.client.remover import DiskRemover
@@ -23,6 +23,13 @@ class ConanProxy(object):
         self._registry = client_cache.registry
 
     def get_recipe(self, conan_reference, check_updates, update, remote_name, recorder):
+        if self._client_cache.installed_as_editable(conan_reference):
+            conanfile_path = self._client_cache.conanfile(conan_reference)
+            status = RECIPE_EDITABLE
+            # TODO: log_recipe_got_from_editable(reference)
+            # TODO: recorder.recipe_fetched_as_editable(reference)
+            return conanfile_path, status, None, conan_reference
+
         with self._client_cache.conanfile_write_lock(conan_reference):
             result = self._get_recipe(conan_reference, check_updates, update, remote_name, recorder)
             conanfile_path, status, remote, reference = result
diff --git a/conans/client/installer.py b/conans/client/installer.py
index 2eadec8fb..a1a79555a 100644
--- a/conans/client/installer.py
+++ b/conans/client/installer.py
@@ -7,7 +7,7 @@ from conans.client import tools
 from conans.client.file_copier import report_copied_files
 from conans.client.generators import TXTGenerator, write_generators
 from conans.client.graph.graph import BINARY_BUILD, BINARY_CACHE, BINARY_DOWNLOAD, BINARY_MISSING, \
-    BINARY_SKIP, BINARY_UPDATE
+    BINARY_SKIP, BINARY_UPDATE, BINARY_EDITABLE
 from conans.client.importer import remove_imports
 from conans.client.output import ScopedOutput
 from conans.client.packager import create_package
@@ -19,6 +19,7 @@ from conans.errors import (ConanException, ConanExceptionInUserConanfileMethod,
                            conanfile_exception_formatter)
 from conans.model.build_info import CppInfo
 from conans.model.conan_file import get_env_context_manager
+from conans.model.editable_cpp_info import EditableCppInfo
 from conans.model.env_info import EnvInfo
 from conans.model.manifest import FileTreeManifest
 from conans.model.ref import PackageReference
@@ -265,6 +266,7 @@ class BinaryInstaller(object):
         self._recorder = recorder
         self._workspace = workspace
         self._hook_manager = hook_manager
+        self._editable_cpp_info = self._load_editables_cpp_info()
 
     def install(self, deps_graph, keep_build=False, graph_info=None):
         # order by levels and separate the root node (conan_ref=None) from the rest
@@ -288,6 +290,10 @@ class BinaryInstaller(object):
                     raise_package_not_found_error(conan_file, conan_ref, package_id, dependencies,
                                                   out=output, recorder=self._recorder)
 
+                if node.binary == BINARY_EDITABLE:
+                    self._handle_node_editable(node)
+                    continue
+
                 workspace_package = self._workspace[node.conan_ref] if self._workspace else None
                 if workspace_package:
                     self._handle_node_workspace(node, workspace_package, inverse_levels, deps_graph,
@@ -311,6 +317,41 @@ class BinaryInstaller(object):
             if node.update_manifest == read_manifest:
                 return True
 
+    def _load_editables_cpp_info(self):
+        editables_path = self._client_cache.default_editable_path
+        if os.path.exists(editables_path):
+            return EditableCppInfo.load(editables_path, require_namespace=True)
+        return None
+
+    def _handle_node_editable(self, node):
+        # Get source of information
+        package_layout = self._client_cache.package_layout(node.conan_ref)
+        base_path = package_layout.conan()
+        self._call_package_info(node.conanfile, package_folder=base_path)
+
+        # Try with package-provided file
+        package_layout_file = package_layout.editable_package_layout_file()
+        if os.path.exists(package_layout_file):
+            editable_cpp_info = EditableCppInfo.load(package_layout_file,
+                                                     require_namespace=False)
+            editable_cpp_info.apply_to(node.conanfile.name,
+                                       node.conanfile.cpp_info,
+                                       base_path=base_path,
+                                       settings=node.conanfile.settings,
+                                       options=node.conanfile.options)
+
+        # Try with the profile-like file
+        elif self._editable_cpp_info and self._editable_cpp_info.has_info_for(node.conanfile.name):
+            self._editable_cpp_info.apply_to(node.conanfile.name,
+                                             node.conanfile.cpp_info,
+                                             base_path=base_path,
+                                             settings=node.conanfile.settings,
+                                             options=node.conanfile.options)
+
+        # Use `package_info()` data
+        else:
+            pass  # It will use `package_info()` data relative to path used as 'package_folder'
+
     def _handle_node_cache(self, node, package_ref, keep_build, processed_package_references):
         conan_file = node.conanfile
         output = conan_file.output
diff --git a/conans/client/manager.py b/conans/client/manager.py
index baf1229ce..ac614826a 100644
--- a/conans/client/manager.py
+++ b/conans/client/manager.py
@@ -2,6 +2,7 @@ import os
 
 from conans.client.client_cache import ClientCache
 from conans.client.generators import write_generators
+from conans.client.graph.graph import RECIPE_CONSUMER, RECIPE_VIRTUAL
 from conans.client.graph.printer import print_graph
 from conans.client.importer import run_deploy, run_imports
 from conans.client.installer import BinaryInstaller, call_system_requirements
@@ -14,7 +15,6 @@ from conans.errors import ConanException
 from conans.model.ref import ConanFileReference
 from conans.paths import CONANINFO
 from conans.util.files import normalize, save
-from conans.client.graph.graph import RECIPE_CONSUMER, RECIPE_VIRTUAL
 
 
 class ConanManager(object):
@@ -52,7 +52,7 @@ class ConanManager(object):
         """ Fetch and build all dependencies for the given reference
         @param reference: ConanFileReference or path to user space conanfile
         @param install_folder: where the output files will be saved
-        @param remote: install only from that remote
+        @param remote_name: install only from that remote
         @param profile: Profile object with both the -s introduced options and profile read values
         @param build_modes: List of build_modes specified
         @param update: Check for updated in the upstream remotes (and update)
diff --git a/conans/client/manifest_manager.py b/conans/client/manifest_manager.py
index 75e1e977b..8115152fc 100644
--- a/conans/client/manifest_manager.py
+++ b/conans/client/manifest_manager.py
@@ -1,11 +1,11 @@
 import os
 
+from conans.client.graph.graph import RECIPE_CONSUMER, RECIPE_VIRTUAL
 from conans.client.remote_registry import Remote
 from conans.errors import ConanException
 from conans.model.manifest import FileTreeManifest
 from conans.model.ref import PackageReference
-from conans.paths import SimplePaths
-from conans.client.graph.graph import RECIPE_CONSUMER, RECIPE_VIRTUAL
+from conans.paths.simple_paths import SimplePaths
 
 
 class ManifestManager(object):
diff --git a/conans/client/printer.py b/conans/client/printer.py
index 771f1e862..36f661783 100644
--- a/conans/client/printer.py
+++ b/conans/client/printer.py
@@ -5,7 +5,7 @@ from conans.client.installer import build_id
 from conans.client.output import Color
 from conans.model.options import OptionsValues
 from conans.model.ref import ConanFileReference, PackageReference
-from conans.paths import SimplePaths
+from conans.paths.simple_paths import SimplePaths
 from conans.util.env_reader import get_env
 from conans.client.graph.graph import RECIPE_CONSUMER, RECIPE_VIRTUAL
 
diff --git a/conans/client/remover.py b/conans/client/remover.py
index 40234bb6f..1955f9b72 100644
--- a/conans/client/remover.py
+++ b/conans/client/remover.py
@@ -99,6 +99,10 @@ class ConanRemover(object):
             return tmp
 
     def _local_remove(self, reference, src, build_ids, package_ids):
+        if self._client_cache.installed_as_editable(reference):
+            raise ConanException("Package '{r}' is installed as editable, unlink it first using "
+                                 "command 'conan link {r} --remove'".format(r=reference))
+
         # Make sure to clean the locks too
         self._client_cache.remove_package_locks(reference)
         remover = DiskRemover(self._client_cache)
diff --git a/conans/model/editable_cpp_info.py b/conans/model/editable_cpp_info.py
new file mode 100644
index 000000000..0f8aa3231
--- /dev/null
+++ b/conans/model/editable_cpp_info.py
@@ -0,0 +1,82 @@
+# coding=utf-8
+import configparser
+import ntpath
+import os
+import posixpath
+from collections import defaultdict
+
+from conans.client.tools.files import load
+
+
+class EditableCppInfo(object):
+    WILDCARD = "*"
+    cpp_info_dirs = ['includedirs', 'libdirs', 'resdirs', 'bindirs']
+
+    def __init__(self, data, uses_namespace):
+        self._data = data
+        self._uses_namespace = uses_namespace
+
+    @classmethod
+    def load(cls, filepath, require_namespace):
+        data = cls._loads(content=load(filepath), require_namespace=require_namespace)
+        return EditableCppInfo(data, uses_namespace=require_namespace)
+
+    @classmethod
+    def loads(cls, content, require_namespace):
+        data = cls._loads(content=content, require_namespace=require_namespace)
+        return EditableCppInfo(data, uses_namespace=require_namespace)
+
+    @classmethod
+    def _loads(cls, content, require_namespace):
+        """ Returns a dictionary containing information about paths for a CppInfo object: includes,
+        libraries, resources, binaries,... """
+
+        parser = configparser.ConfigParser(allow_no_value=True, delimiters=('#', ))
+        parser.optionxform = str
+        parser.read_string(content)
+
+        if not require_namespace:
+            ret = {k: [] for k in cls.cpp_info_dirs}
+            for section in ret.keys():
+                if section in parser:
+                    ret[section] = parser[section]
+            return ret
+        else:
+            ret = defaultdict(lambda: {k: [] for k in cls.cpp_info_dirs})
+            for section in parser:
+                if ':' in section:
+                    namespace, key = section.split(':', 1)
+                    if key in cls.cpp_info_dirs:
+                        ret[namespace][key] = parser[section]
+            return ret
+
+    @staticmethod
+    def _work_on_item(value, base_path, settings, options):
+        value = value.replace('\\', '/')
+        isabs = ntpath.isabs(value) or posixpath.isabs(value)
+        if base_path and not isabs:
+            value = os.path.abspath(os.path.join(base_path, value))
+        value = os.path.normpath(value)
+        value = value.format(settings=settings, options=options)
+        return value
+
+    def has_info_for(self, pck_name, use_wildcard=True):
+        if self._uses_namespace:
+            return pck_name in self._data or (use_wildcard and self.WILDCARD in self._data)
+        else:
+            return True
+
+    def apply_to(self, pkg_name, cpp_info, base_path, settings=None, options=None, use_wildcard=True):
+        if self._uses_namespace:
+            if pkg_name in self._data:
+                data_to_apply = self._data[pkg_name]
+            elif use_wildcard and self.WILDCARD in self._data:
+                data_to_apply = self._data[self.WILDCARD]
+            else:
+                data_to_apply = {k: [] for k in self.cpp_info_dirs}
+        else:
+            data_to_apply = self._data
+
+        for key, items in data_to_apply.items():
+            setattr(cpp_info, key, [self._work_on_item(item, base_path, settings, options)
+                                    for item in items])
diff --git a/conans/model/ref.py b/conans/model/ref.py
index 560534ead..984bb0e3d 100644
--- a/conans/model/ref.py
+++ b/conans/model/ref.py
@@ -78,7 +78,6 @@ class ConanFileReference(namedtuple("ConanFileReference", "name version user cha
     """ Full reference of a package recipes, e.g.:
     opencv/2.4.10@lasote/testing
     """
-    whitespace_pattern = re.compile(r"\s+")
     sep_pattern = re.compile(r"([^/]+)/([^/]+)@([^/]+)/([^/#]+)#?(.+)?")
 
     def __new__(cls, name, version, user, channel, revision=None, validate=True):
@@ -107,7 +106,6 @@ class ConanFileReference(namedtuple("ConanFileReference", "name version user cha
     def loads(text, validate=True):
         """ Parses a text string to generate a ConanFileReference object
         """
-        text = ConanFileReference.whitespace_pattern.sub("", text)
         try:
             # Split returns empty start and end groups
             _, name, version, user, channel, revision, _ = ConanFileReference.sep_pattern.split(text)
diff --git a/conans/paths.py b/conans/paths.py
deleted file mode 100644
index e60d64942..000000000
--- a/conans/paths.py
+++ /dev/null
@@ -1,163 +0,0 @@
-import os
-import platform
-from os.path import join, normpath
-
-from conans.errors import ConanException
-from conans.model.ref import ConanFileReference, PackageReference
-from conans.util.files import rmdir
-
-if platform.system() == "Windows":
-    from conans.util.windows import conan_expand_user,  rm_conandir, path_shortener
-else:
-    def path_shortener(x, _):
-        return x
-    conan_expand_user = os.path.expanduser
-    rm_conandir = rmdir
-
-
-EXPORT_FOLDER = "export"
-EXPORT_SRC_FOLDER = "export_source"
-SRC_FOLDER = "source"
-BUILD_FOLDER = "build"
-PACKAGES_FOLDER = "package"
-SYSTEM_REQS_FOLDER = "system_reqs"
-
-
-CONANFILE = 'conanfile.py'
-CONANFILE_TXT = "conanfile.txt"
-CONAN_MANIFEST = "conanmanifest.txt"
-BUILD_INFO = 'conanbuildinfo.txt'
-BUILD_INFO_GCC = 'conanbuildinfo.gcc'
-BUILD_INFO_COMPILER_ARGS = 'conanbuildinfo.args'
-BUILD_INFO_CMAKE = 'conanbuildinfo.cmake'
-BUILD_INFO_QMAKE = 'conanbuildinfo.pri'
-BUILD_INFO_QBS = 'conanbuildinfo.qbs'
-BUILD_INFO_VISUAL_STUDIO = 'conanbuildinfo.props'
-BUILD_INFO_XCODE = 'conanbuildinfo.xcconfig'
-BUILD_INFO_PREMAKE = 'conanbuildinfo.lua'
-BUILD_INFO_MAKE = 'conanbuildinfo.mak'
-CONANINFO = "conaninfo.txt"
-CONANENV = "conanenv.txt"
-SYSTEM_REQS = "system_reqs.txt"
-PUT_HEADERS = "artifacts.properties"
-SCM_FOLDER = "scm_folder.txt"
-PACKAGE_METADATA = "metadata.json"
-
-PACKAGE_TGZ_NAME = "conan_package.tgz"
-EXPORT_TGZ_NAME = "conan_export.tgz"
-EXPORT_SOURCES_TGZ_NAME = "conan_sources.tgz"
-EXPORT_SOURCES_DIR_OLD = ".c_src"
-
-RUN_LOG_NAME = "conan_run.log"
-DEFAULT_PROFILE_NAME = "default"
-
-
-def get_conan_user_home():
-    user_home = os.getenv("CONAN_USER_HOME", "~")
-    tmp = conan_expand_user(user_home)
-    if not os.path.isabs(tmp):
-        raise Exception("Invalid CONAN_USER_HOME value '%s', "
-                        "please specify an absolute or path starting with ~/ "
-                        "(relative to user home)" % tmp)
-    return os.path.abspath(tmp)
-
-
-def is_case_insensitive_os():
-    system = platform.system()
-    return system != "Linux" and system != "FreeBSD" and system != "SunOS"
-
-
-if is_case_insensitive_os():
-    def check_ref_case(conan_reference, conan_folder, store_folder):
-        if not os.path.exists(conan_folder):  # If it doesn't exist, not a problem
-            return
-        # If exists, lets check path
-        tmp = store_folder
-        for part in conan_reference.dir_repr().split("/"):
-            items = os.listdir(tmp)
-            if part not in items:
-                offending = ""
-                for item in items:
-                    if item.lower() == part.lower():
-                        offending = item
-                        break
-                raise ConanException("Requested '%s' but found case incompatible '%s'\n"
-                                     "Case insensitive filesystem can't manage this"
-                                     % (str(conan_reference), offending))
-            tmp = os.path.normpath(tmp + os.sep + part)
-else:
-    def check_ref_case(conan_reference, conan_folder, store_folder):  # @UnusedVariable
-        pass
-
-
-class SimplePaths(object):
-    """
-    Generate Conan paths. Handles the conan domain path logic. NO DISK ACCESS, just
-    path logic responsability
-    """
-    def __init__(self, store_folder):
-        self._store_folder = store_folder
-
-    @property
-    def store(self):
-        return self._store_folder
-
-    def conan(self, conan_reference):
-        """ the base folder for this package reference, for each ConanFileReference
-        """
-        assert isinstance(conan_reference, ConanFileReference)
-        return normpath(join(self._store_folder, conan_reference.dir_repr()))
-
-    def export(self, conan_reference):
-        assert isinstance(conan_reference, ConanFileReference)
-        return normpath(join(self.conan(conan_reference), EXPORT_FOLDER))
-
-    def export_sources(self, conan_reference, short_paths=False):
-        assert isinstance(conan_reference, ConanFileReference)
-        p = normpath(join(self.conan(conan_reference), EXPORT_SRC_FOLDER))
-        return path_shortener(p, short_paths)
-
-    def source(self, conan_reference, short_paths=False):
-        assert isinstance(conan_reference, ConanFileReference)
-        p = normpath(join(self.conan(conan_reference), SRC_FOLDER))
-        return path_shortener(p, short_paths)
-
-    def conanfile(self, conan_reference):
-        export = self.export(conan_reference)
-        check_ref_case(conan_reference, export, self.store)
-        return normpath(join(export, CONANFILE))
-
-    def builds(self, conan_reference):
-        assert isinstance(conan_reference, ConanFileReference)
-        return normpath(join(self.conan(conan_reference), BUILD_FOLDER))
-
-    def build(self, package_reference, short_paths=False):
-        assert isinstance(package_reference, PackageReference)
-        p = normpath(join(self.conan(package_reference.conan), BUILD_FOLDER,
-                          package_reference.package_id))
-        return path_shortener(p, short_paths)
-
-    def system_reqs(self, conan_reference):
-        assert isinstance(conan_reference, ConanFileReference)
-        return normpath(join(self.conan(conan_reference), SYSTEM_REQS_FOLDER, SYSTEM_REQS))
-
-    def system_reqs_package(self, package_reference):
-        assert isinstance(package_reference, PackageReference)
-        return normpath(join(self.conan(package_reference.conan), SYSTEM_REQS_FOLDER,
-                             package_reference.package_id, SYSTEM_REQS))
-
-    def packages(self, conan_reference):
-        assert isinstance(conan_reference, ConanFileReference)
-        return normpath(join(self.conan(conan_reference), PACKAGES_FOLDER))
-
-    def package(self, package_reference, short_paths=False):
-        assert isinstance(package_reference, PackageReference)
-        p = normpath(join(self.conan(package_reference.conan), PACKAGES_FOLDER,
-                          package_reference.package_id))
-        return path_shortener(p, short_paths)
-
-    def scm_folder(self, conan_reference):
-        return normpath(join(self.conan(conan_reference), SCM_FOLDER))
-
-    def package_metadata(self, conan_reference):
-        return normpath(join(self.conan(conan_reference), PACKAGE_METADATA))
diff --git a/conans/paths/__init__.py b/conans/paths/__init__.py
new file mode 100644
index 000000000..19c9663f1
--- /dev/null
+++ b/conans/paths/__init__.py
@@ -0,0 +1,65 @@
+# coding=utf-8
+
+import os
+import platform
+
+if platform.system() == "Windows":
+    from conans.util.windows import conan_expand_user, rm_conandir
+else:
+    from conans.util.files import rmdir
+
+    conan_expand_user = os.path.expanduser
+    rm_conandir = rmdir
+
+
+def get_conan_user_home():
+    user_home = os.getenv("CONAN_USER_HOME", "~")
+    tmp = conan_expand_user(user_home)
+    if not os.path.isabs(tmp):
+        raise Exception("Invalid CONAN_USER_HOME value '%s', "
+                        "please specify an absolute or path starting with ~/ "
+                        "(relative to user home)" % tmp)
+    return os.path.abspath(tmp)
+
+
+def is_case_insensitive_os():
+    system = platform.system()
+    return system != "Linux" and system != "FreeBSD" and system != "SunOS"
+
+
+# Files
+CONANFILE = 'conanfile.py'
+CONANFILE_TXT = "conanfile.txt"
+CONAN_MANIFEST = "conanmanifest.txt"
+BUILD_INFO = 'conanbuildinfo.txt'
+BUILD_INFO_GCC = 'conanbuildinfo.gcc'
+BUILD_INFO_COMPILER_ARGS = 'conanbuildinfo.args'
+BUILD_INFO_CMAKE = 'conanbuildinfo.cmake'
+BUILD_INFO_QMAKE = 'conanbuildinfo.pri'
+BUILD_INFO_QBS = 'conanbuildinfo.qbs'
+BUILD_INFO_VISUAL_STUDIO = 'conanbuildinfo.props'
+BUILD_INFO_XCODE = 'conanbuildinfo.xcconfig'
+BUILD_INFO_PREMAKE = 'conanbuildinfo.lua'
+BUILD_INFO_MAKE = 'conanbuildinfo.mak'
+CONANINFO = "conaninfo.txt"
+CONANENV = "conanenv.txt"
+SYSTEM_REQS = "system_reqs.txt"
+PUT_HEADERS = "artifacts.properties"
+PACKAGE_TGZ_NAME = "conan_package.tgz"
+EXPORT_TGZ_NAME = "conan_export.tgz"
+EXPORT_SOURCES_TGZ_NAME = "conan_sources.tgz"
+EXPORT_SOURCES_DIR_OLD = ".c_src"
+RUN_LOG_NAME = "conan_run.log"
+DEFAULT_PROFILE_NAME = "default"
+SCM_FOLDER = "scm_folder.txt"
+PACKAGE_METADATA = "metadata.json"
+LINKED_PACKAGE_SENTINEL = '.linked_package'
+CONAN_PACKAGE_LAYOUT_FILE = '.conan_layout'
+
+# Directories
+EXPORT_FOLDER = "export"
+EXPORT_SRC_FOLDER = "export_source"
+SRC_FOLDER = "source"
+BUILD_FOLDER = "build"
+PACKAGES_FOLDER = "package"
+SYSTEM_REQS_FOLDER = "system_reqs"
diff --git a/conans/paths/package_layouts/__init__.py b/conans/paths/package_layouts/__init__.py
new file mode 100644
index 000000000..9bad5790a
--- /dev/null
+++ b/conans/paths/package_layouts/__init__.py
@@ -0,0 +1,1 @@
+# coding=utf-8
diff --git a/conans/paths/package_layouts/package_cache_layout.py b/conans/paths/package_layouts/package_cache_layout.py
new file mode 100644
index 000000000..d7f1ac1b6
--- /dev/null
+++ b/conans/paths/package_layouts/package_cache_layout.py
@@ -0,0 +1,83 @@
+# coding=utf-8
+
+import os
+import platform
+
+from conans.model.ref import ConanFileReference
+from conans.model.ref import PackageReference
+from conans.paths import CONANFILE, SYSTEM_REQS, EXPORT_FOLDER, EXPORT_SRC_FOLDER, SRC_FOLDER, \
+    BUILD_FOLDER, PACKAGES_FOLDER, SYSTEM_REQS_FOLDER, SCM_FOLDER, PACKAGE_METADATA
+
+
+def short_path(func):
+    if platform.system() == "Windows":
+        from conans.util.windows import path_shortener
+
+        def wrap(self, *args, **kwargs):
+            p = func(self,  *args, **kwargs)
+            return path_shortener(p, self._short_paths)
+        return wrap
+    else:
+        return func
+
+
+class PackageCacheLayout(object):
+    """ This is the package layout for Conan cache """
+
+    def __init__(self, base_folder, conan_ref, short_paths):
+        assert isinstance(conan_ref, ConanFileReference)
+        self._conan_ref = conan_ref
+        self._base_folder = os.path.normpath(base_folder)
+        self._short_paths = short_paths
+
+    def conan(self):
+        """ Returns the base folder for this package reference """
+        return self._base_folder
+
+    def export(self):
+        return os.path.join(self.conan(), EXPORT_FOLDER)
+
+    @short_path
+    def export_sources(self):
+        return os.path.join(self.conan(), EXPORT_SRC_FOLDER)
+
+    @short_path
+    def source(self):
+        return os.path.join(self.conan(), SRC_FOLDER)
+
+    def conanfile(self):
+        export = self.export()
+        return os.path.join(export, CONANFILE)
+
+    def builds(self):
+        return os.path.join(self.conan(), BUILD_FOLDER)
+
+    @short_path
+    def build(self, package_reference):
+        assert isinstance(package_reference, PackageReference)
+        assert package_reference.conan == self._conan_ref
+        return os.path.join(self.conan(), BUILD_FOLDER, package_reference.package_id)
+
+    def system_reqs(self):
+        return os.path.join(self.conan(), SYSTEM_REQS_FOLDER, SYSTEM_REQS)
+
+    def system_reqs_package(self, package_reference):
+        assert isinstance(package_reference, PackageReference)
+        assert package_reference.conan == self._conan_ref
+        return os.path.join(self.conan(), SYSTEM_REQS_FOLDER,
+                            package_reference.package_id, SYSTEM_REQS)
+
+    def packages(self):
+        return os.path.join(self.conan(), PACKAGES_FOLDER)
+
+    @short_path
+    def package(self, package_reference):
+        assert isinstance(package_reference, PackageReference)
+        assert package_reference.conan == self._conan_ref
+        return os.path.join(self.conan(), PACKAGES_FOLDER, package_reference.package_id)
+
+    def scm_folder(self):
+        return os.path.join(self.conan(), SCM_FOLDER)
+
+    def package_metadata(self):
+        return os.path.join(self.conan(), PACKAGE_METADATA)
diff --git a/conans/paths/package_layouts/package_editable_layout.py b/conans/paths/package_layouts/package_editable_layout.py
new file mode 100644
index 000000000..15933f0a9
--- /dev/null
+++ b/conans/paths/package_layouts/package_editable_layout.py
@@ -0,0 +1,49 @@
+# coding=utf-8
+
+import os
+
+from conans.model.ref import ConanFileReference
+from conans.model.ref import PackageReference
+from conans.paths import CONANFILE, CONAN_PACKAGE_LAYOUT_FILE
+from conans.util.files import load
+
+
+class PackageEditableLayout(object):
+
+    def __init__(self, linked_package_file, conan_ref):
+        assert isinstance(conan_ref, ConanFileReference)
+        self._conan_ref = conan_ref
+        self._base_folder = os.path.normpath(load(linked_package_file))
+
+    def conan(self):
+        """ Returns the base folder for this package reference """
+        return self._base_folder
+
+    def conanfile(self):
+        """ Path to the conanfile. We can agree that an editable package
+            needs to be a Conan package
+        """
+        return os.path.join(self.conan(), CONANFILE)
+
+    def editable_package_layout_file(self):
+        return os.path.join(self.conan(), CONAN_PACKAGE_LAYOUT_FILE)
+
+    def export(self):
+        raise RuntimeError("Operation not allowed on a package installed as editable")
+
+    def export_sources(self):
+        raise RuntimeError("Operation not allowed on a package installed as editable")
+
+    def source(self):
+        raise RuntimeError("Operation not allowed on a package installed as editable")
+
+    def package(self, package_reference):
+        assert isinstance(package_reference, PackageReference)
+        assert package_reference.conan == self._conan_ref
+        raise RuntimeError("Operation not allowed on a package installed as editable")
+
+    def package_metadata(self):
+        # FIXME: I know that downstream there is an except for IOError
+        raise IOError("Package metadata is not available for editable packages")
+
+
diff --git a/conans/paths/simple_paths.py b/conans/paths/simple_paths.py
new file mode 100644
index 000000000..225f496ba
--- /dev/null
+++ b/conans/paths/simple_paths.py
@@ -0,0 +1,106 @@
+import os
+
+from conans.errors import ConanException
+from conans.model.ref import ConanFileReference
+from conans.paths import LINKED_PACKAGE_SENTINEL, is_case_insensitive_os
+from conans.paths.package_layouts.package_cache_layout import PackageCacheLayout
+from conans.paths.package_layouts.package_editable_layout import PackageEditableLayout
+
+if is_case_insensitive_os():
+    def check_ref_case(conan_reference, store_folder):
+        if not os.path.exists(store_folder):
+            return
+
+        tmp = store_folder
+        for part in conan_reference.dir_repr().split("/"):
+            items = os.listdir(tmp)
+            try:
+                idx = [item.lower() for item in items].index(part.lower())
+                if part != items[idx]:
+                    raise ConanException("Requested '%s' but found case incompatible '%s'\n"
+                                         "Case insensitive filesystem can't manage this"
+                                         % (str(conan_reference), items[idx]))
+                tmp = os.path.normpath(tmp + os.sep + part)
+            except ValueError:
+                return
+else:
+    def check_ref_case(conan_reference, store_folder):  # @UnusedVariable
+        pass
+
+
+class SimplePaths(object):
+    """
+    Generate Conan paths. Handles the conan domain path logic. NO DISK ACCESS, just
+    path logic responsability
+    """
+    def __init__(self, store_folder):
+        self._store_folder = store_folder
+
+    @property
+    def store(self):
+        return self._store_folder
+
+    def _build_path_to_base_folder(self, conan_reference):
+        return os.path.normpath(os.path.join(self.store, conan_reference.dir_repr()))
+
+    def _build_path_to_linked_folder_sentinel(self, conan_reference):
+        base_folder = self._build_path_to_base_folder(conan_reference)
+        linked_package_file = os.path.join(base_folder, LINKED_PACKAGE_SENTINEL)
+        return linked_package_file
+
+    def package_layout(self, conan_reference, short_paths=False):
+        assert isinstance(conan_reference, ConanFileReference), \
+            "It is a {}".format(type(conan_reference))
+        linked_package_file = self._build_path_to_linked_folder_sentinel(conan_reference)
+        if os.path.exists(linked_package_file):
+            return PackageEditableLayout(linked_package_file=linked_package_file,
+                                         conan_ref=conan_reference)
+        else:
+            check_ref_case(conan_reference, self.store)
+            base_folder = self._build_path_to_base_folder(conan_reference)
+            return PackageCacheLayout(base_folder=base_folder,
+                                      conan_ref=conan_reference, short_paths=short_paths)
+
+    def conan(self, conan_reference):
+        """ the base folder for this package reference, for each ConanFileReference
+        """
+        return self.package_layout(conan_reference).conan()
+
+    def export(self, conan_reference):
+        return self.package_layout(conan_reference).export()
+
+    def export_sources(self, conan_reference, short_paths=False):
+        return self.package_layout(conan_reference, short_paths).export_sources()
+
+    def source(self, conan_reference, short_paths=False):
+        return self.package_layout(conan_reference, short_paths).source()
+
+    def conanfile(self, conan_reference):
+        return self.package_layout(conan_reference).conanfile()
+
+    def builds(self, conan_reference):
+        return self.package_layout(conan_reference).builds()
+
+    def build(self, package_reference, short_paths=False):
+        return self.package_layout(package_reference.conan, short_paths).build(package_reference)
+
+    def system_reqs(self, conan_reference):
+        return self.package_layout(conan_reference).system_reqs()
+
+    def system_reqs_package(self, package_reference):
+        return self.package_layout(package_reference.conan).system_reqs_package(package_reference)
+
+    def packages(self, conan_reference):
+        return self.package_layout(conan_reference).packages()
+
+    def package(self, package_reference, short_paths=False):
+        return self.package_layout(package_reference.conan, short_paths).package(package_reference)
+
+    def scm_folder(self, conan_reference):
+        return self.package_layout(conan_reference).scm_folder()
+
+    def package_metadata(self, conan_reference):
+        return self.package_layout(conan_reference).package_metadata()
+
+    def installed_as_editable(self, conan_reference):
+        return isinstance(self.package_layout(conan_reference), PackageEditableLayout)
diff --git a/conans/server/store/disk_adapter.py b/conans/server/store/disk_adapter.py
index 6e635582b..6a1f8c7b9 100644
--- a/conans/server/store/disk_adapter.py
+++ b/conans/server/store/disk_adapter.py
@@ -4,7 +4,7 @@ import fasteners
 
 from conans.client.tools.env import no_op
 from conans.errors import NotFoundException
-from conans.paths import SimplePaths
+from conans.paths.simple_paths import SimplePaths
 from conans.server.store.server_store import REVISIONS_FILE
 from conans.util.files import decode_text, md5sum, path_exists, relative_dirs, rmdir
 
diff --git a/conans/server/store/server_store.py b/conans/server/store/server_store.py
index 0e1ad2c01..674be4888 100644
--- a/conans/server/store/server_store.py
+++ b/conans/server/store/server_store.py
@@ -4,7 +4,8 @@ from os.path import join, normpath, relpath
 from conans import DEFAULT_REVISION_V1
 from conans.errors import ConanException, NotFoundException
 from conans.model.ref import ConanFileReference, PackageReference
-from conans.paths import EXPORT_FOLDER, PACKAGES_FOLDER, SimplePaths
+from conans.paths import EXPORT_FOLDER, PACKAGES_FOLDER
+from conans.paths.simple_paths import SimplePaths
 from conans.server.revision_list import RevisionList
 
 REVISIONS_FILE = "revisions.txt"


Test patch:
diff --git a/conans/test/functional/editable/__init__.py b/conans/test/functional/editable/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/conans/test/functional/editable/commands/__init__.py b/conans/test/functional/editable/commands/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/conans/test/functional/editable/commands/info_on_child_test.py b/conans/test/functional/editable/commands/info_on_child_test.py
new file mode 100644
index 000000000..46ada1273
--- /dev/null
+++ b/conans/test/functional/editable/commands/info_on_child_test.py
@@ -0,0 +1,92 @@
+# coding=utf-8
+
+import os
+import textwrap
+import unittest
+
+from parameterized import parameterized
+
+from conans.model.ref import ConanFileReference
+from conans.paths import CONAN_PACKAGE_LAYOUT_FILE
+from conans.test.utils.tools import TestClient
+
+
+class InfoCommandTest(unittest.TestCase):
+    conanfile_base = textwrap.dedent("""\
+        from conans import ConanFile
+
+        class APck(ConanFile):
+            {body}
+        """)
+    conanfile = conanfile_base.format(body="pass")
+
+    conan_package_layout = textwrap.dedent("""\
+        [includedirs]
+        src/include
+        """)
+
+    def setUp(self):
+        self.ref_parent = ConanFileReference.loads("parent/version@user/name")
+        self.ref = ConanFileReference.loads('lib/version@user/name')
+        self.ref_child = ConanFileReference.loads('child/version@user/name')
+
+        self.t = TestClient(path_with_spaces=False)
+        self.t.save(files={'conanfile.py': self.conanfile})
+        self.t.run('create . {}'.format(self.ref_parent))
+
+        lib_folder = os.path.join(self.t.current_folder, 'lib')
+        self.t.save(files={'conanfile.py':
+                           self.conanfile_base.format(
+                               body='requires = "{}"'.format(self.ref_parent)),
+                           CONAN_PACKAGE_LAYOUT_FILE: self.conan_package_layout, },
+                    path=lib_folder)
+        self.t.run('link "{}" {}'.format(lib_folder, self.ref))
+        self.assertTrue(self.t.client_cache.installed_as_editable(self.ref))
+
+        # Create child
+        self.t.save(files={'conanfile.py':
+                           self.conanfile_base.format(body='requires = "{}"'.format(self.ref))})
+        self.t.run('export . {}'.format(self.ref_child))
+
+    def tearDown(self):
+        self.t.run('link {} --remove'.format(self.ref))
+        self.assertFalse(self.t.client_cache.installed_as_editable(self.ref))
+        self.assertFalse(os.listdir(self.t.client_cache.conan(self.ref)))
+
+    @parameterized.expand([(True, ), (False, )])
+    def test_no_args(self, use_local_path):
+        args = "." if use_local_path else self.ref_child
+        project_name = "conanfile.py" if use_local_path else self.ref_child
+
+        self.t.run('info {}'.format(args))
+        self.assertIn("lib/version@user/name\n"
+                      "    ID: e94ed0d45e4166d2f946107eaa208d550bf3691e\n"
+                      "    BuildID: None\n"
+                      "    Remote: None\n"
+                      "    Recipe: Editable\n"
+                      "    Binary: Editable\n"
+                      "    Binary remote: None\n"
+                      "    Required by:\n"
+                      "        {}\n"
+                      "    Requires:\n"
+                      "        parent/version@user/name\n".format(project_name),
+                      self.t.out)
+
+    @parameterized.expand([(True,), (False,)])
+    def test_only_none(self, use_local_path):
+        args = "." if use_local_path else self.ref_child
+        project_name = "conanfile.py" if use_local_path else self.ref_child
+
+        self.t.run('info {} --only None'.format(args))
+        # Compare, order is not guaranteed
+        self.assertListEqual(sorted(str(self.t.out).splitlines()),
+                             sorted(["lib/version@user/name",
+                                     "parent/version@user/name",
+                                     str(project_name)]))
+
+    @parameterized.expand([(True,), (False,)])
+    def test_paths(self, use_local_path):
+        args = "." if use_local_path else self.ref_child
+        self.t.run('info {} --paths'.format(args), assert_error=True)
+        self.assertIn("ERROR: Operation not allowed on a package installed as editable", self.t.out)
+        # TODO: Cannot show paths for a linked/editable package... what to do here?
diff --git a/conans/test/functional/editable/commands/info_test.py b/conans/test/functional/editable/commands/info_test.py
new file mode 100644
index 000000000..eb88ad19f
--- /dev/null
+++ b/conans/test/functional/editable/commands/info_test.py
@@ -0,0 +1,94 @@
+# coding=utf-8
+
+import os
+import textwrap
+import unittest
+
+from conans.model.ref import ConanFileReference
+from conans.paths import CONAN_PACKAGE_LAYOUT_FILE
+from conans.test.utils.tools import TestClient
+
+
+class LinkedPackageAsProject(unittest.TestCase):
+    conanfile_base = textwrap.dedent("""\
+        from conans import ConanFile
+
+        class APck(ConanFile):
+            {body}
+        """)
+    conanfile = conanfile_base.format(body="pass")
+
+    conan_package_layout = textwrap.dedent("""\
+        [includedirs]
+        src/include
+        """)
+
+    def setUp(self):
+        self.ref_parent = ConanFileReference.loads("parent/version@user/name")
+        self.reference = ConanFileReference.loads('lib/version@user/name')
+
+        self.t = TestClient()
+        self.t.save(files={'conanfile.py': self.conanfile})
+        self.t.run('create . {}'.format(self.ref_parent))
+
+        self.t.save(files={'conanfile.py':
+                           self.conanfile_base.format(
+                               body='requires = "{}"'.format(self.ref_parent)),
+                           CONAN_PACKAGE_LAYOUT_FILE: self.conan_package_layout, })
+        self.t.run('link . {}'.format(self.reference))
+        self.assertTrue(self.t.client_cache.installed_as_editable(self.reference))
+
+    def tearDown(self):
+        self.t.run('link {} --remove'.format(self.reference))
+        self.assertFalse(self.t.client_cache.installed_as_editable(self.reference))
+        self.assertFalse(os.listdir(self.t.client_cache.conan(self.reference)))
+
+
+class InfoCommandOnLocalWorkspaceTest(LinkedPackageAsProject):
+    """ Check that commands info/inspect running over an editable package work"""
+
+    def test_no_args(self):
+        self.t.run('info .')
+        self.assertIn("conanfile.py\n"
+                      "    ID: e94ed0d45e4166d2f946107eaa208d550bf3691e\n"
+                      "    BuildID: None\n"
+                      "    Requires:\n"
+                      "        parent/version@user/name\n", self.t.out)
+
+    def test_only_none(self):
+        self.t.run('info . --only None')
+        self.assertIn("parent/version@user/name\n"
+                      "conanfile.py", self.t.out)
+
+    def test_paths(self):
+        self.t.run('info . --paths')
+        self.assertIn("conanfile.py\n"
+                      "    ID: e94ed0d45e4166d2f946107eaa208d550bf3691e\n"
+                      "    BuildID: None\n"
+                      "    Requires:\n"
+                      "        parent/version@user/name\n", self.t.out)
+
+
+class InfoCommandUsingReferenceTest(LinkedPackageAsProject):
+
+    def test_no_args(self):
+        self.t.run('info {}'.format(self.reference))
+        self.assertIn("lib/version@user/name\n"
+                      "    ID: e94ed0d45e4166d2f946107eaa208d550bf3691e\n"
+                      "    BuildID: None\n"
+                      "    Remote: None\n"
+                      "    Recipe: Editable\n"
+                      "    Binary: Editable\n"
+                      "    Binary remote: None\n"
+                      "    Requires:\n"
+                      "        parent/version@user/name\n", self.t.out)
+
+    def test_only_none(self):
+        self.t.run('info {} --only None'.format(self.reference))
+        self.assertListEqual(sorted(str(self.t.out).splitlines()),
+                             sorted(["lib/version@user/name", "parent/version@user/name"]))
+
+    def test_paths(self):
+        self.t.run('info {} --paths'.format(self.reference), assert_error=True)
+        self.assertIn("Operation not allowed on a package installed as editable", self.t.out)
+        # TODO: Cannot show paths for a linked/editable package... what to do here?
diff --git a/conans/test/functional/editable/commands/inspect_test.py b/conans/test/functional/editable/commands/inspect_test.py
new file mode 100644
index 000000000..1b715250b
--- /dev/null
+++ b/conans/test/functional/editable/commands/inspect_test.py
@@ -0,0 +1,52 @@
+# coding=utf-8
+
+import os
+import textwrap
+import unittest
+
+from conans.model.ref import ConanFileReference
+from conans.paths import CONAN_PACKAGE_LAYOUT_FILE
+from conans.test.utils.tools import TestClient
+
+
+class InspectCommandTest(unittest.TestCase):
+    conanfile_base = textwrap.dedent("""\
+        from conans import ConanFile
+
+        class APck(ConanFile):
+            {body}
+        """)
+    conanfile = conanfile_base.format(body="pass")
+
+    conan_package_layout = textwrap.dedent("""\
+        [includedirs]
+        src/include
+        """)
+
+    def setUp(self):
+        self.ref_parent = ConanFileReference.loads("parent/version@user/name")
+        self.reference = ConanFileReference.loads('lib/version@user/name')
+
+        self.t = TestClient()
+        self.t.save(files={'conanfile.py': self.conanfile})
+        self.t.run('create . {}'.format(self.ref_parent))
+
+        self.t.save(files={'conanfile.py':
+                           self.conanfile_base.format(
+                               body='requires = "{}"'.format(self.ref_parent)),
+                           CONAN_PACKAGE_LAYOUT_FILE: self.conan_package_layout, })
+        self.t.run('link . {}'.format(self.reference))
+        self.assertTrue(self.t.client_cache.installed_as_editable(self.reference))
+
+    def tearDown(self):
+        self.t.run('link {} --remove'.format(self.reference))
+        self.assertFalse(self.t.client_cache.installed_as_editable(self.reference))
+        self.assertFalse(os.listdir(self.t.client_cache.conan(self.reference)))
+
+    def test_reference(self):
+        self.t.run('inspect {}'.format(self.reference))
+        self.assertIn("url: None", self.t.out)
+
+        self.t.save(files={'conanfile.py': self.conanfile_base.format(body='url ="hh"')})
+        self.t.run('inspect {}'.format(self.reference))
+        self.assertIn('url: hh', self.t.out)
diff --git a/conans/test/functional/editable/consume_header_only_test.py b/conans/test/functional/editable/consume_header_only_test.py
new file mode 100644
index 000000000..632c1a8b7
--- /dev/null
+++ b/conans/test/functional/editable/consume_header_only_test.py
@@ -0,0 +1,182 @@
+# coding=utf-8
+
+import os
+import tempfile
+import unittest
+import textwrap
+
+from parameterized import parameterized
+
+from conans.model.ref import ConanFileReference
+from conans.paths import LINKED_PACKAGE_SENTINEL, CONAN_PACKAGE_LAYOUT_FILE
+from conans.test import CONAN_TEST_FOLDER
+from conans.test.utils.tools import TestClient
+from conans.util.files import save
+
+
+class HeaderOnlyLibTestClient(TestClient):
+    header = textwrap.dedent("""\
+        #include <iostream>
+
+        void hello() {{
+            std::cout << "Hello {word}!" << std::endl;
+            std::cout << "...using {origin}" << std::endl;
+        }}
+        """)
+
+    conanfile = textwrap.dedent("""\
+        import os
+        from conans import ConanFile, tools
+        
+        class Pkg(ConanFile):
+            name = "MyLib"
+            version = "0.1"
+        
+            exports_sources = "*"
+        
+            def package(self):
+                self.copy("*.hpp", dst="include-inrepo", src="src/include-inrepo")
+                self.copy("*.hpp", dst="include-cache", src="src/include-cache")
+        
+            def package_info(self):
+                self.cpp_info.libs = ["MyLib", "otra", ]
+                self.cpp_info.defines = ["MyLibDEFINES",]
+                self.cpp_info.libdirs = ["MyLib-libdirs", ]
+                self.cpp_info.includedirs = ["src/include-local", ]
+        """)
+
+    conan_inrepo_layout = textwrap.dedent("""\
+        [includedirs]
+        src/include-inrepo
+        """)
+
+    conan_cache_layout = textwrap.dedent("""\
+        [MyLib:includedirs]
+        src/include-cache
+        """)
+
+    def __init__(self, use_repo_file, use_cache_file, *args, **kwargs):
+        super(HeaderOnlyLibTestClient, self).__init__(*args, **kwargs)
+
+        self.save({"conanfile.py": self.conanfile,
+                   "src/include-inrepo/hello.hpp": self.header.format(word="EDITABLE",
+                                                                      origin="inrepo"),
+                   "src/include-cache/hello.hpp": self.header.format(word="EDITABLE",
+                                                                     origin="cache"),
+                   "src/include-local/hello.hpp": self.header.format(word="EDITABLE",
+                                                                     origin="local")
+                   })
+
+        if use_repo_file:
+            self.save({CONAN_PACKAGE_LAYOUT_FILE: self.conan_inrepo_layout, })
+
+        if use_cache_file:
+            save(self.client_cache.default_editable_path, self.conan_cache_layout)
+
+    def update_hello_word(self, hello_word):
+        self.save({"src/include-inrepo/hello.hpp": self.header.format(word=hello_word,
+                                                                      origin='inrepo'),
+                   "src/include-cache/hello.hpp": self.header.format(word=hello_word,
+                                                                     origin='cache'),
+                   "src/include-local/hello.hpp": self.header.format(word=hello_word,
+                                                                     origin='local')})
+
+    def make_editable(self, full_reference):
+        conan_ref = ConanFileReference.loads(full_reference)
+        cache_dir = self.client_cache.conan(conan_ref)
+        save(os.path.join(cache_dir, LINKED_PACKAGE_SENTINEL), content=self.current_folder)
+
+
+class EditableReferenceTest(unittest.TestCase):
+
+    @parameterized.expand([(False, True), (True, False), (True, True), (False, False)])
+    def test_header_only(self, use_repo_file, use_cache_file):
+        # We need two clients sharing the same Conan cache
+        base_folder = tempfile.mkdtemp(suffix='conans', dir=CONAN_TEST_FOLDER)
+
+        # Editable project
+        client_editable = HeaderOnlyLibTestClient(use_repo_file=use_repo_file,
+                                                  use_cache_file=use_cache_file,
+                                                  base_folder=base_folder)
+        client_editable.make_editable(full_reference="MyLib/0.1@user/editable")
+
+        # Consumer project
+        client = TestClient(base_folder=base_folder)
+        conanfile_py = """
+import os
+from conans import ConanFile, CMake
+
+class TestConan(ConanFile):
+    name = "pkg"
+    version = "0.0"
+
+    requires = "MyLib/0.1@user/editable"
+    settings = "os", "compiler", "build_type", "arch"
+    generators = "cmake"
+    exports_sources = "src/*"
+
+    def build(self):
+        cmake = CMake(self)
+        cmake.configure(source_folder="src")
+        cmake.build()
+
+        os.chdir("bin")
+        self.run(".%shello" % os.sep)
+
+"""
+        cmakelists = """
+cmake_minimum_required(VERSION 2.8.12)
+project(MyHello CXX)
+
+include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
+conan_basic_setup()
+
+add_executable(hello main.cpp)
+"""
+        main_cpp = """
+#include "hello.hpp"
+
+int main() {
+    hello();
+}
+"""
+
+        client.save({"conanfile.py": conanfile_py,
+                     "src/CMakeLists.txt": cmakelists,
+                     "src/main.cpp": main_cpp})
+
+        # Build consumer project
+        client.run("create . pkg/0.0@user/testing")
+        self.assertIn("    MyLib/0.1@user/editable from local cache - Editable", client.out)
+        self.assertIn("    MyLib/0.1@user/editable:"
+                      "5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9 - Editable", client.out)
+        self.assertIn("Hello EDITABLE!", client.out)
+        if use_repo_file:  # Repo file will override folders from cache
+            self.assertIn("...using inrepo", client.out)
+            self.assertNotIn("...using cache", client.out)
+            self.assertNotIn("...using local", client.out)
+        elif use_cache_file:
+            self.assertIn("...using cache", client.out)
+            self.assertNotIn("...using inrepo", client.out)
+            self.assertNotIn("...using local", client.out)
+        else:
+            self.assertIn("...using local", client.out)
+            self.assertNotIn("...using inrepo", client.out)
+            self.assertNotIn("...using cache", client.out)
+
+        # Modify editable and build again
+        client_editable.update_hello_word(hello_word="EDITED")
+        client.run("create . pkg/0.0@user/testing")
+        self.assertIn("Hello EDITED!", client.out)
+        if use_repo_file:  # Repo file will override folders from cache
+            self.assertIn("...using inrepo", client.out)
+            self.assertNotIn("...using cache", client.out)
+            self.assertNotIn("...using local", client.out)
+        elif use_cache_file:
+            self.assertIn("...using cache", client.out)
+            self.assertNotIn("...using inrepo", client.out)
+            self.assertNotIn("...using local", client.out)
+        else:
+            self.assertIn("...using local", client.out)
+            self.assertNotIn("...using inrepo", client.out)
+            self.assertNotIn("...using cache", client.out)
diff --git a/conans/test/functional/editable/consume_settings_and_options_test.py b/conans/test/functional/editable/consume_settings_and_options_test.py
new file mode 100644
index 000000000..0c55bba86
--- /dev/null
+++ b/conans/test/functional/editable/consume_settings_and_options_test.py
@@ -0,0 +1,147 @@
+# coding=utf-8
+
+import os
+import unittest
+import tempfile
+import itertools
+from parameterized import parameterized
+
+from conans.test.utils.tools import TestClient
+from conans.test import CONAN_TEST_FOLDER
+from conans.util.files import save
+from conans.paths import LINKED_PACKAGE_SENTINEL, CONAN_PACKAGE_LAYOUT_FILE
+from conans.model.ref import ConanFileReference
+
+
+class HeaderOnlyLibTestClient(TestClient):
+    header = """
+        #include <iostream>
+
+        void hello() {{
+            std::cout << "Hello {build_type}!" << std::endl;
+            std::cout << " - options.shared: {shared}" << std::endl;
+        }}
+        """
+
+    conanfile = """
+import os
+from conans import ConanFile, tools
+
+class Pkg(ConanFile):
+    name = "MyLib"
+    version = "0.1"
+
+    exports_sources = "*"
+    settings = "build_type"
+
+    options = {"shared": [True, False], }
+    default_options = {"shared": True, }
+
+    def package(self):
+        self.copy("*.hpp", dst="include", src="src/include")
+
+    def package_info(self):
+        self.cpp_info.libs = ["MyLib", "otra", ]
+        self.cpp_info.defines = ["MyLibDEFINES",]
+        self.cpp_info.libdirs = ["MyLib-libdirs", ]
+        self.cpp_info.includedirs = ["MyLib-includedirs", "dddd", ]
+
+    """
+
+    conan_package_layout = """
+[{namespace}includedirs]
+src/include/{{settings.build_type}}/{{options.shared}}
+"""
+
+    def __init__(self, use_repo_file, *args, **kwargs):
+        super(HeaderOnlyLibTestClient, self).__init__(*args, **kwargs)
+
+        files = {"conanfile.py": self.conanfile,
+                 "src/include/Debug/True/hello.hpp": self.header.format(build_type="Debug",
+                                                                        shared="True"),
+                 "src/include/Debug/False/hello.hpp": self.header.format(build_type="Debug",
+                                                                         shared="False"),
+                 "src/include/Release/True/hello.hpp": self.header.format(build_type="Release",
+                                                                          shared="True"),
+                 "src/include/Release/False/hello.hpp": self.header.format(build_type="Release",
+                                                                           shared="False"),
+                 }
+
+        if use_repo_file:
+            files[CONAN_PACKAGE_LAYOUT_FILE] = self.conan_package_layout.format(namespace="")
+        else:
+            save(self.client_cache.default_editable_path,
+                 self.conan_package_layout.format(namespace="MyLib:"))
+
+        self.save(files)
+
+    def make_editable(self, full_reference):
+        conan_ref = ConanFileReference.loads(full_reference)
+        cache_dir = self.client_cache.conan(conan_ref)
+        save(os.path.join(cache_dir, LINKED_PACKAGE_SENTINEL), content=self.current_folder)
+
+
+class SettingsAndOptionsTest(unittest.TestCase):
+
+    @parameterized.expand(itertools.product(["Debug", "Release", ],  # build_type
+                                            [True, False, ],  # shared
+                                            [True, False, ]))  # use_repo_file
+    def test_settings_options(self, build_type, shared, use_repo_file):
+        # We need two clients sharing the same Conan cache
+        base_folder = tempfile.mkdtemp(suffix='conans', dir=CONAN_TEST_FOLDER)
+
+        # Editable project
+        client_editable = HeaderOnlyLibTestClient(use_repo_file=use_repo_file,
+                                                  base_folder=base_folder)
+        client_editable.make_editable(full_reference="MyLib/0.1@user/editable")
+
+        # Consumer project
+        client = TestClient(base_folder=base_folder)
+        conanfile_txt = """
+import os
+from conans import ConanFile, CMake
+
+class TestConan(ConanFile):
+    name = "pkg"
+    version = "0.0"
+
+    requires = "MyLib/0.1@user/editable"
+    settings = "os", "compiler", "build_type", "arch"
+    generators = "cmake"
+    exports_sources = "src/*"
+
+    def build(self):
+        cmake = CMake(self)
+        cmake.configure(source_folder="src")
+        cmake.build()
+
+        os.chdir("bin")
+        self.run(".%shello" % os.sep)
+
+"""
+        cmakelists = """
+cmake_minimum_required(VERSION 2.8.12)
+project(MyHello CXX)
+
+include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
+conan_basic_setup()
+
+add_executable(hello main.cpp)
+"""
+        main_cpp = """
+#include "hello.hpp"
+
+int main() {
+    hello();
+}
+"""
+
+        client.save({"conanfile.py": conanfile_txt,
+                     "src/CMakeLists.txt": cmakelists,
+                     "src/main.cpp": main_cpp})
+
+        # Build consumer project
+        client.run("create . pkg/0.0@user/testing -s build_type={} -o MyLib:shared={}".format(build_type, str(shared)))
+        self.assertIn("    MyLib/0.1@user/editable from local cache - Editable", client.out)
+        self.assertIn("Hello {}!".format(build_type), client.out)
+        self.assertIn(" - options.shared: {}".format(shared), client.out)
diff --git a/conans/test/functional/editable/forbidden_commands_test.py b/conans/test/functional/editable/forbidden_commands_test.py
new file mode 100644
index 000000000..945522676
--- /dev/null
+++ b/conans/test/functional/editable/forbidden_commands_test.py
@@ -0,0 +1,57 @@
+# coding=utf-8
+
+import textwrap
+import unittest
+
+from conans.model.ref import ConanFileReference
+from conans.paths import CONAN_PACKAGE_LAYOUT_FILE
+from conans.test.utils.tools import TestClient, TestServer
+
+
+class ForbiddenCommandsTest(unittest.TestCase):
+    conanfile = textwrap.dedent("""\
+        from conans import ConanFile
+
+        class APck(ConanFile):
+            pass
+        """)
+
+    def setUp(self):
+        self.reference = ConanFileReference.loads('lib/version@user/name')
+
+        test_server = TestServer()
+        self.servers = {"default": test_server}
+        self.t = TestClient(servers=self.servers, users={"default": [("lasote", "mypass")]})
+        self.t.save(files={'conanfile.py': self.conanfile,
+                           CONAN_PACKAGE_LAYOUT_FILE: "", })
+        self.t.run('link . {}'.format(self.reference))
+        self.assertTrue(self.t.client_cache.installed_as_editable(self.reference))
+
+    def test_export(self):
+        self.t.run('export . {}'.format(self.reference), assert_error=True)
+        self.assertIn("Operation not allowed on a package installed as editable", self.t.out)
+
+    def test_create(self):
+        self.t.run('create . {}'.format(self.reference), assert_error=True)
+        self.assertIn("Operation not allowed on a package installed as editable", self.t.out)
+
+    def test_create_update(self):
+        self.t.run('create . {} --update'.format(self.reference), assert_error=True)
+        self.assertIn("Operation not allowed on a package installed as editable", self.t.out)
+
+    def test_upload(self):
+        self.t.run('upload --force {}'.format(self.reference), assert_error=True)
+        self.assertIn("Operation not allowed on a package installed as editable", self.t.out)
+
+    def test_export_pkg(self):
+        self.t.run('export-pkg -f . {}'.format(self.reference), assert_error=True)
+        self.assertIn("Operation not allowed on a package installed as editable", self.t.out)
+
+    def test_copy(self):
+        self.t.run('copy --force {} ouser/ochannel'.format(self.reference), assert_error=True)
+        self.assertIn("Operation not allowed on a package installed as editable", self.t.out)
+
+    def test_remove(self):
+        self.t.run('remove {} --force'.format(self.reference), assert_error=True)
+        self.assertIn("Package 'lib/version@user/name' is installed as editable, unlink it first "
+                      "using command 'conan link lib/version@user/name --remove'", self.t.out)
diff --git a/conans/test/functional/editable/graph_related_test.py b/conans/test/functional/editable/graph_related_test.py
new file mode 100644
index 000000000..33c0c714a
--- /dev/null
+++ b/conans/test/functional/editable/graph_related_test.py
@@ -0,0 +1,136 @@
+# coding=utf-8
+
+import os
+import textwrap
+import unittest
+from parameterized import parameterized
+
+from conans.model.ref import ConanFileReference
+from conans.paths import CONAN_PACKAGE_LAYOUT_FILE
+from conans.test.utils.tools import TestClient, TestServer
+
+
+conanfile_base = textwrap.dedent("""\
+    from conans import ConanFile
+
+    class APck(ConanFile):
+        {body}
+    """)
+conanfile = conanfile_base.format(body="pass")
+
+conan_package_layout = textwrap.dedent("""\
+    [includedirs]
+    src/include
+    """)
+
+
+class EmptyCacheTestMixin(object):
+    """ Will check that the cache after using the link is empty """
+    def setUp(self):
+        self.servers = {"default": TestServer()}
+        self.t = TestClient(servers=self.servers, users={"default": [("lasote", "mypass")]},
+                            path_with_spaces=False)
+        self.reference = ConanFileReference.loads('lib/version@user/channel')
+        self.assertFalse(os.path.exists(self.t.client_cache.conan(self.reference)))
+
+    def tearDown(self):
+        self.t.run('link {} --remove'.format(self.reference))
+        self.assertFalse(self.t.client_cache.installed_as_editable(self.reference))
+        self.assertFalse(os.listdir(self.t.client_cache.conan(self.reference)))
+
+
+class ExistingCacheTestMixin(object):
+    """ Will check that the cache after using the link contains the same data as before """
+    def setUp(self):
+        self.servers = {"default": TestServer()}
+        self.t = TestClient(servers=self.servers, users={"default": [("lasote", "mypass")]},
+                            path_with_spaces=False)
+        self.reference = ConanFileReference.loads('lib/version@user/channel')
+        self.t.save(files={'conanfile.py': conanfile})
+        self.t.run('create . {}'.format(self.reference))
+        self.assertTrue(os.path.exists(self.t.client_cache.conan(self.reference)))
+        self.assertListEqual(sorted(os.listdir(self.t.client_cache.conan(self.reference))),
+                             ['build', 'export', 'export_source', 'locks', 'metadata.json',
+                              'package', 'source'])
+
+    def tearDown(self):
+        self.t.run('link {} --remove'.format(self.reference))
+        self.assertTrue(os.path.exists(self.t.client_cache.conan(self.reference)))
+        self.assertListEqual(sorted(os.listdir(self.t.client_cache.conan(self.reference))),
+                             ['build', 'export', 'export_source', 'locks', 'metadata.json',
+                              'package', 'source'])
+
+
+class RelatedToGraphBehavior(object):
+
+    def test_do_nothing(self):
+        self.t.save(files={'conanfile.py': conanfile,
+                           CONAN_PACKAGE_LAYOUT_FILE: conan_package_layout, })
+        self.t.run('link . {}'.format(self.reference))
+        self.assertTrue(self.t.client_cache.installed_as_editable(self.reference))
+
+    @parameterized.expand([(True, ), (False, )])
+    def test_install_requirements(self, update):
+        # Create a parent and remove it from cache
+        ref_parent = ConanFileReference.loads("parent/version@lasote/channel")
+        self.t.save(files={'conanfile.py': conanfile})
+        self.t.run('create . {}'.format(ref_parent))
+        self.t.run('upload {} --all'.format(ref_parent))
+        self.t.run('remove {} --force'.format(ref_parent))
+        self.assertFalse(os.path.exists(self.t.client_cache.conan(ref_parent)))
+
+        # Create our project and link it
+        self.t.save(files={'conanfile.py':
+                           conanfile_base.format(body='requires = "{}"'.format(ref_parent)),
+                           CONAN_PACKAGE_LAYOUT_FILE: conan_package_layout, })
+        self.t.run('link . {}'.format(self.reference))
+
+        # Install our project and check that everything is in place
+        update = ' --update' if update else ''
+        self.t.run('install {}{}'.format(self.reference, update))
+        self.assertIn("    lib/version@user/channel from local cache - Editable", self.t.out)
+        self.assertIn("    parent/version@lasote/channel from 'default' - Downloaded",
+                      self.t.out)
+        self.assertTrue(os.path.exists(self.t.client_cache.conan(ref_parent)))
+
+    @parameterized.expand([(True,), (False,)])
+    def test_middle_graph(self, update):
+        # Create a parent and remove it from cache
+        ref_parent = ConanFileReference.loads("parent/version@lasote/channel")
+        self.t.save(files={'conanfile.py': conanfile})
+        self.t.run('create . {}'.format(ref_parent))
+        self.t.run('upload {} --all'.format(ref_parent))
+        self.t.run('remove {} --force'.format(ref_parent))
+        self.assertFalse(os.path.exists(self.t.client_cache.conan(ref_parent)))
+
+        # Create our project and link it
+        path_to_lib = os.path.join(self.t.current_folder, 'lib')
+        self.t.save(files={'conanfile.py':
+                           conanfile_base.format(body='requires = "{}"'.format(ref_parent)),
+                           CONAN_PACKAGE_LAYOUT_FILE: conan_package_layout, },
+                    path=path_to_lib)
+        self.t.run('link "{}" {}'.format(path_to_lib, self.reference))
+
+        # Create a child an install it (in other folder, do not override the link!)
+        path_to_child = os.path.join(self.t.current_folder, 'child')
+        ref_child = ConanFileReference.loads("child/version@lasote/channel")
+        self.t.save(files={'conanfile.py': conanfile_base.
+                    format(body='requires = "{}"'.format(self.reference)), },
+                    path=path_to_child)
+
+        update = ' --update' if update else ''
+        self.t.run('create "{}" {} {}'.format(path_to_child, ref_child, update))
+        child_remote = 'No remote' if update else 'Cache'
+        self.assertIn("    child/version@lasote/channel from local cache - {}".format(child_remote),
+                      self.t.out)
+        self.assertIn("    lib/version@user/channel from local cache - Editable", self.t.out)
+        self.assertIn("    parent/version@lasote/channel from 'default' - Downloaded", self.t.out)
+        self.assertTrue(os.path.exists(self.t.client_cache.conan(ref_parent)))
+
+
+class CreateLinkOverEmptyCache(EmptyCacheTestMixin, RelatedToGraphBehavior, unittest.TestCase):
+    pass
+
+
+class CreateLinkOverExistingCache(ExistingCacheTestMixin, RelatedToGraphBehavior, unittest.TestCase):
+    pass
diff --git a/conans/test/functional/editable/link_create_test.py b/conans/test/functional/editable/link_create_test.py
new file mode 100644
index 000000000..ac383a4a9
--- /dev/null
+++ b/conans/test/functional/editable/link_create_test.py
@@ -0,0 +1,46 @@
+# coding=utf-8
+import textwrap
+import unittest
+
+from conans.model.ref import ConanFileReference
+from conans.paths import CONAN_PACKAGE_LAYOUT_FILE
+from conans.test.utils.tools import TestClient
+
+
+class CreateEditablePackageTest(unittest.TestCase):
+
+    conanfile_base = textwrap.dedent("""\
+        from conans import ConanFile
+
+        class APck(ConanFile):
+            {body}
+        """)
+    conanfile = conanfile_base.format(body="pass")
+
+    conan_package_layout = textwrap.dedent("""\
+        [includedirs]
+        src/include
+        """)
+
+    def test_install_ok(self):
+        reference = ConanFileReference.loads('lib/version@user/name')
+        t = TestClient()
+        t.save(files={'conanfile.py': self.conanfile, CONAN_PACKAGE_LAYOUT_FILE: ""})
+        t.run('link . {}'.format(reference))
+        self.assertIn("Reference 'lib/version@user/name' linked to directory '", t.out)
+
+    def test_install_wrong_reference(self):
+        reference = ConanFileReference.loads('lib/version@user/name')
+
+        t = TestClient()
+        t.save(files={'conanfile.py': textwrap.dedent("""\
+            from conans import ConanFile
+
+            class Pck(ConanFile):
+                name = "lib"
+                version = "version"
+            """)})
+        t.run('export  . {}'.format(reference))
+        t.run('link . wrong/version@user/channel', assert_error=True)
+        self.assertIn("ERROR: Name and version from reference (wrong/version@user/channel) and "
+                      "target conanfile.py (lib/version) must match", t.out)
diff --git a/conans/test/functional/editable/link_remove_test.py b/conans/test/functional/editable/link_remove_test.py
new file mode 100644
index 000000000..8116a3f28
--- /dev/null
+++ b/conans/test/functional/editable/link_remove_test.py
@@ -0,0 +1,37 @@
+# coding=utf-8
+
+import textwrap
+import unittest
+
+from conans.model.ref import ConanFileReference
+from conans.paths import CONAN_PACKAGE_LAYOUT_FILE
+from conans.test.utils.tools import TestClient
+
+
+class RemoveEditablePackageTest(unittest.TestCase):
+    conanfile = textwrap.dedent("""\
+        from conans import ConanFile
+
+        class APck(ConanFile):
+            pass
+        """)
+
+    def setUp(self):
+        self.reference = ConanFileReference.loads('lib/version@user/name')
+
+        self.t = TestClient()
+        self.t.save(files={'conanfile.py': self.conanfile,
+                           CONAN_PACKAGE_LAYOUT_FILE: "", })
+        self.t.run('link . {}'.format(self.reference))
+        self.assertTrue(self.t.client_cache.installed_as_editable(self.reference))
+
+    def test_unlink(self):
+        self.t.run('link {} --remove'.format(self.reference))
+        self.assertIn("Removed linkage for reference '{}'".format(self.reference), self.t.out)
+        self.assertFalse(self.t.client_cache.installed_as_editable(self.reference))
+
+    def test_unlink_not_linked(self):
+        reference = 'otherlib/version@user/name'
+        self.t.run('search {}'.format(reference), assert_error=True)
+        self.t.run('link {} --remove'.format(reference))
+        self.assertIn("Reference '{}' was not installed as editable".format(reference), self.t.out)
diff --git a/conans/test/functional/old/package_copier_test.py b/conans/test/functional/old/package_copier_test.py
index a0aa19cd3..add2dd2c3 100644
--- a/conans/test/functional/old/package_copier_test.py
+++ b/conans/test/functional/old/package_copier_test.py
@@ -5,7 +5,7 @@ import unittest
 from conans.client.cmd.copy import package_copy
 from conans.client.userio import UserIO
 from conans.model.ref import ConanFileReference, PackageReference
-from conans.paths import SimplePaths
+from conans.paths.simple_paths import SimplePaths
 from conans.test.utils.test_files import temp_folder
 from conans.test.utils.tools import TestBufferConanOutput
 from conans.util.files import load, mkdir, save
diff --git a/conans/test/functional/old/paths_test.py b/conans/test/functional/old/paths_test.py
index 6fe72b0e7..59f079b1a 100644
--- a/conans/test/functional/old/paths_test.py
+++ b/conans/test/functional/old/paths_test.py
@@ -9,8 +9,8 @@ from parameterized import parameterized
 
 from conans.client.tools.env import environment_append
 from conans.model.ref import ConanFileReference, PackageReference
-from conans.paths import (BUILD_FOLDER, EXPORT_FOLDER, PACKAGES_FOLDER, SimplePaths,
-                          conan_expand_user)
+from conans.paths import BUILD_FOLDER, EXPORT_FOLDER, PACKAGES_FOLDER, conan_expand_user
+from conans.paths.simple_paths import SimplePaths
 from conans.test.utils.test_files import temp_folder
 from conans.util.windows import path_shortener
 
@@ -32,7 +32,7 @@ class PathsTest(unittest.TestCase):
         folder = temp_folder()
         paths = SimplePaths(folder)
         self.assertEqual(paths._store_folder, folder)
-        conan_ref = ConanFileReference.loads("opencv/2.4.10 @ lasote /testing")
+        conan_ref = ConanFileReference.loads("opencv/2.4.10@lasote/testing")
         package_ref = PackageReference(conan_ref, "456fa678eae68")
         expected_base = os.path.join(folder, os.path.sep.join(["opencv", "2.4.10",
                                                                "lasote", "testing"]))
@@ -84,4 +84,3 @@ class PathShortenerTest(unittest.TestCase):
 
             self.assertEqual(self.home_short in r, short_paths)
             self.assertEqual(self.home in r, not short_paths)
-
diff --git a/conans/test/integration/manifest_validation_test.py b/conans/test/integration/manifest_validation_test.py
index d958c3c4a..b295e71aa 100644
--- a/conans/test/integration/manifest_validation_test.py
+++ b/conans/test/integration/manifest_validation_test.py
@@ -5,7 +5,8 @@ from parameterized.parameterized import parameterized
 
 from conans.model.manifest import FileTreeManifest
 from conans.model.ref import ConanFileReference, PackageReference
-from conans.paths import CONANFILE, CONAN_MANIFEST, SimplePaths
+from conans.paths import CONANFILE, CONAN_MANIFEST
+from conans.paths.simple_paths import SimplePaths
 from conans.test.utils.test_files import temp_folder
 from conans.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient, TestServer
 from conans.util.files import load, md5, save
diff --git a/conans/test/unittests/model/editable_cpp_info/__init__.py b/conans/test/unittests/model/editable_cpp_info/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/conans/test/unittests/model/editable_cpp_info/apply_test.py b/conans/test/unittests/model/editable_cpp_info/apply_test.py
new file mode 100644
index 000000000..11312a056
--- /dev/null
+++ b/conans/test/unittests/model/editable_cpp_info/apply_test.py
@@ -0,0 +1,77 @@
+# coding=utf-8
+
+import os
+import textwrap
+import unittest
+from collections import namedtuple
+
+import six
+
+from conans.model.editable_cpp_info import EditableCppInfo
+
+base_content = six.u(textwrap.dedent("""\
+    [{namespace}includedirs]
+    {path_prefix}dirs/includedirs
+
+    [{namespace}libdirs]
+    {path_prefix}dirs/libdirs
+
+    [{namespace}resdirs]
+    {path_prefix}dirs/resdirs
+
+    #[{namespace}bindirs]
+    #{path_prefix}dirs/bindirs
+
+    [{namespace}extradirs]
+    {path_prefix}dirs/extradirs
+    """))
+
+
+class ApplyEditableCppInfoTest(unittest.TestCase):
+    content = '\n\n'.join([
+        base_content.format(namespace="", path_prefix=""),
+        base_content.format(namespace="*:", path_prefix="all/"),
+        base_content.format(namespace="libA:", path_prefix="libA/")
+    ])
+
+    def test_require_no_namespace(self):
+        editable_cpp_info = EditableCppInfo.loads(content=self.content, require_namespace=False)
+
+        cpp_info = namedtuple('_', EditableCppInfo.cpp_info_dirs)
+        editable_cpp_info.apply_to('libA', cpp_info, base_path=None, settings=None, options=None)
+        self.assertTrue(editable_cpp_info.has_info_for('any-thing'))
+        self.assertListEqual(cpp_info.includedirs, [os.path.normpath('dirs/includedirs'), ])
+        self.assertListEqual(cpp_info.libdirs, [os.path.normpath('dirs/libdirs'), ])
+        self.assertListEqual(cpp_info.resdirs, [os.path.normpath('dirs/resdirs'), ])
+        self.assertListEqual(cpp_info.bindirs, [])
+
+    def test_require_namespace(self):
+        editable_cpp_info = EditableCppInfo.loads(content=self.content, require_namespace=True)
+        self.assertTrue(editable_cpp_info.has_info_for('libA', use_wildcard=True))
+        self.assertTrue(editable_cpp_info.has_info_for('libA', use_wildcard=False))
+        self.assertTrue(editable_cpp_info.has_info_for('libOther', use_wildcard=True))
+        self.assertFalse(editable_cpp_info.has_info_for('libOther', use_wildcard=False))
+
+        cpp_info = namedtuple('_', EditableCppInfo.cpp_info_dirs)
+
+        # Apply to 'libA' ==> existing one
+        editable_cpp_info.apply_to('libA', cpp_info, base_path=None, settings=None, options=None)
+        self.assertListEqual(cpp_info.includedirs, [os.path.normpath('libA/dirs/includedirs'), ])
+        self.assertListEqual(cpp_info.libdirs, [os.path.normpath('libA/dirs/libdirs'), ])
+        self.assertListEqual(cpp_info.resdirs, [os.path.normpath('libA/dirs/resdirs'), ])
+        self.assertListEqual(cpp_info.bindirs, [])
+
+        # Apply to non existing lib ==> uses wildcard ones
+        editable_cpp_info.apply_to('libOther', cpp_info, base_path=None, settings=None, options=None)
+        self.assertListEqual(cpp_info.includedirs, [os.path.normpath('all/dirs/includedirs'), ])
+        self.assertListEqual(cpp_info.libdirs, [os.path.normpath('all/dirs/libdirs'), ])
+        self.assertListEqual(cpp_info.resdirs, [os.path.normpath('all/dirs/resdirs'), ])
+        self.assertListEqual(cpp_info.bindirs, [])
+
+        # Apply to non existing lib ==> not found, and not wildcard allowed
+        editable_cpp_info.apply_to('libOther', cpp_info, base_path=None, settings=None, options=None,
+                                   use_wildcard=False)
+        self.assertListEqual(cpp_info.includedirs, [])
+        self.assertListEqual(cpp_info.libdirs, [])
+        self.assertListEqual(cpp_info.resdirs, [])
+        self.assertListEqual(cpp_info.bindirs, [])
diff --git a/conans/test/unittests/model/editable_cpp_info/parse_test.py b/conans/test/unittests/model/editable_cpp_info/parse_test.py
new file mode 100644
index 000000000..13fb16366
--- /dev/null
+++ b/conans/test/unittests/model/editable_cpp_info/parse_test.py
@@ -0,0 +1,91 @@
+# coding=utf-8
+
+import textwrap
+import unittest
+
+import six
+
+from conans.model.editable_cpp_info import EditableCppInfo
+
+base_content = six.u(textwrap.dedent("""\
+    [{namespace}includedirs]
+    {path_prefix}dirs/includedirs
+
+    [{namespace}libdirs]
+    {path_prefix}dirs/libdirs
+
+    [{namespace}resdirs]
+    {path_prefix}dirs/resdirs
+
+    #[{namespace}bindirs]
+    #{path_prefix}dirs/bindirs
+
+    [{namespace}extradirs]
+    {path_prefix}dirs/extradirs
+    """))
+
+
+class NoNamespaceFileTest(unittest.TestCase):
+    content = '\n\n'.join([
+        base_content.format(namespace="", path_prefix=""),
+        base_content.format(namespace="*:", path_prefix="all/"),
+        base_content.format(namespace="libA:", path_prefix="libA/")
+    ])
+
+    def test_read_empty_no_namespace(self):
+        editable_cpp_info = EditableCppInfo.loads(
+            content=base_content.format(namespace="libA:", path_prefix="libA/"),
+            require_namespace=False)
+        self.assertFalse(editable_cpp_info._uses_namespace)
+
+        data = editable_cpp_info._data
+        self.assertListEqual(sorted(data.keys()), sorted(EditableCppInfo.cpp_info_dirs))
+        self.assertFalse(len(data['includedirs']))
+        self.assertFalse(len(data['libdirs']))
+        self.assertFalse(len(data['resdirs']))
+        self.assertFalse(len(data['bindirs']))
+
+    def test_read_empty_namespace(self):
+        editable_cpp_info = EditableCppInfo.loads(
+            content=base_content.format(namespace="", path_prefix=""),
+            require_namespace=True)
+        self.assertTrue(editable_cpp_info._uses_namespace)
+        self.assertFalse(len(editable_cpp_info._data.keys()))
+
+    def test_no_namespace(self):
+        editable_cpp_info = EditableCppInfo.loads(self.content, require_namespace=False)
+        self.assertFalse(editable_cpp_info._uses_namespace)
+
+        data = editable_cpp_info._data
+        self.assertListEqual(sorted(data.keys()),
+                             sorted(EditableCppInfo.cpp_info_dirs))
+        self.assertListEqual(list(data['includedirs']), ['dirs/includedirs', ])
+        self.assertListEqual(list(data['libdirs']), ['dirs/libdirs', ])
+        self.assertListEqual(list(data['resdirs']), ['dirs/resdirs', ])
+        self.assertListEqual(list(data['bindirs']), [])
+
+    def test_namespace(self):
+        editable_cpp_info = EditableCppInfo.loads(self.content, require_namespace=True)
+        self.assertTrue(editable_cpp_info._uses_namespace)
+
+        self.assertListEqual(sorted(editable_cpp_info._data.keys()), sorted(['*', 'libA', ]))
+
+        # Check '*'
+        data = editable_cpp_info._data['*']
+        self.assertListEqual(sorted(data.keys()),
+                             sorted(EditableCppInfo.cpp_info_dirs))
+        self.assertListEqual(list(data['includedirs']), ['all/dirs/includedirs', ])
+        self.assertListEqual(list(data['libdirs']), ['all/dirs/libdirs', ])
+        self.assertListEqual(list(data['resdirs']), ['all/dirs/resdirs', ])
+        self.assertListEqual(list(data['bindirs']), [])
+
+        # Check 'libA'
+        data = editable_cpp_info._data['libA']
+        self.assertListEqual(sorted(data.keys()),
+                             sorted(EditableCppInfo.cpp_info_dirs))
+        self.assertListEqual(list(data['includedirs']), ['libA/dirs/includedirs', ])
+        self.assertListEqual(list(data['libdirs']), ['libA/dirs/libdirs', ])
+        self.assertListEqual(list(data['resdirs']), ['libA/dirs/resdirs', ])
+        self.assertListEqual(list(data['bindirs']), [])
+
+
diff --git a/conans/test/unittests/model/editable_cpp_info/work_on_items_test.py b/conans/test/unittests/model/editable_cpp_info/work_on_items_test.py
new file mode 100644
index 000000000..e4f61c64b
--- /dev/null
+++ b/conans/test/unittests/model/editable_cpp_info/work_on_items_test.py
@@ -0,0 +1,80 @@
+# coding=utf-8
+
+import os
+import unittest
+
+from parameterized import parameterized
+
+from conans.client.conf import default_settings_yml
+from conans.model.editable_cpp_info import EditableCppInfo
+from conans.model.settings import Settings
+
+
+def _make_abs(base_path, *args):
+    p = os.path.join(*args)
+    if base_path:
+        p = os.path.join(base_path, p)
+        p = os.path.abspath(p)
+    return p
+
+
+class WorkOnItemsTest(unittest.TestCase):
+
+    def test_empty(self):
+        self.assertEqual(".", EditableCppInfo._work_on_item("", None, None, None))
+
+    @parameterized.expand([(False,), (True,)])
+    def test_basic(self, make_abs):
+        base_path = os.path.dirname(__file__) if make_abs else None
+
+        self.assertIn(_make_abs(base_path, '.'),
+                      EditableCppInfo._work_on_item(".", base_path, None, None))
+        self.assertIn(_make_abs(base_path, 'src', 'include'),
+                      EditableCppInfo._work_on_item("src/include", base_path, None, None))
+        self.assertIn(_make_abs(base_path, '..', 'relative', 'include'),
+                      EditableCppInfo._work_on_item("../relative/include", base_path, None, None))
+        self.assertIn(_make_abs(base_path, 'src', 'path with spaces', 'include'),
+                      EditableCppInfo._work_on_item("src/path with spaces/include",
+                                                    base_path, None, None))
+        self.assertIn(_make_abs(base_path, 'ending-slash', 'include'),
+                      EditableCppInfo._work_on_item("ending-slash/include/", base_path, None, None))
+
+    @parameterized.expand([(False,), (True,)])
+    def test_windows(self, make_abs):
+        base_path = os.path.dirname(__file__) if make_abs else None
+
+        self.assertIn(os.path.join('C:' + os.sep, 'Windows-single-slash', 'include'),
+                      EditableCppInfo._work_on_item("C:\Windows-single-slash\include",
+                                                    base_path, None, None))
+        self.assertIn(os.path.join('D:' + os.sep, 'Windows-double-slash', 'include'),
+                      EditableCppInfo._work_on_item("D:\\Windows-double-slash\\include",
+                                                    base_path, None, None))
+
+    @parameterized.expand([(False,), (True,)])
+    def test_unix(self, make_abs):
+        base_path = os.path.dirname(__file__) if make_abs else None
+
+        self.assertIn(os.path.join(os.sep, 'abs', 'path', 'include'),
+                      EditableCppInfo._work_on_item("/abs/path/include", base_path, None, None))
+
+    @parameterized.expand([(False,), (True,)])
+    def test_placeholders(self, make_abs):
+        base_path = os.path.dirname(__file__) if make_abs else None
+
+        settings = Settings.loads(default_settings_yml)
+        settings.compiler = 'Visual Studio'
+        settings.compiler.version = '14'
+        settings.build_type = 'Debug'
+
+        self.assertIn(_make_abs(base_path, 'src', 'Visual Studio14', 'Debug', 'include'),
+                      EditableCppInfo._work_on_item("src/{settings.compiler}{settings.compiler.version}/{settings.build_type}/include",
+                                                    base_path=base_path, settings=settings,
+                                                    options=None))
+        self.assertIn(os.path.join('C:' + os.sep, 'Visual Studio', 'include'),
+                      EditableCppInfo._work_on_item("C:\\{settings.compiler}\\include\\",
+                                                    base_path=base_path, settings=settings,
+                                                    options=None))
+        self.assertIn(os.path.join(os.sep, 'usr', 'path with spaces', 'Visual Studio', 'dir'),
+                      EditableCppInfo._work_on_item("/usr/path with spaces/{settings.compiler}/dir",
+                                                    base_path=base_path, settings=settings,
+                                                    options=None))
diff --git a/conans/test/unittests/model/ref_test.py b/conans/test/unittests/model/ref_test.py
index 0497334ec..e07910846 100644
--- a/conans/test/unittests/model/ref_test.py
+++ b/conans/test/unittests/model/ref_test.py
@@ -7,7 +7,7 @@ from conans.model.ref import ConanFileReference, ConanName, InvalidNameException
 
 class RefTest(unittest.TestCase):
     def basic_test(self):
-        ref = ConanFileReference.loads("opencv/2.4.10 @ lasote/testing")
+        ref = ConanFileReference.loads("opencv/2.4.10@lasote/testing")
         self.assertEqual(ref.name, "opencv")
         self.assertEqual(ref.version, "2.4.10")
         self.assertEqual(ref.user, "lasote")
@@ -37,8 +37,10 @@ class RefTest(unittest.TestCase):
     def errors_test(self):
         self.assertRaises(ConanException, ConanFileReference.loads, "")
         self.assertRaises(ConanException, ConanFileReference.loads, "opencv/2.4.10")
-        self.assertRaises(ConanException, ConanFileReference.loads, "opencv/2.4.10 @ lasote")
+        self.assertRaises(ConanException, ConanFileReference.loads, "opencv/2.4.10@lasote")
         self.assertRaises(ConanException, ConanFileReference.loads, "opencv??/2.4.10@laso/testing")
+        self.assertRaises(ConanException, ConanFileReference.loads, "opencv/2.4.10 @ laso/testing")
+        self.assertRaises(ConanException, ConanFileReference.loads, "o/2.4.10@laso/testing")
         self.assertRaises(ConanException, ConanFileReference.loads, ".opencv/2.4.10@lasote/testing")
         self.assertRaises(ConanException, ConanFileReference.loads, "o/2.4.10 @ lasote/testing")
         self.assertRaises(ConanException, ConanFileReference.loads, "lib/1.0@user&surname/channel")
diff --git a/conans/test/unittests/model/version_ranges_test.py b/conans/test/unittests/model/version_ranges_test.py
index 6790896b7..661f0ec65 100644
--- a/conans/test/unittests/model/version_ranges_test.py
+++ b/conans/test/unittests/model/version_ranges_test.py
@@ -10,7 +10,7 @@ from conans.client.loader import ConanFileLoader
 from conans.errors import ConanException
 from conans.model.ref import ConanFileReference
 from conans.model.requires import Requirements
-from conans.paths import SimplePaths
+from conans.paths.simple_paths import SimplePaths
 from conans.test.unittests.model.fake_retriever import Retriever
 from conans.test.utils.tools import TestBufferConanOutput, test_processed_profile
 
@@ -287,8 +287,10 @@ class HelloConan(ConanFile):
                            # ranges
                            ('"Say/[<=1.2]@myuser/testing"', "1.2.1", False, False),
                            ('"Say/[>=0.2,<=1.0]@myuser/testing"', "0.3", False, True),
+                           ('"Say/[>=0.2 <=1.0]@myuser/testing"', "0.3", False, True),
                            ('("Say/[<=1.2]@myuser/testing", "override")', "1.2.1", True, False),
                            ('("Say/[>=0.2,<=1.0]@myuser/testing", "override")', "0.3", True, True),
+                           ('("Say/[>=0.2 <=1.0]@myuser/testing", "override")', "0.3", True, True),
                            ])
     def transitive_test(self, version_range, solution, override, valid):
         hello_text = hello_content % ">0.1, <1"
diff --git a/conans/test/utils/server_launcher.py b/conans/test/utils/server_launcher.py
index dc88071b1..70ac16c1a 100644
--- a/conans/test/utils/server_launcher.py
+++ b/conans/test/utils/server_launcher.py
@@ -4,7 +4,7 @@ import shutil
 import time
 
 from conans import SERVER_CAPABILITIES
-from conans.paths import SimplePaths
+from conans.paths.simple_paths import SimplePaths
 from conans.server.conf import get_server_store
 from conans.server.crypto.jwt.jwt_credentials_manager import JWTCredentialsManager
 from conans.server.crypto.jwt.jwt_updown_manager import JWTUpDownAuthManager
diff --git a/conans/test/utils/tools.py b/conans/test/utils/tools.py
index b62cd4af6..6efb7cfb7 100644
--- a/conans/test/utils/tools.py
+++ b/conans/test/utils/tools.py
@@ -466,16 +466,17 @@ class TestClient(object):
 
         if cpu_count:
             self.client_cache.conan_config
-            replace_in_file(os.path.join(self.client_cache.conan_conf_path),
+            replace_in_file(self.client_cache.conan_conf_path,
                             "# cpu_count = 1", "cpu_count = %s" % cpu_count,
-                            output=TestBufferConanOutput())
+                            output=TestBufferConanOutput(), strict=not bool(base_folder))
             # Invalidate the cached config
             self.client_cache.invalidate()
         if self.revisions:
             # Generate base file
             self.client_cache.conan_config
-            replace_in_file(os.path.join(self.client_cache.conan_conf_path),
-                            "revisions_enabled = False", "revisions_enabled = True", strict=False)
+            replace_in_file(self.client_cache.conan_conf_path,
+                            "revisions_enabled = False", "revisions_enabled = True",
+                            output=TestBufferConanOutput(), strict=not bool(base_folder))
             # Invalidate the cached config
             self.client_cache.invalidate()