Repository: conan-io/conan

Issue:
Enable "conan config install" to install from a (git/svn cloned) folder
Conan 1.7.

Right now, conan can config-install from a repo, or from a zip file. It would be easy to allow doing the process from any folder. That could help with config in other sources or in unreachable repos by conan (user/passwd auth) (https://github.com/conan-io/conan/issues/3668).



Discussion:

Patch:
diff --git a/README.rst b/README.rst
index 729317e77..9cf9b56cb 100644
--- a/README.rst
+++ b/README.rst
@@ -195,7 +195,7 @@ To run specific tests, you can specify the test name too, something like:
 
 .. code-block:: bash
 
-    $ nosetests conans.test.integration.flat_requirements_test --nocapture
+    $ nosetests conans.test.command.config_install_test:ConfigInstallTest.install_file_test --nocapture
 
 The ``--nocapture`` argument can be useful to see some output that otherwise is captured by nosetests.
 
diff --git a/conans/client/command.py b/conans/client/command.py
index 3154542e9..580080ced 100644
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -386,7 +386,7 @@ class Command(object):
         rm_subparser.add_argument("item", help="Item to remove")
         get_subparser.add_argument("item", nargs="?", help="Item to print")
         set_subparser.add_argument("item", help="'item=value' to set")
-        install_subparser.add_argument("item", nargs="?", help="Configuration file to use")
+        install_subparser.add_argument("item", nargs="?", help="Configuration file or directory to use")
 
         install_subparser.add_argument("--verify-ssl", nargs="?", default="True",
                                        help='Verify SSL connection when downloading file')
diff --git a/conans/client/conf/__init__.py b/conans/client/conf/__init__.py
index 3efb42b2e..741fb2fb8 100644
--- a/conans/client/conf/__init__.py
+++ b/conans/client/conf/__init__.py
@@ -290,10 +290,8 @@ class ConanClientConfigParser(ConfigParser, object):
         ret = os.environ.get("CONAN_DEFAULT_PROFILE_PATH", None)
         if ret:
             if not os.path.isabs(ret):
-                from conans.client.client_cache import PROFILES_FOLDER
-                profiles_folder = os.path.join(os.path.dirname(self.filename), PROFILES_FOLDER)
-                ret = os.path.abspath(os.path.join(profiles_folder, ret))
-
+                raise ConanException("Environment variable 'CONAN_DEFAULT_PROFILE_PATH' must point "
+                                     "to an absolute path.")
             if not os.path.exists(ret):
                 raise ConanException("Environment variable 'CONAN_DEFAULT_PROFILE_PATH' must point to "
                                      "an existing profile file.")
diff --git a/conans/client/conf/config_installer.py b/conans/client/conf/config_installer.py
index c743b11df..b53cd3dcb 100644
--- a/conans/client/conf/config_installer.py
+++ b/conans/client/conf/config_installer.py
@@ -123,8 +123,9 @@ def configuration_install(item, client_cache, output, verify_ssl, config_type=No
 
         if item.endswith(".git") or config_type == "git":
             _process_git_repo(item, client_cache, output, tmp_folder, verify_ssl, args)
-        elif os.path.exists(item):
-            # is a local file
+        elif os.path.isdir(item):
+            _process_folder(item, client_cache, output)
+        elif os.path.isfile(item):
             _process_zip_file(item, client_cache, output, tmp_folder)
         elif item.startswith("http"):
             _process_download(item, client_cache, output, tmp_folder, verify_ssl)
diff --git a/conans/client/conf/detect.py b/conans/client/conf/detect.py
index c0909af9b..3f5a2dea0 100644
--- a/conans/client/conf/detect.py
+++ b/conans/client/conf/detect.py
@@ -5,7 +5,7 @@ from subprocess import Popen, PIPE, STDOUT
 
 from conans.client.output import Color
 from conans.model.version import Version
-from conans.client.tools.win import latest_visual_studio_version_installed
+from conans.client.tools import latest_visual_studio_version_installed
 
 
 def _execute(command):
diff --git a/conans/client/tools/win.py b/conans/client/tools/win.py
index 2b8b2ebd2..beaa17a3b 100644
--- a/conans/client/tools/win.py
+++ b/conans/client/tools/win.py
@@ -90,10 +90,6 @@ def _visual_compiler(output, version):
     return None
 
 
-def latest_vs_version_installed():
-    return latest_visual_studio_version_installed(_global_output)
-
-
 def latest_visual_studio_version_installed(output):
     for version in reversed(["8", "9", "10", "11", "12", "14", "15"]):
         vs = _visual_compiler(output, version)
@@ -322,7 +318,7 @@ def vcvars_command(settings, arch=None, compiler_version=None, force=False, vcva
         # vcvars might be still needed for other compilers, e.g. clang-cl or Intel C++,
         # as they might be using Microsoft STL and other tools (e.g. resource compiler, manifest tool, etc)
         # in this case, use the latest Visual Studio available on the machine
-        last_version = latest_vs_version_installed()
+        last_version = latest_visual_studio_version_installed(output=_global_output)
 
         compiler_version = compiler_version or last_version
     os_setting = settings.get_safe("os")
diff --git a/conans/model/conan_file.py b/conans/model/conan_file.py
index 6fbb88e4c..798790c00 100644
--- a/conans/model/conan_file.py
+++ b/conans/model/conan_file.py
@@ -22,13 +22,13 @@ def create_options(conanfile):
 
         default_options = getattr(conanfile, "default_options", None)
         if default_options:
-            if isinstance(default_options, (list, tuple, dict)):
+            if isinstance(default_options, (list, tuple)):
                 default_values = OptionsValues(default_options)
             elif isinstance(default_options, str):
                 default_values = OptionsValues.loads(default_options)
             else:
-                raise ConanException("Please define your default_options as list, "
-                                     "multiline string or dictionary")
+                raise ConanException("Please define your default_options as list or "
+                                     "multiline string")
             options.values = default_values
         return options
     except Exception as e:
diff --git a/conans/model/options.py b/conans/model/options.py
index 612e187c2..883721c62 100644
--- a/conans/model/options.py
+++ b/conans/model/options.py
@@ -1,11 +1,8 @@
-
+from conans.util.sha import sha1
+from conans.errors import ConanException
 import yaml
 import six
 import fnmatch
-from collections import Counter
-
-from conans.util.sha import sha1
-from conans.errors import ConanException
 
 
 _falsey_options = ["false", "none", "0", "off", ""]
@@ -165,20 +162,19 @@ class OptionsValues(object):
 
         # convert tuple "Pkg:option=value", "..." to list of tuples(name, value)
         if isinstance(values, tuple):
-            values = [item.split("=", 1) for item in values]
-
-        # convert dict {"Pkg:option": "value", "..": "..", ...} to list of tuples (name, value)
-        if isinstance(values, dict):
-            values = [(k, v) for k, v in values.items()]
+            new_values = []
+            for v in values:
+                option, value = v.split("=", 1)
+                new_values.append((option.strip(), value.strip()))
+            values = new_values
 
         # handle list of tuples (name, value)
         for (k, v) in values:
-            k = k.strip()
-            v = v.strip() if isinstance(v, six.string_types) else v
             tokens = k.split(":")
             if len(tokens) == 2:
                 package, option = tokens
-                package_values = self._reqs_options.setdefault(package.strip(), PackageOptionValues())
+                package_values = self._reqs_options.setdefault(package.strip(),
+                                                               PackageOptionValues())
                 package_values.add_option(option, v)
             else:
                 self._package_values.add_option(k, v)
@@ -268,8 +264,14 @@ class OptionsValues(object):
         other_option=3
         OtherPack:opt3=12.1
         """
-        options = tuple(line.strip() for line in text.splitlines() if line.strip())
-        return OptionsValues(options)
+        result = []
+        for line in text.splitlines():
+            line = line.strip()
+            if not line:
+                continue
+            name, value = line.split("=", 1)
+            result.append((name.strip(), value.strip()))
+        return OptionsValues(result)
 
     @property
     def sha(self):


Test patch:
diff --git a/conans/test/command/config_install_test.py b/conans/test/command/config_install_test.py
index 58cce2b3e..e1a37f369 100644
--- a/conans/test/command/config_install_test.py
+++ b/conans/test/command/config_install_test.py
@@ -168,6 +168,14 @@ class Pkg(ConanFile):
         self._check(zippath)
         self.assertTrue(os.path.exists(zippath))
 
+    def install_dir_test(self):
+        """ should install from a dir in current dir
+        """
+        folder = self._create_profile_folder()
+        self.assertTrue(os.path.isdir(folder))
+        self.client.run('config install "%s"' % folder)
+        self._check(folder)
+
     def test_without_profile_folder(self):
         shutil.rmtree(self.client.client_cache.profiles_path)
         zippath = self._create_zip()
diff --git a/conans/test/functional/default_profile_test.py b/conans/test/functional/default_profile_test.py
index 3bb9fda84..d868aaa92 100644
--- a/conans/test/functional/default_profile_test.py
+++ b/conans/test/functional/default_profile_test.py
@@ -7,7 +7,6 @@ from conans.test.utils.test_files import temp_folder
 from conans.test.utils.tools import TestClient
 from conans.util.files import save
 from conans import tools
-from conans.client.client_cache import PROFILES_FOLDER
 
 
 class DefaultProfileTest(unittest.TestCase):
@@ -162,15 +161,10 @@ class MyConanfile(ConanFile):
             self.assertIn(">>> " + env_variable, client.out)
 
         # Use relative path defined in environment variable
-        env_variable = "env_variable=relative_profile"
-        rel_path = os.path.join('..', 'env_rel_profile')
-        self.assertFalse(os.path.isabs(rel_path))
-        default_profile_path = os.path.join(client.client_cache.conan_folder,
-                                            PROFILES_FOLDER, rel_path)
-        save(default_profile_path, "[env]\n" + env_variable)
-        with tools.environment_append({'CONAN_DEFAULT_PROFILE_PATH': rel_path}):
-            client.run("create . name/version@user/channel")
-            self.assertIn(">>> " + env_variable, client.out)
+        with tools.environment_append({'CONAN_DEFAULT_PROFILE_PATH': '../whatever'}):
+            client.run("create . name/version@user/channel", ignore_error=True)
+            self.assertIn("Environment variable 'CONAN_DEFAULT_PROFILE_PATH' must point to "
+                          "an absolute path", client.out)
 
         # Use non existing path
         profile_path = os.path.join(tmp, "this", "is", "a", "path")
diff --git a/conans/test/integration/options_test.py b/conans/test/integration/options_test.py
index bec784910..5af849018 100644
--- a/conans/test/integration/options_test.py
+++ b/conans/test/integration/options_test.py
@@ -142,73 +142,6 @@ zlib/0.1@lasote/testing
         conaninfo = load(os.path.join(client.current_folder, CONANINFO))
         self.assertNotIn("zlib:shared=True", conaninfo)
 
-    def test_default_options(self):
-        client = TestClient()
-        conanfile = """
-from conans import ConanFile
-
-class MyConanFile(ConanFile):
-    name = "MyConanFile"
-    version = "1.0"
-    options = {"config": %s}
-    default_options = "config%s"
-
-    def configure(self):
-        if self.options.config:
-            self.output.info("Boolean evaluation")
-        if self.options.config is None:
-            self.output.info("None evaluation")
-        if self.options.config == "None":
-            self.output.info("String evaluation")
-"""
-        # Using "ANY" as possible options
-        client.save({"conanfile.py": conanfile % ("\"ANY\"", "")})
-        error = client.run("create . danimtb/testing", ignore_error=True)
-        self.assertTrue(error)
-        self.assertIn("Error while initializing options.", client.out)
-        client.save({"conanfile.py": conanfile % ("\"ANY\"", "=None")})
-        client.run("create . danimtb/testing")
-        self.assertNotIn("Boolean evaluation", client.out)
-        self.assertNotIn("None evaluation", client.out)
-        self.assertIn("String evaluation", client.out)
-
-        # Using None as possible options
-        client.save({"conanfile.py": conanfile % ("[None]", "")})
-        error = client.run("create . danimtb/testing", ignore_error=True)
-        self.assertTrue(error)
-        self.assertIn("Error while initializing options.", client.out)
-        client.save({"conanfile.py": conanfile % ("[None]", "=None")})
-        client.run("create . danimtb/testing")
-        self.assertNotIn("Boolean evaluation", client.out)
-        self.assertNotIn("None evaluation", client.out)
-        self.assertIn("String evaluation", client.out)
-
-        # Using "None" as possible options
-        client.save({"conanfile.py": conanfile % ("[\"None\"]", "")})
-        error = client.run("create . danimtb/testing", ignore_error=True)
-        self.assertTrue(error)
-        self.assertIn("Error while initializing options.", client.out)
-        client.save({"conanfile.py": conanfile % ("[\"None\"]", "=None")})
-        client.run("create . danimtb/testing")
-        self.assertNotIn("Boolean evaluation", client.out)
-        self.assertNotIn("None evaluation", client.out)
-        self.assertIn("String evaluation", client.out)
-        client.save({"conanfile.py": conanfile % ("[\"None\"]", "=\\\"None\\\"")})
-        error = client.run("create . danimtb/testing", ignore_error=True)
-        self.assertTrue(error)
-        self.assertIn("'\"None\"' is not a valid 'options.config' value", client.out)
-
-        # Using "ANY" as possible options and "otherstringvalue" as default
-        client.save({"conanfile.py": conanfile % ("[\"otherstringvalue\"]", "")})
-        error = client.run("create . danimtb/testing", ignore_error=True)
-        self.assertTrue(error)
-        self.assertIn("Error while initializing options.", client.out)
-        client.save({"conanfile.py": conanfile % ("\"ANY\"", "=otherstringvalue")})
-        client.run("create . danimtb/testing")
-        self.assertIn("Boolean evaluation", client.out)
-        self.assertNotIn("None evaluation", client.out)
-        self.assertNotIn("String evaluation", client.out)
-
     def general_scope_options_test(self):
         # https://github.com/conan-io/conan/issues/2538
         client = TestClient()
diff --git a/conans/test/model/options_test.py b/conans/test/model/options_test.py
index 6b4a24e2d..c672bf204 100644
--- a/conans/test/model/options_test.py
+++ b/conans/test/model/options_test.py
@@ -1,4 +1,3 @@
-import six
 import unittest
 from conans.model.options import OptionsValues, PackageOptions, Options, PackageOptionValues,\
     option_undefined_msg
@@ -240,39 +239,6 @@ class OptionsValuesTest(unittest.TestCase):
         option_values = OptionsValues(self.sut.as_list())
         self.assertEqual(option_values.dumps(), self.sut.dumps())
 
-    def test_from_dict(self):
-        options_as_dict = dict([item.split('=') for item in self.sut.dumps().splitlines()])
-        option_values = OptionsValues(options_as_dict)
-        self.assertEqual(option_values.dumps(), self.sut.dumps())
-
-    def test_consistency(self):
-        def _check_equal(hs1, hs2, hs3, hs4):
-            opt_values1 = OptionsValues(hs1)
-            opt_values2 = OptionsValues(hs2)
-            opt_values3 = OptionsValues(hs3)
-            opt_values4 = OptionsValues(hs4)
-
-            self.assertEqual(opt_values1.dumps(), opt_values2.dumps())
-            self.assertEqual(opt_values1.dumps(), opt_values3.dumps())
-            self.assertEqual(opt_values1.dumps(), opt_values4.dumps())
-
-        # Check that all possible input options give the same result
-        _check_equal([('opt', 3)],       [('opt', '3'), ],       ('opt=3', ),       {'opt': 3})
-        _check_equal([('opt', True)],    [('opt', 'True'), ],    ('opt=True', ),    {'opt': True})
-        _check_equal([('opt', False)],   [('opt', 'False'), ],   ('opt=False', ),   {'opt': False})
-        _check_equal([('opt', None)],    [('opt', 'None'), ],    ('opt=None', ),    {'opt': None})
-        _check_equal([('opt', 0)],       [('opt', '0'), ],       ('opt=0', ),       {'opt': 0})
-        _check_equal([('opt', '')],      [('opt', ''), ],        ('opt=', ),        {'opt': ''})
-
-        # Check for leading and trailing spaces
-        _check_equal([('  opt  ', 3)], [(' opt  ', '3'), ], ('  opt =3', ), {' opt ': 3})
-        _check_equal([('opt', '  value  ')], [('opt', '  value '), ], ('opt= value  ', ),
-                     {'opt': ' value '})
-
-        # This is expected behaviour:
-        self.assertNotEqual(OptionsValues([('opt', ''), ]).dumps(),
-                            OptionsValues(('opt=""', )).dumps())
-
     def test_dumps(self):
         self.assertEqual(self.sut.dumps(), "\n".join(["optimized=3",
                                                       "static=True",
@@ -299,34 +265,3 @@ class OptionsValuesTest(unittest.TestCase):
                                                       "Poco:new_option=0"]))
         self.assertEqual(self.sut.sha,
                          "2442d43f1d558621069a15ff5968535f818939b5")
-
-    def test_loads_exceptions(self):
-        emsg = "not enough values to unpack" if six.PY3 else "need more than 1 value to unpack"
-        with self.assertRaisesRegexp(ValueError, emsg):
-            OptionsValues.loads("a=2\nconfig\nb=3")
-
-        with self.assertRaisesRegexp(ValueError, emsg):
-            OptionsValues.loads("config\na=2\ncommit\nb=3")
-
-    def test_exceptions_empty_value(self):
-        emsg = "not enough values to unpack" if six.PY3 else "need more than 1 value to unpack"
-        with self.assertRaisesRegexp(ValueError, emsg):
-            OptionsValues("a=2\nconfig\nb=3")
-
-        with self.assertRaisesRegexp(ValueError, emsg):
-            OptionsValues(("a=2", "config"))
-
-        with self.assertRaisesRegexp(ValueError, emsg):
-            OptionsValues([('a', 2), ('config', ), ])
-
-    def test_exceptions_repeated_value(self):
-        try:
-            OptionsValues.loads("a=2\na=12\nb=3").dumps()
-            OptionsValues(("a=2", "b=23", "a=12"))
-            OptionsValues([('a', 2), ('b', True), ('a', '12')])
-        except Exception as e:
-            self.fail("Not expected exception: {}".format(e))
-
-    def test_package_with_spaces(self):
-        self.assertEqual(OptionsValues([('pck2:opt', 50), ]).dumps(),
-                         OptionsValues([('pck2 :opt', 50), ]).dumps())
diff --git a/conans/test/model/transitive_reqs_test.py b/conans/test/model/transitive_reqs_test.py
index fb867ce48..5ef9a24ea 100644
--- a/conans/test/model/transitive_reqs_test.py
+++ b/conans/test/model/transitive_reqs_test.py
@@ -560,17 +560,6 @@ class HelloConan(ConanFile):
 """
         _assert_conanfile(hello_content_tuple)
 
-        hello_content_dict = """
-from conans import ConanFile
-
-class HelloConan(ConanFile):
-    name = "Hello"
-    version = "1.2"
-    requires = "Say/0.1@user/testing"
-    default_options = {"Say:myoption" : 234, }  # To test dict definition
-"""
-        _assert_conanfile(hello_content_dict)
-
     def test_transitive_two_levels_options(self):
         say_content = """
 from conans import ConanFile
diff --git a/conans/test/util/vcvars_clangcl_test.py b/conans/test/util/vcvars_clangcl_test.py
index fc8448b3c..3cce7ddb7 100644
--- a/conans/test/util/vcvars_clangcl_test.py
+++ b/conans/test/util/vcvars_clangcl_test.py
@@ -43,7 +43,7 @@ class VCVarsClangClTest(unittest.TestCase):
         settings.arch = 'x86_64'
         settings.os = 'Windows'
 
-        with mock.patch('conans.client.tools.win.latest_vs_version_installed',
+        with mock.patch('conans.client.tools.win.latest_visual_studio_version_installed',
                         mock.MagicMock(return_value=None)):
             with self.assertRaises(ConanException):
                 tools.vcvars_command(settings)