Repository: conan-io/conan
Pull request #18265 (2025-05-08 09:56:25)

Allow composition of conf values that are different categories of numbers
Changelog: Fix: Allow composition of conf values that are different categories of numbers
Docs: Omit

Closes https://github.com/conan-io/conan/issues/18260


Patch:
diff --git a/conan/api/subapi/cache.py b/conan/api/subapi/cache.py
index 5036772c8..b0d3ede2d 100644
--- a/conan/api/subapi/cache.py
+++ b/conan/api/subapi/cache.py
@@ -6,6 +6,7 @@ from io import BytesIO
 
 from conan.api.model import PackagesList
 from conan.api.output import ConanOutput
+from conan.internal.api.uploader import gzopen_without_timestamps
 from conan.internal.cache.cache import PkgCache
 from conan.internal.cache.conan_reference_layout import EXPORT_SRC_FOLDER, EXPORT_FOLDER, SRC_FOLDER, \
     METADATA, DOWNLOAD_EXPORT_FOLDER
@@ -16,7 +17,7 @@ from conan.errors import ConanException
 from conan.api.model import PkgReference
 from conan.api.model import RecipeReference
 from conan.internal.util.dates import revision_timestamp_now
-from conan.internal.util.files import rmdir, gzopen_without_timestamps, mkdir, remove
+from conan.internal.util.files import rmdir, mkdir, remove
 
 
 class CacheAPI:
@@ -133,7 +134,7 @@ class CacheAPI:
         name = os.path.basename(tgz_path)
         compresslevel = global_conf.get("core.gzip:compresslevel", check_type=int)
         with open(tgz_path, "wb") as tgz_handle:
-            tgz = gzopen_without_timestamps(name, mode="w", fileobj=tgz_handle,
+            tgz = gzopen_without_timestamps(name, fileobj=tgz_handle,
                                             compresslevel=compresslevel)
             for ref, ref_bundle in package_list.refs().items():
                 ref_layout = cache.recipe_layout(ref)
diff --git a/conan/internal/api/config/config_installer.py b/conan/internal/api/config/config_installer.py
index 120ee44c6..e411897c9 100644
--- a/conan/internal/api/config/config_installer.py
+++ b/conan/internal/api/config/config_installer.py
@@ -1,6 +1,7 @@
 import os
 import shutil
 import fnmatch
+import zipfile
 
 from urllib.parse import urlparse, urlsplit
 from contextlib import contextmanager
@@ -9,8 +10,9 @@ from conan.api.output import ConanOutput
 from conan.internal.paths import find_file_walk_up
 from conan.internal.rest.file_downloader import FileDownloader
 from conan.errors import ConanException
-from conan.internal.util.files import mkdir, rmdir, remove, unzip, chdir
+from conan.internal.util.files import mkdir, rmdir, remove, chdir
 from conan.internal.util.runners import detect_runner
+from conan.tools.files.files import untargz
 
 
 class _ConanIgnoreMatcher:
@@ -85,7 +87,20 @@ def _process_git_repo(config, cache_folder):
 
 
 def _process_zip_file(config, zippath, cache_folder, tmp_folder, first_remove=False):
-    unzip(zippath, tmp_folder)
+    # First, unzip. This is repeated with the tools.unzip, but better do not mess
+    # Same list as below
+    tgz_exts = (".tar.gz", ".tgz", ".tbz2", ".tar.bz2", ".tar", ".tar.xz", ".txz")
+    if any(zippath.endswith(e) for e in tgz_exts):
+        untargz(zippath, tmp_folder)
+    else:
+        full_path = os.path.normpath(os.path.join(os.getcwd(), tmp_folder))
+        with zipfile.ZipFile(zippath, "r") as z:
+            zip_info = z.infolist()
+            extracted_size = 0
+            for file_ in zip_info:
+                extracted_size += file_.file_size
+                z.extract(file_, full_path)
+
     if first_remove:
         os.unlink(zippath)
     _process_folder(config, tmp_folder, cache_folder)
@@ -190,7 +205,7 @@ def _is_compressed_file(filename):
     import zipfile
     if zipfile.is_zipfile(filename):
         return True
-    tgz_exts = (".tar.gz", ".tgz", ".tbz2", ".tar.bz2", ".tar", ".gz", ".tar.xz", ".txz")
+    tgz_exts = (".tar.gz", ".tgz", ".tbz2", ".tar.bz2", ".tar", ".tar.xz", ".txz")
     return any(filename.endswith(e) for e in tgz_exts)
 
 
diff --git a/conan/internal/api/uploader.py b/conan/internal/api/uploader.py
index 4433921ce..b075e4f07 100644
--- a/conan/internal/api/uploader.py
+++ b/conan/internal/api/uploader.py
@@ -1,6 +1,8 @@
 import fnmatch
+import gzip
 import os
 import shutil
+import tarfile
 import time
 
 from conan.internal.conan_app import ConanApp
@@ -11,7 +13,7 @@ from conan.errors import ConanException
 from conan.internal.paths import (CONAN_MANIFEST, CONANFILE, EXPORT_SOURCES_TGZ_NAME,
                                   EXPORT_TGZ_NAME, PACKAGE_TGZ_NAME, CONANINFO)
 from conan.internal.util.files import (clean_dirty, is_dirty, gather_files,
-                                       gzopen_without_timestamps, set_dirty_context_manager, mkdir, human_size)
+                                       set_dirty_context_manager, mkdir, human_size)
 
 UPLOAD_POLICY_FORCE = "force-upload"
 UPLOAD_POLICY_SKIP = "skip-upload"
