Repository: conan-io/conan

Issue:
Allow None as subsettings in settings.yml
They shouldn't need definition in command line or profiles, should be like a None value in a list of possible values in settings.yml

```
Linux:
       distro:
           None:
           Ubuntu:
           Yocto:
               version: ["sumo", "rocko"]
```



Discussion:

Resolving patch:
diff --git a/conans/model/settings.py b/conans/model/settings.py
--- a/conans/model/settings.py
+++ b/conans/model/settings.py
@@ -181,7 +181,8 @@ def validate(self):
             raise undefined_value(self._name)
 
         if isinstance(self._definition, dict):
-            self._definition[self._value].validate()
+            key = "None" if self._value is None else self._value
+            self._definition[key].validate()
 
     def remove_undefined(self):
         if isinstance(self._definition, dict):
@@ -190,6 +191,8 @@ def remove_undefined(self):
 
 class Settings(object):
     def __init__(self, definition=None, name="settings", parent_value=None):
+        if parent_value == "None" and definition:
+            raise ConanException("settings.yml: None setting can't have subsettings")
         definition = definition or {}
         self._name = name  # settings, settings.compiler
         self._parent_value = parent_value  # gcc, x86


Test patch:
diff --git a/conans/test/functional/settings_constraint_test.py b/conans/test/functional/settings_constraint_test.py
deleted file mode 100644
--- a/conans/test/functional/settings_constraint_test.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import unittest
-from conans.test.utils.tools import TestClient
-from conans.util.files import save
-import os
-
-
-class SettingConstraintTest(unittest.TestCase):
-
-    def settings_constraint_test(self):
-        conanfile = """from conans import ConanFile
-class Test(ConanFile):
-    name = "Hello"
-    version = "0.1"
-    settings = {"compiler": {"gcc": {"version": ["7.1"]}}}
-    def build(self):
-        self.output.info("Compiler version!: %s" % self.settings.compiler.version)
-    """
-        test = """from conans import ConanFile
-class Test(ConanFile):
-    requires = "Hello/0.1@user/channel"
-    def test(self):
-        pass
-    """
-        client = TestClient()
-        client.save({"conanfile.py": conanfile,
-                     "test_package/conanfile.py": test})
-        default_profile = os.path.join(client.base_folder, ".conan/profiles/default")
-        save(default_profile, "[settings]\ncompiler=gcc\ncompiler.version=6.3")
-        error = client.run("create . user/channel", ignore_error=True)
-        self.assertTrue(error)
-        self.assertIn("Invalid setting '6.3' is not a valid 'settings.compiler.version'",
-                      client.user_io.out)
-        client.run("create . user/channel -s compiler=gcc -s compiler.version=7.1")
-        self.assertIn("Hello/0.1@user/channel: Compiler version!: 7.1", client.user_io.out)
-        self.assertIn("Hello/0.1@user/channel: Generating the package", client.user_io.out)
diff --git a/conans/test/integration/update_settings_yml_test.py b/conans/test/integration/update_settings_yml_test.py
deleted file mode 100644
--- a/conans/test/integration/update_settings_yml_test.py
+++ /dev/null
@@ -1,76 +0,0 @@
-import unittest
-from conans.test.utils.tools import TestClient
-from conans.util.files import load, save
-from conans.client.conf import default_settings_yml
-from conans.model.settings import Settings
-
-
-class UpdateSettingsYmlTest(unittest.TestCase):
-    """ This test is to validate that after adding a new settings, that allows a None
-    value, this None value does not modify exisisting packages SHAs
-    """
-
-    def test_update_settings(self):
-        file_content = '''
-from conans import ConanFile
-
-class ConanFileToolsTest(ConanFile):
-    name = "test"
-    version = "1.9"
-    settings = "os", "compiler", "arch"
-
-    def source(self):
-        self.output.warn("Sourcing...")
-
-    def build(self):
-        self.output.warn("Building...")
-    '''
-        prev_settings = """
-os: [Windows, Linux, Macos, Android, FreeBSD, SunOS]
-arch: [x86, x86_64, armv6, armv7, armv7hf, armv8, sparc, sparcv9]
-os_build: [Windows, Linux, Macos, Android, FreeBSD, SunOS]
-arch_build: [x86, x86_64, armv6, armv7, armv7hf, armv8, sparc, sparcv9]
-
-compiler:
-    sun-cc:
-        version: ["5.10", "5.11", "5.12", "5.13", "5.14"]
-        libcxx: [libCstd, libstdcxx libstlport, libstdc++]
-    gcc:
-        version: ["4.4", "4.5", "4.6", "4.7", "4.8", "4.9", "5.1", "5.2", "5.3", "5.4", "6.1", "6.2", "6.3"]
-        libcxx: [libstdc++, libstdc++11]
-    Visual Studio:
-        runtime: [None, MD, MT, MTd, MDd]
-        version: ["8", "9", "10", "11", "12", "14"]
-    clang:
-        version: ["3.3", "3.4", "3.5", "3.6", "3.7", "3.8"]
-        libcxx: [libstdc++, libstdc++11, libc++]
-    apple-clang:
-        version: ["5.0", "5.1", "6.0", "6.1", "7.0", "7.1", "7.2", "7.3", "8.0", "8.1"]
-        libcxx: [libstdc++, libc++]
-
-"""
-        files = {"conanfile.py": file_content}
-        client = TestClient()
-        client.save(files)
-        client.run("export . lasote/testing")
-        save(client.paths.settings_path, prev_settings)
-        client.client_cache.default_profile  # Generate the default
-        conf = load(client.client_cache.default_profile_path)
-        conf = conf.replace("build_type=Release", "")
-        self.assertNotIn("build_type", conf)
-        save(client.client_cache.default_profile_path, conf)
-
-        client.run("install test/1.9@lasote/testing --build -s arch=x86_64 -s compiler=gcc "
-                   "-s compiler.version=4.9 -s os=Windows -s compiler.libcxx=libstdc++")
-        self.assertIn("390146894f59dda18c902ee25e649ef590140732", client.user_io.out)
-
-        # Now the new one
-        files = {"conanfile.py": file_content.replace('"arch"', '"arch", "build_type"')}
-        client = TestClient()
-        client.save(files)
-        client.run("export . lasote/testing")
-
-        client.run("install test/1.9@lasote/testing --build -s arch=x86_64 -s compiler=gcc "
-                   "-s compiler.version=4.9 -s os=Windows -s build_type=None -s compiler.libcxx=libstdc++")
-        self.assertIn("build_type", load(client.paths.settings_path))
-        self.assertIn("390146894f59dda18c902ee25e649ef590140732", client.user_io.out)
diff --git a/conans/test/model/other_settings_test.py b/conans/test/model/other_settings_test.py
--- a/conans/test/model/other_settings_test.py
+++ b/conans/test/model/other_settings_test.py
@@ -1,10 +1,12 @@
+import os
 import unittest
