Repository: conan-io/conan (python)

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).


Pull request description:
Export msvs_toolset to conan.tools.microsoft.visual
It's useful in few recipes in CCI, the build system is not well planned by the project and we need from the scratch, like, passing all env vars and compiler arguments.

Changelog: Feature: Add msvs_toolset method to conan.tools.microsoft.visual to identify which toolset version is associated to the compiler version

Docs: https://github.com/conan-io/docs/pull/2951

closes #12044

/cc @jwillikers

- [ ] Refer to the issue that supports this Pull Request.
- [ ] If the issue has missing info, explain the purpose/use case/pain/need that covers this Pull Request.
- [ ] I've read the [Contributing guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).
- [ ] I've followed the PEP8 style guides for Python code.
- [ ] I've opened another PR in the Conan docs repo to the ``develop`` branch, documenting this one. 

<sup>**Note:** By default this PR will skip the slower tests and will use a limited set of python versions. Check [here](https://github.com/conan-io/conan/blob/develop/.github/PR_INCREASE_TESTING.md) how to increase the testing level by writing some tags in the current PR body text.</sup>


Patch:
diff --git a/conan/tools/microsoft/__init__.py b/conan/tools/microsoft/__init__.py
index 9c0bb920d..c060c64b8 100644
--- a/conan/tools/microsoft/__init__.py
+++ b/conan/tools/microsoft/__init__.py
@@ -6,4 +6,4 @@ from conan.tools.microsoft.toolchain import MSBuildToolchain
 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
index b98f6be45..2be8a67da 100644
--- a/conan/tools/microsoft/toolchain.py
+++ b/conan/tools/microsoft/toolchain.py
@@ -7,7 +7,7 @@ from jinja2 import Template
 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 @@ class MSBuildToolchain(object):
         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 @@ class MSBuildToolchain(object):
         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 @@ class MSBuildToolchain(object):
 
         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
index 5050ee8b8..1c6925949 100644
--- a/conan/tools/microsoft/visual.py
+++ b/conan/tools/microsoft/visual.py
@@ -4,6 +4,7 @@ import textwrap
 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
index 000000000..de1ec93b0
--- /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