@@ -254,14 +256,28 @@ class UploadExecutor:
         output.debug(f"Upload {pref} in {duration} time")
 
 
+def gzopen_without_timestamps(name, fileobj, compresslevel=None):
+    """ !! Method overrided by laso to pass mtime=0 (!=None) to avoid time.time() was
+        setted in Gzip file causing md5 to change. Not possible using the
+        previous tarfile open because arguments are not passed to GzipFile constructor
+    """
+    compresslevel = compresslevel if compresslevel is not None else 9  # default Gzip = 9
+    fileobj = gzip.GzipFile(name, "w", compresslevel, fileobj, mtime=0)
+    # Format is forced because in Python3.8, it changed and it generates different tarfiles
+    # with different checksums, which break hashes of tgzs
+    # PAX_FORMAT is the default for Py38, lets make it explicit for older Python versions
+    t = tarfile.TarFile.taropen(name, "w", fileobj, format=tarfile.PAX_FORMAT)
+    t._extfileobj = False
+    return t
+
+
 def compress_files(files, name, dest_dir, compresslevel=None, ref=None):
     t1 = time.time()
     # FIXME, better write to disk sequentially and not keep tgz contents in memory
     tgz_path = os.path.join(dest_dir, name)
     ConanOutput(scope=str(ref)).info(f"Compressing {name}")
     with set_dirty_context_manager(tgz_path), open(tgz_path, "wb") as tgz_handle:
-        tgz = gzopen_without_timestamps(name, mode="w", fileobj=tgz_handle,
-                                        compresslevel=compresslevel)
+        tgz = gzopen_without_timestamps(name, fileobj=tgz_handle, compresslevel=compresslevel)
         for filename, abs_path in sorted(files.items()):
             # recursive is False in case it is a symlink to a folder
             tgz.add(abs_path, filename, recursive=False)
diff --git a/conan/internal/graph/graph.py b/conan/internal/graph/graph.py
index 8348af88d..8e4dc2e29 100644
--- a/conan/internal/graph/graph.py
+++ b/conan/internal/graph/graph.py
@@ -342,7 +342,9 @@ class Overrides:
         return self._overrides.items()
 
     def serialize(self):