+
 from conans.util.files import save, load
 from conans.test.utils.tools import TestClient
 from conans.paths import CONANFILE, CONANINFO
 from conans.model.info import ConanInfo
-import os
 from conans.model.settings import undefined_value, bad_value_msg
+from conans.model.ref import PackageReference
 
 
 class SettingsTest(unittest.TestCase):
@@ -12,6 +14,151 @@ class SettingsTest(unittest.TestCase):
     def setUp(self):
         self.client = TestClient()
 
+    def wrong_settings_test(self):
+        settings = """os:
+    None:
+        subsystem: [None, msys]
+"""
+        client = TestClient()
+        save(client.paths.settings_path, settings)
+        save(client.paths.default_profile_path, "")
+        conanfile = """from conans import ConanFile
+class Pkg(ConanFile):
+    settings = "os", "compiler"
+"""
+        client.save({"conanfile.py": conanfile})
+        error = client.run("create . Pkg/0.1@lasote/testing", ignore_error=True)
+        self.assertTrue(error)
+        self.assertIn("ERROR: settings.yml: None setting can't have subsettings", client.out)
+
+    def custom_settings_test(self):
+        settings = """os:
+    None:
+    Windows:
+        subsystem: [None, cygwin]
+    Linux:
+compiler: [gcc, visual]
+"""
+        client = TestClient()
+        save(client.paths.settings_path, settings)
+        save(client.paths.default_profile_path, "")
+        conanfile = """from conans import ConanFile
+class Pkg(ConanFile):
+    settings = "os", "compiler"
+"""
+        client.save({"conanfile.py": conanfile})
+        client.run("create . Pkg/0.1@lasote/testing -s compiler=gcc")
+        self.assertIn("544c1d8c53e9d269737e68e00ec66716171d2704", client.out)
+        client.run("search Pkg/0.1@lasote/testing")
+        self.assertNotIn("os: None", client.out)
+        package_reference = PackageReference.loads("Pkg/0.1@lasote/testing:544c1d8c53e9d269737e68e00ec66716171d2704")
+        info_path = os.path.join(client.paths.package(package_reference), CONANINFO)
+        info = load(info_path)
+        self.assertNotIn("os", info)
+        # Explicitly specifying None, put it in the conaninfo.txt, but does not affect the hash
+        client.run("create . Pkg/0.1@lasote/testing -s compiler=gcc -s os=None")
+        self.assertIn("544c1d8c53e9d269737e68e00ec66716171d2704", client.out)
+        client.run("search Pkg/0.1@lasote/testing")
+        self.assertIn("os: None", client.out)
+        info = load(info_path)
+        self.assertIn("os", info)
+
+    def test_update_settings(self):
+        """ This test is to validate that after adding a new settings, that allows a None
+    value, this None value does not modify exisisting packages SHAs
+    """
+        file_content = '''
+from conans import ConanFile
+
+class ConanFileToolsTest(ConanFile):
+    name = "test"
+    version = "1.9"
+    settings = "os", "compiler", "arch"
+
+    def source(self):
+        self.output.warn("Sourcing...")
+
+    def build(self):
+        self.output.warn("Building...")
+    '''
+        prev_settings = """
+os: [Windows, Linux, Macos, Android, FreeBSD, SunOS]
+arch: [x86, x86_64, armv6, armv7, armv7hf, armv8, sparc, sparcv9]
+os_build: [Windows, Linux, Macos, Android, FreeBSD, SunOS]
+arch_build: [x86, x86_64, armv6, armv7, armv7hf, armv8, sparc, sparcv9]
+
+compiler:
+    sun-cc:
+        version: ["5.10", "5.11", "5.12", "5.13", "5.14"]
+        libcxx: [libCstd, libstdcxx libstlport, libstdc++]
+    gcc:
+        version: ["4.4", "4.5", "4.6", "4.7", "4.8", "4.9", "5.1", "5.2", "5.3", "5.4", "6.1", "6.2", "6.3"]
+        libcxx: [libstdc++, libstdc++11]
+    Visual Studio:
+        runtime: [None, MD, MT, MTd, MDd]
+        version: ["8", "9", "10", "11", "12", "14"]
+    clang:
+        version: ["3.3", "3.4", "3.5", "3.6", "3.7", "3.8"]
+        libcxx: [libstdc++, libstdc++11, libc++]
+    apple-clang:
+        version: ["5.0", "5.1", "6.0", "6.1", "7.0", "7.1", "7.2", "7.3", "8.0", "8.1"]
+        libcxx: [libstdc++, libc++]
+
+"""
+        files = {"conanfile.py": file_content}
+        client = TestClient()
+        client.save(files)
+        client.run("export . lasote/testing")
+        save(client.paths.settings_path, prev_settings)
+        client.client_cache.default_profile  # Generate the default
+        conf = load(client.client_cache.default_profile_path)
+        conf = conf.replace("build_type=Release", "")
+        self.assertNotIn("build_type", conf)
+        save(client.client_cache.default_profile_path, conf)
+
+        client.run("install test/1.9@lasote/testing --build -s arch=x86_64 -s compiler=gcc "
+                   "-s compiler.version=4.9 -s os=Windows -s compiler.libcxx=libstdc++")
+        self.assertIn("390146894f59dda18c902ee25e649ef590140732", client.user_io.out)
+
+        # Now the new one
+        files = {"conanfile.py": file_content.replace('"arch"', '"arch", "build_type"')}
+        client = TestClient()
+        client.save(files)
+        client.run("export . lasote/testing")
+
+        client.run("install test/1.9@lasote/testing --build -s arch=x86_64 -s compiler=gcc "
+                   "-s compiler.version=4.9 -s os=Windows -s build_type=None -s compiler.libcxx=libstdc++")
+        self.assertIn("build_type", load(client.paths.settings_path))
+        self.assertIn("390146894f59dda18c902ee25e649ef590140732", client.user_io.out)
+
+    def settings_constraint_test(self):
+        conanfile = """from conans import ConanFile
+class Test(ConanFile):
+    name = "Hello"
+    version = "0.1"
+    settings = {"compiler": {"gcc": {"version": ["7.1"]}}}
+    def build(self):
+        self.output.info("Compiler version!: %s" % self.settings.compiler.version)
+    """
+        test = """from conans import ConanFile
+class Test(ConanFile):
+    requires = "Hello/0.1@user/channel"
+    def test(self):
+        pass
+    """
+        client = TestClient()
+        client.save({"conanfile.py": conanfile,
+                     "test_package/conanfile.py": test})
+        default_profile = os.path.join(client.base_folder, ".conan/profiles/default")
+        save(default_profile, "[settings]\ncompiler=gcc\ncompiler.version=6.3")
+        error = client.run("create . user/channel", ignore_error=True)
+        self.assertTrue(error)
+        self.assertIn("Invalid setting '6.3' is not a valid 'settings.compiler.version'",
+                      client.user_io.out)
+        client.run("create . user/channel -s compiler=gcc -s compiler.version=7.1")
+        self.assertIn("Hello/0.1@user/channel: Compiler version!: 7.1", client.user_io.out)
+        self.assertIn("Hello/0.1@user/channel: Generating the package", client.user_io.out)
+
     def settings_as_a_str_test(self):
         content = """
 from conans import ConanFile
diff --git a/conans/test/model/settings_test.py b/conans/test/model/settings_test.py
--- a/conans/test/model/settings_test.py
+++ b/conans/test/model/settings_test.py
@@ -3,6 +3,59 @@
 from conans.model.settings import Settings, bad_value_msg, undefined_value, undefined_field
 
 
+class SettingsLoadsTest(unittest.TestCase):
+
+    def test_none_value(self):
+        yml = "os: [None, Windows]"
+        settings = Settings.loads(yml)
+        # Same sha as if settings were empty
+        self.assertEqual(settings.values.sha, Settings.loads("").values.sha)
+        settings.validate()
+        self.assertTrue(settings.os == None)
+        self.assertEqual("", settings.values.dumps())
+        settings.os = "None"
+        self.assertEqual(settings.values.sha, Settings.loads("").values.sha)
+        settings.validate()
+        self.assertTrue(settings.os == "None")
+        self.assertEqual("os=None", settings.values.dumps())
+        settings.os = "Windows"
+        self.assertTrue(settings.os == "Windows")
+        self.assertEqual("os=Windows", settings.values.dumps())
+
+    def test_none_subsetting(self):
+        yml = """os:
+    None:
+    Windows:
+        subsystem: [None, cygwin]
+"""
+        settings = Settings.loads(yml)
+        # Same sha as if settings were empty
+        self.assertEqual(settings.values.sha, Settings.loads("").values.sha)
+        settings.validate()
+        self.assertTrue(settings.os == None)
+        self.assertEqual("", settings.values.dumps())
+        settings.os = "None"
+        self.assertEqual(settings.values.sha, Settings.loads("").values.sha)
+        settings.validate()
+        self.assertTrue(settings.os == "None")
+        self.assertEqual("os=None", settings.values.dumps())
+        settings.os = "Windows"
+        self.assertTrue(settings.os.subsystem == None)
+        self.assertEqual("os=Windows", settings.values.dumps())
+        settings.os.subsystem = "cygwin"
+        self.assertEqual("os=Windows\nos.subsystem=cygwin", settings.values.dumps())
+
+    def test_none__sub_subsetting(self):
+        yml = """os:
+    None:
+        subsystem: [None, cygwin]
+    Windows:
+"""
+        with self.assertRaisesRegexp(ConanException,
+                                     "settings.yml: None setting can't have subsettings"):
+            Settings.loads(yml)
+
+
 class SettingsTest(unittest.TestCase):
 
     def setUp(self):