Repository: conan-io/conan

Issue:
[feature] Provide conan.tools.microsoft.msvs_toolset equivalent of conans.tools.msvs_toolset
Would it be possible to provide the [msvs_toolset](https://docs.conan.io/en/latest/reference/tools.html#tools-msvs-toolset) function in the `conan` namespace and add support for the `msvc` compiler in it?
I had to workaround the use of the original `msvs_toolset` function in my recent update to the Boost recipe in conan-io/conan-center-index#12819.


- [X] I've read the [CONTRIBUTING guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).



Discussion:
Yes, I think it should be possible, should be relatively easy, want to contribute it?

Resolving patch:
diff --git a/conan/tools/microsoft/__init__.py b/conan/tools/microsoft/__init__.py
--- a/conan/tools/microsoft/__init__.py
+++ b/conan/tools/microsoft/__init__.py
@@ -6,4 +6,4 @@
 from conan.tools.microsoft.nmaketoolchain import NMakeToolchain
 from conan.tools.microsoft.nmakedeps import NMakeDeps
 from conan.tools.microsoft.visual import msvc_runtime_flag, VCVars, is_msvc, \
-    is_msvc_static_runtime, check_min_vs
+    is_msvc_static_runtime, check_min_vs, msvs_toolset
diff --git a/conan/tools/microsoft/toolchain.py b/conan/tools/microsoft/toolchain.py
--- a/conan/tools/microsoft/toolchain.py
+++ b/conan/tools/microsoft/toolchain.py
@@ -7,7 +7,7 @@
 from conan.tools._check_build_profile import check_using_build_profile
 from conan.tools.build import build_jobs
 from conan.tools.intel.intel_cc import IntelCC
-from conan.tools.microsoft.visual import VCVars, msvc_version_to_toolset_version
+from conan.tools.microsoft.visual import VCVars, msvs_toolset
 from conans.errors import ConanException
 from conans.util.files import save, load
 
@@ -54,7 +54,7 @@ def __init__(self, conanfile):
         self.configuration = conanfile.settings.build_type
         self.runtime_library = self._runtime_library(conanfile.settings)
         self.cppstd = conanfile.settings.get_safe("compiler.cppstd")
-        self.toolset = self._msvs_toolset(conanfile)
+        self.toolset = msvs_toolset(conanfile)
         self.properties = {}
         check_using_build_profile(self._conanfile)
 
@@ -82,37 +82,6 @@ def generate(self):
         else:
             VCVars(self._conanfile).generate()
 
-    @staticmethod
-    def _msvs_toolset(conanfile):
-        settings = conanfile.settings
-        compiler = settings.get_safe("compiler")
-        compiler_version = settings.get_safe("compiler.version")
-        if compiler == "msvc":
-            subs_toolset = settings.get_safe("compiler.toolset")
-            if subs_toolset:
-                return subs_toolset
-            return msvc_version_to_toolset_version(compiler_version)
-        if compiler == "intel":
-            compiler_version = compiler_version if "." in compiler_version else \
-                "%s.0" % compiler_version
-            return "Intel C++ Compiler " + compiler_version
-        if compiler == "intel-cc":
-            return IntelCC(conanfile).ms_toolset
-        if compiler == "Visual Studio":
-            toolset = settings.get_safe("compiler.toolset")
-            if not toolset:
-                toolsets = {"17": "v143",
-                            "16": "v142",
-                            "15": "v141",
-                            "14": "v140",
-                            "12": "v120",
-                            "11": "v110",
-                            "10": "v100",
-                            "9": "v90",
-                            "8": "v80"}
-                toolset = toolsets.get(compiler_version)
-            return toolset or ""
-
     @staticmethod
     def _runtime_library(settings):
         compiler = settings.compiler
@@ -148,7 +117,7 @@ def format_macro(key, value):
 
         cppstd = "stdcpp%s" % self.cppstd if self.cppstd else ""
         runtime_library = self.runtime_library
-        toolset = self.toolset
+        toolset = self.toolset or ""
         compile_options = self._conanfile.conf.get("tools.microsoft.msbuildtoolchain:compile_options",
                                                    default={}, check_type=dict)
         self.compile_options.update(compile_options)
diff --git a/conan/tools/microsoft/visual.py b/conan/tools/microsoft/visual.py
--- a/conan/tools/microsoft/visual.py
+++ b/conan/tools/microsoft/visual.py
@@ -4,6 +4,7 @@
 from conans.client.tools import vs_installation_path
 from conans.client.tools.version import Version
 from conans.errors import ConanException, ConanInvalidConfiguration
+from conan.tools.intel.intel_cc import IntelCC
 
 CONAN_VCVARS_FILE = "conanvcvars.bat"
 
@@ -56,7 +57,7 @@ def msvc_version_to_toolset_version(version):
                 '191': 'v141',
                 '192': 'v142',
                 "193": 'v143'}