-        return {k.repr_notime(): [e.repr_notime() if e else None for e in v]
+        return {k.repr_notime():
+                    sorted([e.repr_notime() if e else None for e in v],
+                           key= lambda e: "" if e is None else e)
                 for k, v in self._overrides.items()}
 
     @staticmethod
diff --git a/conan/internal/model/conf.py b/conan/internal/model/conf.py
index 4cb9d7c29..0b4125bd1 100644
--- a/conan/internal/model/conf.py
+++ b/conan/internal/model/conf.py
@@ -263,6 +263,9 @@ class _ConfValue:
                 new_value = other._value.copy()
                 new_value.update(self._value)
                 self._value = new_value
+        elif issubclass(v_type, numbers.Number) and issubclass(o_type, numbers.Number):
+            # They might be different kind of numbers, so skip the check below
+            pass
         elif self._value is None or other._value is None:
             # It means any of those values were an "unset" so doing nothing because we don't
             # really know the original value type
diff --git a/conan/internal/model/info.py b/conan/internal/model/info.py
index 2e7798970..e5f59d845 100644
--- a/conan/internal/model/info.py
+++ b/conan/internal/model/info.py
@@ -24,15 +24,13 @@ class _VersionRepr:
             return str(self._version.major)
         return ".".join([str(self._version.major), 'Y', 'Z'])
 
-    def minor(self, fill=True):
+    def minor(self):
         if not isinstance(self._version.major.value, int):
             return str(self._version.major)
 
         v0 = str(self._version.major)
         v1 = str(self._version.minor) if self._version.minor is not None else "0"
-        if fill:
-            return ".".join([v0, v1, 'Z'])
-        return ".".join([v0, v1])
+        return ".".join([v0, v1, 'Z'])
 
     def patch(self):
         if not isinstance(self._version.major.value, int):
@@ -135,14 +133,6 @@ class RequirementInfo:
         self.package_id = None
         self.recipe_revision = None
 
-    def full_recipe_mode(self):
-        self.name = self._ref.name
-        self.version = self._ref.version
-        self.user = self._ref.user
-        self.channel = self._ref.channel
-        self.package_id = None
-        self.recipe_revision = None
-
     def full_package_mode(self):
         self.name = self._ref.name
         self.version = self._ref.version
@@ -167,6 +157,7 @@ class RequirementInfo:
         self.package_id = self._package_id
         self.recipe_revision = self._ref.revision
 
+    full_recipe_mode = full_version_mode
     recipe_revision_mode = full_mode  # to not break everything and help in upgrade
 
 
diff --git a/conan/internal/util/files.py b/conan/internal/util/files.py
index fd03715ee..98746479a 100644
--- a/conan/internal/util/files.py
+++ b/conan/internal/util/files.py
@@ -256,40 +256,6 @@ def mkdir(path):
     os.makedirs(path)
 
 
-def gzopen_without_timestamps(name, mode="r", fileobj=None, compresslevel=None, **kwargs):
-    """ !! Method overrided by laso to pass mtime=0 (!=None) to avoid time.time() was
-        setted in Gzip file causing md5 to change. Not possible using the
-        previous tarfile open because arguments are not passed to GzipFile constructor
-    """
-
-    if mode not in ("r", "w"):
-        raise ValueError("mode must be 'r' or 'w'")
-
-    try:
-        compresslevel = compresslevel if compresslevel is not None else 9  # default Gzip = 9
-        fileobj = gzip.GzipFile(name, mode, compresslevel, fileobj, mtime=0)
-    except OSError:
-        if fileobj is not None and mode == 'r':
-            raise tarfile.ReadError("not a gzip file")
-        raise
-
-    try:
-        # Format is forced because in Python3.8, it changed and it generates different tarfiles
-        # with different checksums, which break hashes of tgzs
-        # PAX_FORMAT is the default for Py38, lets make it explicit for older Python versions
-        t = tarfile.TarFile.taropen(name, mode, fileobj, format=tarfile.PAX_FORMAT, **kwargs)
-    except IOError:
-        fileobj.close()
-        if mode == 'r':
-            raise tarfile.ReadError("not a gzip file")
-        raise
-    except Exception:
-        fileobj.close()
-        raise
-    t._extfileobj = False
-    return t
-
-
 def tar_extract(fileobj, destination_dir):
     the_tar = tarfile.open(fileobj=fileobj)
     # NOTE: The errorlevel=2 has been removed because it was failing in Win10, it didn't allow to
@@ -328,33 +294,6 @@ def gather_files(folder):
     return file_dict, symlinked_folders
 
 
-# FIXME: This is very repeated with the tools.unzip, but wsa needed for config-install unzip
-def unzip(filename, destination="."):
-    from conan.tools.files.files import untargz  # FIXME, importing from conan.tools
-    if (filename.endswith(".tar.gz") or filename.endswith(".tgz") or
-            filename.endswith(".tbz2") or filename.endswith(".tar.bz2") or
-            filename.endswith(".tar")):
-        return untargz(filename, destination)
-    if filename.endswith(".gz"):
-        with gzip.open(filename, 'rb') as f:
-            file_content = f.read()
-        target_name = filename[:-3] if destination == "." else destination
-        save(target_name, file_content)
-        return
-    if filename.endswith(".tar.xz") or filename.endswith(".txz"):
-        return untargz(filename, destination)
-
-    import zipfile
-    full_path = os.path.normpath(os.path.join(os.getcwd(), destination))
-
-    with zipfile.ZipFile(filename, "r") as z:
-        zip_info = z.infolist()
-        extracted_size = 0
-        for file_ in zip_info:
-            extracted_size += file_.file_size
-            z.extract(file_, full_path)
-
-
 def human_size(size_bytes):
     """
     format a size in bytes into a 'human' file size, e.g. B, KB, MB, GB, TB, PB


Test patch:
diff --git a/conan/test/utils/test_files.py b/conan/test/utils/test_files.py
index c65d6e169..7b3f01111 100644
--- a/conan/test/utils/test_files.py
+++ b/conan/test/utils/test_files.py
@@ -8,11 +8,11 @@ import tempfile
 import time
 from io import BytesIO
 
+from conan.internal.api.uploader import gzopen_without_timestamps
 from conan.tools.files.files import untargz
 from conan.internal.subsystems import get_cased_path
 from conan.errors import ConanException
 from conan.internal.paths import PACKAGE_TGZ_NAME
-from conan.internal.util.files import gzopen_without_timestamps
 
 
 def wait_until_removed(folder):
@@ -85,7 +85,7 @@ def tgz_with_contents(files, output_path=None):
     file_path = output_path or os.path.join(folder, "myfile.tar.gz")
 
     with open(file_path, "wb") as tgz_handle:
-        tgz = gzopen_without_timestamps("myfile.tar.gz", mode="w", fileobj=tgz_handle)
+        tgz = gzopen_without_timestamps("myfile.tar.gz", fileobj=tgz_handle)
 
         for name, content in files.items():
             info = tarfile.TarInfo(name=name)
diff --git a/test/integration/command/upload/upload_test.py b/test/integration/command/upload/upload_test.py
index 7b0bbaa82..633682087 100644
--- a/test/integration/command/upload/upload_test.py
+++ b/test/integration/command/upload/upload_test.py
@@ -13,10 +13,11 @@ from requests import Response
 from conan.errors import ConanException
 from conan.api.model import PkgReference
 from conan.api.model import RecipeReference
+from conan.internal.api.uploader import gzopen_without_timestamps
 from conan.internal.paths import EXPORT_SOURCES_TGZ_NAME, PACKAGE_TGZ_NAME
 from conan.test.utils.tools import NO_SETTINGS_PACKAGE_ID, TestClient, TestServer, \
     GenConanfile, TestRequester, TestingResponse
-from conan.internal.util.files import gzopen_without_timestamps, is_dirty, save, set_dirty, sha1sum
+from conan.internal.util.files import is_dirty, save, set_dirty, sha1sum
 
 conanfile = """from conan import ConanFile
 from conan.tools.files import copy
@@ -148,10 +149,10 @@ class UploadTest(unittest.TestCase):
         pref = client.get_latest_package_reference(RecipeReference.loads("hello0/1.2.1@user/testing"),
                                                    NO_SETTINGS_PACKAGE_ID)
 
-        def gzopen_patched(name, mode="r", fileobj=None, **kwargs):
+        def gzopen_patched(name, fileobj, compresslevel=None):  # noqa
             if name == PACKAGE_TGZ_NAME:
                 raise ConanException("Error gzopen %s" % name)
-            return gzopen_without_timestamps(name, mode, fileobj, **kwargs)
+            return gzopen_without_timestamps(name, fileobj)
         with patch('conan.internal.api.uploader.gzopen_without_timestamps', new=gzopen_patched):
             client.run("upload * --confirm -r default", assert_error=True)
             self.assertIn("Error gzopen conan_package.tgz", client.out)
diff --git a/test/integration/configuration/profile_test.py b/test/integration/configuration/profile_test.py
index 434e06233..de8371031 100644
--- a/test/integration/configuration/profile_test.py
+++ b/test/integration/configuration/profile_test.py
@@ -636,3 +636,12 @@ def test_consumer_invalid_profile_multiple_groups():
                assert_error=True)
     assert ("ERROR: Error reading 'myprofs/myprofile' profile: ConfigParser: "
             "Duplicated section: [settings]") in client.out
