Repository: conan-io/conan

Issue:
[bug] conan profile show fails on Windows
### Environment details

* Operating System+version: Windows 11 Pro 23H2
* Compiler+version: msvc 193
* Conan version: 2.1.0
* Python version: 3.12.2


### Steps to reproduce

1. `conan profile detect`
2. `conan profile show --profile:host default --profile:build default`

Note that I can't reproduce if I completely delete my `~/.conan2` directory.

### Logs

```
$ conan profile show --profile:host default
ERROR: Traceback (most recent call last):
  File "C:\Users\Quickemu\AppData\Local\Programs\Python\Python312\Lib\site-packages\conan\cli\cli.py", line 281, in main
    cli.run(args)
  File "C:\Users\Quickemu\AppData\Local\Programs\Python\Python312\Lib\site-packages\conan\cli\cli.py", line 191, in run
    command.run(self._conan_api, args[0][1:])
  File "C:\Users\Quickemu\AppData\Local\Programs\Python\Python312\Lib\site-packages\conan\cli\command.py", line 158, in run
    sub.run(conan_api, parser, *args)
  File "C:\Users\Quickemu\AppData\Local\Programs\Python\Python312\Lib\site-packages\conan\cli\command.py", line 172, in run
    info = self._method(conan_api, parent_parser, self._parser, *args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Quickemu\AppData\Local\Programs\Python\Python312\Lib\site-packages\conan\cli\commands\profile.py", line 40, in profile_show
    result = conan_api.profiles.get_profiles_from_args(args)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Quickemu\AppData\Local\Programs\Python\Python312\Lib\site-packages\conan\api\subapi\profiles.py", line 60, in get_profiles_from_args
    cache_settings = self._conan_api.config.settings_yml
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Quickemu\AppData\Local\Programs\Python\Python312\Lib\site-packages\conan\api\subapi\config.py", line 122, in settings_yml
    return Settings(settings)
           ^^^^^^^^^^^^^^^^^^
  File "C:\Users\Quickemu\AppData\Local\Programs\Python\Python312\Lib\site-packages\conans\model\settings.py", line 189, in __init__
    self._data = {k: SettingsItem(v, "%s.%s" % (name, k))
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Quickemu\AppData\Local\Programs\Python\Python312\Lib\site-packages\conans\model\settings.py", line 35, in __init__
    self._definition[k] = Settings(v, name, k)
                          ^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Quickemu\AppData\Local\Programs\Python\Python312\Lib\site-packages\conans\model\settings.py", line 189, in __init__
    self._data = {k: SettingsItem(v, "%s.%s" % (name, k))
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\Quickemu\AppData\Local\Programs\Python\Python312\Lib\site-packages\conans\model\settings.py", line 38, in __init__
    self._definition = [str(v) if v is not None else None for v in definition]
                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: 'NoneType' object is not iterable

ERROR: 'NoneType' object is not iterable
```


Discussion:
Hi! Thanks for the report. I've tried to reproduce locally and I'm not able to get it to crash - Maybe this is something to do with the detected profile? Could you please share the `default` profile that conan is detecting? Thanks!
I figured out that deleting my entire `~/.conan2` directory and running `conan profile detect` followed by `conan profile show ...` works.

```
Host profile:
[settings]
arch=x86_64
build_type=Release
compiler=msvc
compiler.cppstd=14
compiler.runtime=dynamic
compiler.runtime_type=Release
compiler.version=193
os=Windows

Build profile:
[settings]
arch=x86_64
build_type=Release
compiler=msvc
compiler.cppstd=14
compiler.runtime=dynamic
compiler.runtime_type=Release
compiler.version=193
os=Windows
```
It's an issue with `settings_user.yml`. If I delete my file for that it works.
Can you please share a ``settings_user.yml`` file that would make it crash? Thanks!
Yes, I believe I narrowed it down to me not including square brackets:

```
os:
    Windows:
        libc: null
```

The following doesn't crash:

```
os:
    Windows:
        libc: [null]
```

It seems to require that I add `libc` for Windows since I have it for Linux, which is why I added it like that:

```
$ conan profile show --profile:host windows/msvc/2022/debug --profile:build windows/msvc/2022/release
ERROR: 'settings.os.libc' doesn't exist for 'Windows'
'settings.os' possible configurations are ['subsystem']
```
Makes sense, kind of expected.
```
os:
    Windows:
        libc:
            null:
```
will probably not crash either.

So this is more a UX thing that a bug, might be worth checking if it can be easily improved.
Closed by https://github.com/conan-io/conan/pull/16065, will be in Conan 2.3