-    return toolsets[str(version)]
+    return toolsets.get(str(version))
 
 
 class VCVars:
@@ -275,3 +276,42 @@ def is_msvc_static_runtime(conanfile):
     :return: True, if msvc + runtime MT. Otherwise, False
     """
     return is_msvc(conanfile) and "MT" in msvc_runtime_flag(conanfile)
+
+
+def msvs_toolset(conanfile):
+    """ Returns the corresponding platform toolset based on the compiler of the given conanfile.
+        In case no toolset is configured in the profile, it will return a toolset based on the
+        compiler version, otherwise, it will return the toolset from the profile.
+        When there is no compiler version neither toolset configured, it will return None
+        It supports Visual Studio, msvc and Intel.
+    :param conanfile: Conanfile instance to access settings.compiler
+    :return: A toolset when compiler.version is valid or compiler.toolset is configured. Otherwise, None.
+    """
+    settings = conanfile.settings
+    compiler = settings.get_safe("compiler")
+    compiler_version = settings.get_safe("compiler.version")
+    if compiler == "msvc":
+        subs_toolset = settings.get_safe("compiler.toolset")
+        if subs_toolset:
+            return subs_toolset
+        return msvc_version_to_toolset_version(compiler_version)
+    if compiler == "intel" and compiler_version:
+        compiler_version = compiler_version if "." in compiler_version else \
+            f"{compiler_version}.0"
+        return "Intel C++ Compiler " + compiler_version
+    if compiler == "intel-cc":
+        return IntelCC(conanfile).ms_toolset
+    if compiler == "Visual Studio":
+        toolset = settings.get_safe("compiler.toolset")
+        if not toolset:
+            toolsets = {"17": "v143",
+                        "16": "v142",
+                        "15": "v141",
+                        "14": "v140",
+                        "12": "v120",
+                        "11": "v110",
+                        "10": "v100",
+                        "9": "v90",
+                        "8": "v80"}
+            toolset = toolsets.get(str(compiler_version))
+        return toolset


Test patch:
diff --git a/conans/test/unittests/tools/microsoft/test_msvs_toolset.py b/conans/test/unittests/tools/microsoft/test_msvs_toolset.py
new file mode 100644
--- /dev/null
+++ b/conans/test/unittests/tools/microsoft/test_msvs_toolset.py
@@ -0,0 +1,141 @@
+import pytest
+from conan.tools.microsoft import msvs_toolset
+from conan.errors import ConanException
+from conans.test.utils.mocks import ConanFileMock, MockSettings
+
+
+@pytest.mark.parametrize("compiler_version,expected_toolset", [
+    ("17", "v143"),
+    ("16", "v142"),
+    ("15", "v141"),
+    ("14", "v140"),
+    ("12", "v120"),
+    ("11", "v110"),
+    ("10", "v100"),
+    ("9", "v90"),
+    ("8", "v80")])
+def test_visual_studio_default(compiler_version, expected_toolset):
+    """ When running Visual Studio as compiler, and there is no toolset configured,
+        msvs_toolset must return a specific version based on the compiler version
+    """
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"compiler": "Visual Studio", "compiler.version": compiler_version})
+    assert msvs_toolset(conanfile) == expected_toolset
+
+
+@pytest.mark.parametrize("compiler_version,toolset,expected_toolset", [
+    ("16", "v142_xp", "v142_xp"),
+    ("15", "v141_xp", "v141_xp"),
+    ("14", "v140_xp", "v140_xp"),
+    ("12", "v120_xp", "v120_xp"),
+    ("11", "v110_xp", "v110_xp")])
+def test_visual_studio_custom(compiler_version, toolset, expected_toolset):
+    """ When running Visual Studio as compiler, and there is a toolset configured,
+        msvs_toolset must return the specified toolset from profile
+    """
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"compiler": "Visual Studio",
+                             "compiler.version": compiler_version,
+                             "compiler.toolset": toolset})
+    assert msvs_toolset(conanfile) == expected_toolset
+
+
+def test_invalid_compiler():
+    """ When the compiler version is unknown and there is no toolset configured,
+        msvs_toolset must return None
+    """
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"compiler": "Visual Studio",
+                                       "compiler.version": "20220203"})
+    assert msvs_toolset(conanfile) is None
+
+
+@pytest.mark.parametrize("compiler_version,expected_toolset", [
+    ("193", "v143"),
+    ("192", "v142"),
+    ("191", "v141"),
+    ("190", "v140"),
+    ("180", "v120"),
+    ("170", "v110")])
+def test_msvc_default(compiler_version, expected_toolset):
+    """ When running msvc as compiler, and there is no toolset configured,
+        msvs_toolset must return a specific version based on the compiler version
+    """
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"compiler": "msvc", "compiler.version": compiler_version})
+    assert msvs_toolset(conanfile) == expected_toolset
+
+
+@pytest.mark.parametrize("compiler_version,toolset,expected_toolset", [
+    ("193", "v143_xp", "v143_xp"),
+    ("192", "v142_xp", "v142_xp"),
+    ("191", "v141_xp", "v141_xp"),
+    ("190", "v140_xp", "v140_xp"),
+    ("180", "v120_xp", "v120_xp"),
+    ("170", "v110_xp", "v110_xp")])
+def test_msvc_custom(compiler_version, toolset, expected_toolset):
+    """ When running msvc as compiler, and there is a toolset configured,
+        msvs_toolset must return the specified toolset from profile
+    """
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"compiler": "msvc",
+                             "compiler.version": compiler_version,
+                             "compiler.toolset": toolset})
+    assert msvs_toolset(conanfile) == expected_toolset
+
+
+@pytest.mark.parametrize("compiler_version,expected_toolset", [
+    ("19.1", "Intel C++ Compiler 19.1"),
+    ("19", "Intel C++ Compiler 19.0"),
+    ("18", "Intel C++ Compiler 18.0"),
+    ("17", "Intel C++ Compiler 17.0")])
+def test_intel_default(compiler_version, expected_toolset):
+    """ When running intel as compiler, and there is a compiler version configured,
+        msvs_toolset must return the compiler version as its toolset
+    """
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"compiler": "intel", "compiler.version": compiler_version})
+    assert msvs_toolset(conanfile) == expected_toolset
+
+
+def test_intel_invalid():
+    """ When running intel as compiler, and there is no compiler version configured,
+        msvs_toolset must return None
+    """
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"compiler": "intel"})
+    assert msvs_toolset(conanfile) is None
+
+
+def test_intel_cc_old_compiler():
+    """ When running intel-cc as compiler, and the compiler version configured is older than 2021
+        msvs_toolset must raise an ConanExpection
+    """
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"compiler": "intel-cc", "compiler.version": "19.1"})
+    with pytest.raises(ConanException) as error:
+        msvs_toolset(conanfile)
+    assert "You have to use 'intel' compiler" in str(error.value)
+
+
+@pytest.mark.parametrize("compiler_version,compiler_mode,expected_toolset", [
+    ("2021.3", "classic", "Intel C++ Compiler 19.2"),
+    ("2021.2", "classic", "Intel C++ Compiler 19.2"),
+    ("2021.1", "classic", "Intel C++ Compiler 19.2"),
+    ("2021.3", "icx", "Intel C++ Compiler 2021"),
+    ("2021.2", "icx", "Intel C++ Compiler 2021"),
+    ("2021.1", "icx", "Intel C++ Compiler 2021"),
+    ("2021.3", "dpcpp", "Intel(R) oneAPI DPC++ Compiler"),
+    ("2021.2", "dpcpp", "Intel(R) oneAPI DPC++ Compiler"),
+    ("2021.1", "dpcpp", "Intel(R) oneAPI DPC++ Compiler"),
+    ("2021.3", None, "Intel(R) oneAPI DPC++ Compiler")])
+def test_intel_cc_default(compiler_version, compiler_mode, expected_toolset):
+    """ When running intel-cc as compiler, and there is a proper compiler version configured,
+        and the compiler.mode is configured,
+        msvs_toolset must return a compiler toolset based on compiler.mode
+    """
+    conanfile = ConanFileMock()
+    conanfile.settings = MockSettings({"compiler": "intel-cc",
+                                       "compiler.version": compiler_version,
+                                       "compiler.mode": compiler_mode})
+    assert msvs_toolset(conanfile) == expected_toolset