+
+def test_compose_numeric_values_scoped_pkg():
+    tc = TestClient(light=True)
+    tc.save({"conanfile.py": GenConanfile("hello", "0.1")
+                .with_package("self.output.info('user.var.value: ' + str(self.conf.get('user.var:value')))"),
+             "profile": """[conf]\nuser.var:value=8.1\nhello/*:user.var:value=10\n"""})
+
+    tc.run("create . -pr=profile")
+    assert "user.var.value: 10" in tc.out
diff --git a/test/integration/package_id/package_id_requires_modes_test.py b/test/integration/package_id/package_id_requires_modes_test.py
index a3a067fad..cad10498c 100644
--- a/test/integration/package_id/package_id_requires_modes_test.py
+++ b/test/integration/package_id/package_id_requires_modes_test.py
@@ -1,5 +1,4 @@
 import textwrap
-import unittest
 
 import pytest
 
@@ -34,7 +33,29 @@ class TestPackageIDRequirementsModes:
             assert f"dep/{pattern}" in c.out
 
 
-class PackageIDErrorTest(unittest.TestCase):
+@pytest.mark.parametrize("mode, pkg_id",
+                             [("unrelated_mode", "da39a3ee5e6b4b0d3255bfef95601890afd80709"),
+                              ("semver_mode", "13b9e753af3958dd1b2d4b3f935b04b8fb6b6760"),
+                              ("patch_mode", "38d7a3ec6a09165ab3e5306f81c539a2e0a784bd"),
+                              ("minor_mode", "a5e7ad26ccf4a5049090976846da1c6ed165cced"),
+                              ("major_mode", "6ac597ffb99c3747ed78699f206dc1041537a8df"),
+                              # This is equal to semver_mode for 0.X.Y.Z..
+                              ("full_version_mode", "13b9e753af3958dd1b2d4b3f935b04b8fb6b6760"),
+                              ("revision_mode", "ae5b9eeb74880aeb1cfa3db7f84c007a05ce3a76"),
+                              ("full_mode", "d1b2a9538cd69363b4bae7e66c9f900b8f4c58bb")])
+def test_modes(mode, pkg_id):
+    c = TestClient(light=True)
+    package_id_text = f'self.info.requires.{mode}()'
+    c.save({"dep/conanfile.py": GenConanfile("dep", "0.1.1.1").with_settings("os"),
+            "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_requires("dep/[*]")
+                                                          .with_package_id(package_id_text)})
+    c.run("create dep -s os=Linux")
+    c.run("create pkg -s os=Linux")
+    pkgid = c.created_package_id("pkg/0.1")
+    assert pkgid == pkg_id
+
+
+class TestPackageIDError:
 
     def test_transitive_multi_mode_package_id(self):
         # https://github.com/conan-io/conan/issues/6942