Patch:
diff --git a/conans/model/settings.py b/conans/model/settings.py
index d9cf3e8ed7b..8ebe924b4bf 100644
--- a/conans/model/settings.py
+++ b/conans/model/settings.py
@@ -18,25 +18,32 @@ def undefined_field(name, field, fields=None, value=None):
     return ConanException("\n".join(result))
 
 
-class SettingsItem(object):
+class SettingsItem:
     """ represents a setting value and its child info, which could be:
     - A range of valid values: [Debug, Release] (for settings.compiler.runtime of VS)
     - List [None, "ANY"] to accept None or any value
     - A dict {subsetting: definition}, e.g. {version: [], runtime: []} for VS
     """
-    def __init__(self, definition, name):
+    def __init__(self, definition, name, value):
+        self._definition = definition  # range of possible values
         self._name = name  # settings.compiler
-        self._value = None  # gcc
+        self._value = value  # gcc
+
+    @staticmethod
+    def new(definition, name):
+        if definition is None:
+            raise ConanException(f"Definition of settings.yml '{name}' cannot be null")
         if isinstance(definition, dict):
-            self._definition = {}
+            parsed_definitions = {}
             # recursive
             for k, v in definition.items():
                 # None string from yaml definition maps to python None, means not-defined value
                 k = str(k) if k is not None else None
-                self._definition[k] = Settings(v, name, k)
+                parsed_definitions[k] = Settings(v, name, k)
         else:
             # list or tuple of possible values, it can include "ANY"
-            self._definition = [str(v) if v is not None else None for v in definition]
+            parsed_definitions = [str(v) if v is not None else None for v in definition]
+        return SettingsItem(parsed_definitions, name, None)
 
     def __contains__(self, value):
         return value in (self._value or "")
@@ -44,13 +51,11 @@ def __contains__(self, value):
     def copy(self):
         """ deepcopy, recursive
         """
-        result = SettingsItem({}, name=self._name)
-        result._value = self._value
         if not isinstance(self._definition, dict):
-            result._definition = self._definition  # Not necessary to copy this, not mutable
+            definition = self._definition  # Not necessary to copy this, not mutable
         else:
-            result._definition = {k: v.copy() for k, v in self._definition.items()}
-        return result
+            definition = {k: v.copy() for k, v in self._definition.items()}
+        return SettingsItem(definition, self._name, self._value)
 
     def copy_conaninfo_settings(self):
         """ deepcopy, recursive
@@ -66,15 +71,12 @@ def copy_conaninfo_settings(self):
         - Settings that are "final" (lists), like build_type, or arch or compiler.version they
         can get any value without issues.
         """
-        result = SettingsItem({}, name=self._name)
-        result._value = self._value
         if not isinstance(self._definition, dict):
-            result._definition = self._definition[:] + ["ANY"]
+            definition = self._definition[:] + ["ANY"]
         else:
-            result._definition = {k: v.copy_conaninfo_settings()
-                                  for k, v in self._definition.items()}
-            result._definition["ANY"] = Settings()
-        return result
+            definition = {k: v.copy_conaninfo_settings() for k, v in self._definition.items()}
+            definition["ANY"] = Settings()
+        return SettingsItem(definition, self._name, self._value)
 
     def __bool__(self):
         if not self._value:
@@ -188,8 +190,7 @@ def __init__(self, definition=None, name="settings", parent_value="settings"):
             raise ConanException(f"Invalid settings.yml format: '{name}{val}' is not a dictionary")
         self._name = name  # settings, settings.compiler
         self._parent_value = parent_value  # gcc, x86
-        self._data = {k: SettingsItem(v, "%s.%s" % (name, k))
-                      for k, v in definition.items()}
+        self._data = {k: SettingsItem.new(v, f"{name}.{k}") for k, v in definition.items()}
         self._frozen = False
 
     def serialize(self):


Test patch:
diff --git a/conans/test/integration/settings/test_settings_user.py b/conans/test/integration/settings/test_settings_user.py
index 984f8137f1b..eb24e909f36 100644
--- a/conans/test/integration/settings/test_settings_user.py
+++ b/conans/test/integration/settings/test_settings_user.py
@@ -81,3 +81,15 @@ def test_settings_user_convert_list_dict():
     c.run("install . -s arch=x86_64 -s arch.subarch=2 ")
     assert "arch=x86_64" in c.out
     assert "arch.subarch=2" in c.out
+
+
+def test_settings_user_error():
+    c = TestClient()
+    settings_user = textwrap.dedent("""\
+        os:
+            Windows:
+                libc: null
+        """)
+    save(os.path.join(c.cache_folder, "settings_user.yml"), settings_user)
+    c.run("profile show", assert_error=True)
+    assert "ERROR: Definition of settings.yml 'settings.os.libc' cannot be null" in c.out