@@ -52,7 +73,7 @@ class PackageIDErrorTest(unittest.TestCase):
         client.save({"conanfile.py": GenConanfile().with_require("dep2/1.0@user/testing")
                                                    .with_require("dep3/1.0@user/testing")})
         client.run('create . --name=consumer --version=1.0 --user=user --channel=testing --build=*')
-        self.assertIn("consumer/1.0@user/testing: Created", client.out)
+        assert "consumer/1.0@user/testing: Created" in client.out
 
     def test_transitive_multi_mode2_package_id(self):
         # https://github.com/conan-io/conan/issues/6942
@@ -77,9 +98,9 @@ class PackageIDErrorTest(unittest.TestCase):
                 """)
         client.save({"conanfile.py": consumer})
         client.run('create . --name=consumer --version=1.0 --user=user --channel=testing --build=*')
-        self.assertIn("dep2/1.0@user/testing: PkgNames: ['dep1']", client.out)
-        self.assertIn("consumer/1.0@user/testing: PKGNAMES: ['dep1', 'dep2']", client.out)
-        self.assertIn("consumer/1.0@user/testing: Created", client.out)
+        assert "dep2/1.0@user/testing: PkgNames: ['dep1']" in client.out
+        assert "consumer/1.0@user/testing: PKGNAMES: ['dep1', 'dep2']" in client.out
+        assert "consumer/1.0@user/testing: Created" in client.out
 
     def test_transitive_multi_mode_build_requires(self):
         # https://github.com/conan-io/conan/issues/6942
@@ -106,9 +127,9 @@ class PackageIDErrorTest(unittest.TestCase):
                 """)
         client.save({"conanfile.py": consumer})
         client.run('create . --name=consumer --version=1.0 --user=user --channel=testing --build=*')
-        self.assertIn("dep2/1.0@user/testing: PkgNames: ['dep1']", client.out)
-        self.assertIn("consumer/1.0@user/testing: PKGNAMES: ['dep1', 'dep2']", client.out)
-        self.assertIn("consumer/1.0@user/testing: Created", client.out)
+        assert "dep2/1.0@user/testing: PkgNames: ['dep1']" in client.out
+        assert "consumer/1.0@user/testing: PKGNAMES: ['dep1', 'dep2']" in client.out
+        assert "consumer/1.0@user/testing: Created" in client.out
 
 
 class TestRequirementPackageId:
diff --git a/test/integration/package_id/python_requires_package_id_test.py b/test/integration/package_id/python_requires_package_id_test.py
index 7c547431b..b7847b09a 100644
--- a/test/integration/package_id/python_requires_package_id_test.py
+++ b/test/integration/package_id/python_requires_package_id_test.py
@@ -1,6 +1,8 @@
 import textwrap
 import unittest
 
+import pytest
+
 from conan.test.utils.tools import TestClient, GenConanfile
 from conan.internal.util.files import save
 
@@ -176,3 +178,23 @@ class TestPythonRequiresHeaderOnly:
         c.run("create tool --version=1.2")
         c.run("install --requires=pkg/0.1")
         c.assert_listed_binary({"pkg/0.1": (pkgid, "Cache")})
+
+
+@pytest.mark.parametrize("mode, pkg_id",
+                         [("unrelated_mode", "da39a3ee5e6b4b0d3255bfef95601890afd80709"),
+                          ("semver_mode", "1f0070b00ccebfec93dc90854a163c7af229f587"),
+                          ("patch_mode", "b9ca872dfd5b48f5f1f69d66f0950fc35469d0cd"),
+                          ("minor_mode", "c53bd9e48dd09ceeaa1bb425830490d8b243e39c"),
+                          ("major_mode", "331c17383dcdf37f79bc2b86fa55ac56afdc6fec"),
+                          ("full_version_mode", "1f0070b00ccebfec93dc90854a163c7af229f587"),
+                          ("revision_mode", "0071dd0296afa0db533e21f924273485c87f0d32"),
+                          ("full_mode", "0071dd0296afa0db533e21f924273485c87f0d32")])
+def test_modes(mode, pkg_id):
+    c = TestClient(light=True)
+    c.save_home({"global.conf": f"core.package_id:default_python_mode={mode}"})
+    c.save({"dep/conanfile.py": GenConanfile("dep", "0.1.1.1"),
+            "pkg/conanfile.py": GenConanfile("pkg", "0.1").with_python_requires("dep/[*]")})
+    c.run("create dep")
+    c.run("create pkg")
+    pkgid = c.created_package_id("pkg/0.1")
+    assert pkgid == pkg_id
diff --git a/test/unittests/client/profile_loader/profile_loader_test.py b/test/unittests/client/profile_loader/profile_loader_test.py
index 88effcfbf..18bfe1088 100644
--- a/test/unittests/client/profile_loader/profile_loader_test.py
+++ b/test/unittests/client/profile_loader/profile_loader_test.py
@@ -252,3 +252,19 @@ def test_profile_core_confs_error(conf_name):
     with pytest.raises(ConanException) as exc:
         profile_loader.from_cli_args([], [], [], [conf_name], None)
     assert "[conf] 'core.*' configurations are not allowed in profiles" in str(exc.value)
+
+
+def test_profile_compose_numbers():
+    tmp = temp_folder()
+    txt = textwrap.dedent("""
+            [conf]
+            user.version:value=8.1
+            pkg/*:user.version:value=10
+            """)
+    current_profile_path = os.path.join(tmp, "default")
+    save(current_profile_path, txt)
+
+    profile_loader = ProfileLoader(cache_folder=temp_folder())  # If not used cache, will not error
+    profile = profile_loader.load_profile(current_profile_path)
+    assert profile.conf.get("user.version:value") == 8.1
+    assert profile.conf.get("pkg/*:user.version:value") == 10
diff --git a/test/unittests/model/test_conf.py b/test/unittests/model/test_conf.py
index 9afa00248..a06707f6d 100644
--- a/test/unittests/model/test_conf.py
+++ b/test/unittests/model/test_conf.py
@@ -2,6 +2,7 @@ import textwrap
 
 import pytest
 
+from conan.api.model import RecipeReference
 from conan.errors import ConanException
 from conan.internal.model.conf import ConfDefinition
 
@@ -274,6 +275,13 @@ def test_compose_conf_dict_updates():
     assert c.dumps() == ("user.company:mydict={'2': 'b'}\n"
                          "user.company:mydict2={'1': 'a', '2': 'b'}\n")
 
+def test_compose_conf_numbers():
+    c = ConfDefinition()
+    c.loads("user.version:value=8.1\n"
+            "foo/*:user.version:value=10")
+    assert c.get("user.version:value") == 8.1
+    assert c.get_conanfile_conf(RecipeReference.loads("foo/1.0")).get("user.version:value") == 10
+
 
 def test_conf_get_check_type_and_default():
     text = textwrap.dedent("""\
diff --git a/test/unittests/util/files/strip_root_extract_test.py b/test/unittests/util/files/strip_root_extract_test.py
index 6906f7a11..1e219be78 100644
--- a/test/unittests/util/files/strip_root_extract_test.py
+++ b/test/unittests/util/files/strip_root_extract_test.py
@@ -3,6 +3,7 @@ import tarfile
 import unittest
 import zipfile
 
+from conan.internal.api.uploader import gzopen_without_timestamps
 from conan.tools.files.files import untargz, unzip
 from conan.errors import ConanException
 from conan.internal.model.manifest import gather_files
@@ -11,7 +12,7 @@ from conan.test.utils.mocks import RedirectedTestOutput
 from conan.test.utils.test_files import temp_folder
 from conan.test.utils.tools import redirect_output
 from conan.internal.util.files import chdir, save
-from conan.internal.util.files import gzopen_without_timestamps, rmdir
+from conan.internal.util.files import rmdir
 
 
 class ZipExtractPlainTest(unittest.TestCase):
@@ -110,7 +111,7 @@ class TarExtractPlainTest(unittest.TestCase):
         # Create a tar.gz file with the files in the folder and an additional TarInfo entry
         # for the folder_entry (the gather files doesn't return empty dirs)
         with open(tgz_path, "wb") as tgz_handle:
-            tgz = gzopen_without_timestamps("name", mode="w", fileobj=tgz_handle)
+            tgz = gzopen_without_timestamps("name", fileobj=tgz_handle)
             if folder_entry:
                 # Create an empty folder in the tgz file
                 t = tarfile.TarInfo(folder_entry)
@@ -131,7 +132,7 @@ class TarExtractPlainTest(unittest.TestCase):
         tgz_path = os.path.join(tmp_folder, "foo.tgz")
 
         with open(tgz_path, "wb") as tgz_handle:
-            tgz = gzopen_without_timestamps("name", mode="w", fileobj=tgz_handle)
+            tgz = gzopen_without_timestamps("name", fileobj=tgz_handle)
 
             # Regular file
             info = tarfile.TarInfo(name="common/foo.txt")
@@ -286,7 +287,7 @@ def _compress_root_folder(folder, tgz_path, root_folder_name="root"):
     root/parent/bin/file2
     """
     with open(tgz_path, "wb") as tgz_handle:
-        tgz = gzopen_without_timestamps("name", mode="w", fileobj=tgz_handle)
+        tgz = gzopen_without_timestamps("name", fileobj=tgz_handle)
         files, _ = gather_files(folder)
         tgz.add(root_folder_name, arcname=os.path.basename(root_folder_name))
         tgz.close()
diff --git a/test/unittests/util/files/tar_extract_test.py b/test/unittests/util/files/tar_extract_test.py
index 8327902bd..6f031a56a 100644
--- a/test/unittests/util/files/tar_extract_test.py
+++ b/test/unittests/util/files/tar_extract_test.py
@@ -1,5 +1,3 @@
-# coding=utf-8
-
 import os
 import platform
 import tarfile
@@ -7,8 +5,9 @@ import unittest
 
 import pytest
 
+from conan.internal.api.uploader import gzopen_without_timestamps
 from conan.test.utils.test_files import temp_folder
-from conan.internal.util.files import tar_extract, gzopen_without_timestamps, save, gather_files, chdir
+from conan.internal.util.files import tar_extract, save, gather_files, chdir
 
 
 class TarExtractTest(unittest.TestCase):
@@ -26,7 +25,7 @@ class TarExtractTest(unittest.TestCase):
             # Create a tar.gz file with the above files
             self.tgz_file = os.path.join(self.tmp_folder, "file.tar.gz")
             with open(self.tgz_file, "wb") as tgz_handle:
-                tgz = gzopen_without_timestamps("name", mode="w", fileobj=tgz_handle)
+                tgz = gzopen_without_timestamps("name", fileobj=tgz_handle)
 
                 files, _ = gather_files(ori_files_dir)
                 for filename, abs_path in files.items():