From d92513390a1a0da781bb08c284136f4d7abea36d Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Tue, 27 Apr 2021 16:45:55 +0800 Subject: [PATCH] bpo-43312: Functions returning default and preferred sysconfig schemes (GH-24644) --- Doc/library/sysconfig.rst | 37 ++++++++ Lib/sysconfig.py | 90 ++++++++++++------- Lib/test/test_sysconfig.py | 39 ++++++-- .../2021-02-25-14-43-59.bpo-43312.6dg9_2.rst | 3 + 4 files changed, 131 insertions(+), 38 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2021-02-25-14-43-59.bpo-43312.6dg9_2.rst diff --git a/Doc/library/sysconfig.rst b/Doc/library/sysconfig.rst index d4463f39fda..bb8e2d88c0f 100644 --- a/Doc/library/sysconfig.rst +++ b/Doc/library/sysconfig.rst @@ -107,6 +107,43 @@ identifier. Python currently uses eight paths: :mod:`sysconfig`. +.. function:: get_default_scheme() + + Return the default scheme name for the current platform. + + .. versionchanged:: 3.10 + This function was previously named ``_get_default_scheme()`` and + considered an implementation detail. + + +.. function:: get_preferred_scheme(key) + + Return a preferred scheme name for an installation layout specified by *key*. + + *key* must be either ``"prefix"``, ``"home"``, or ``"user"``. + + The return value is a scheme name listed in :func:`get_scheme_names`. It + can be passed to :mod:`sysconfig` functions that take a *scheme* argument, + such as :func:`get_paths`. + + .. versionadded:: 3.10 + + +.. function:: _get_preferred_schemes() + + Return a dict containing preferred scheme names on the current platform. + Python implementers and redistributors may add their preferred schemes to + the ``_INSTALL_SCHEMES`` module-level global value, and modify this function + to return those scheme names, to e.g. provide different schemes for system + and language package managers to use, so packages installed by either do not + mix with those by the other. + + End users should not use this function, but :func:`get_default_scheme` and + :func:`get_preferred_scheme()` instead. + + .. versionadded:: 3.10 + + .. function:: get_path_names() Return a tuple containing all path names currently supported in diff --git a/Lib/sysconfig.py b/Lib/sysconfig.py index 0e0decd4271..e8869af0b5c 100644 --- a/Lib/sysconfig.py +++ b/Lib/sysconfig.py @@ -79,7 +79,7 @@ def _getuserbase(): if sys.platform == "darwin" and sys._framework: return joinuser("~", "Library", sys._framework, - "%d.%d" % sys.version_info[:2]) + f"{sys.version_info[0]}.{sys.version_info[1]}") return joinuser("~", ".local") @@ -121,8 +121,8 @@ _SCHEME_KEYS = ('stdlib', 'platstdlib', 'purelib', 'platlib', 'include', 'scripts', 'data') _PY_VERSION = sys.version.split()[0] -_PY_VERSION_SHORT = '%d.%d' % sys.version_info[:2] -_PY_VERSION_SHORT_NO_DOT = '%d%d' % sys.version_info[:2] +_PY_VERSION_SHORT = f'{sys.version_info[0]}.{sys.version_info[1]}' +_PY_VERSION_SHORT_NO_DOT = f'{sys.version_info[0]}{sys.version_info[1]}' _PREFIX = os.path.normpath(sys.prefix) _BASE_PREFIX = os.path.normpath(sys.base_prefix) _EXEC_PREFIX = os.path.normpath(sys.exec_prefix) @@ -189,7 +189,7 @@ def _subst_vars(s, local_vars): try: return s.format(**os.environ) except KeyError: - raise AttributeError('{%s}' % var) from None + raise AttributeError(f'{var}') from None def _extend_dict(target_dict, other_dict): target_keys = target_dict.keys() @@ -212,13 +212,38 @@ def _expand_vars(scheme, vars): return res -def _get_default_scheme(): - if os.name == 'posix': - # the default scheme for posix is posix_prefix - return 'posix_prefix' - return os.name +def _get_preferred_schemes(): + if os.name == 'nt': + return { + 'prefix': 'nt', + 'home': 'posix_home', + 'user': 'nt_user', + } + if sys.platform == 'darwin' and sys._framework: + return { + 'prefix': 'posix_prefix', + 'home': 'posix_home', + 'user': 'osx_framework_user', + } + return { + 'prefix': 'posix_prefix', + 'home': 'posix_home', + 'user': 'posix_user', + } +def get_preferred_scheme(key): + scheme = _get_preferred_schemes()[key] + if scheme not in _INSTALL_SCHEMES: + raise ValueError( + f"{key!r} returned {scheme!r}, which is not a valid scheme " + f"on this platform" + ) + return scheme + + +def get_default_scheme(): + return get_preferred_scheme('prefix') def _parse_makefile(filename, vars=None, keep_unresolved=True): @@ -354,21 +379,20 @@ def get_makefile_filename(): if _PYTHON_BUILD: return os.path.join(_sys_home or _PROJECT_BASE, "Makefile") if hasattr(sys, 'abiflags'): - config_dir_name = 'config-%s%s' % (_PY_VERSION_SHORT, sys.abiflags) + config_dir_name = f'config-{_PY_VERSION_SHORT}{sys.abiflags}' else: config_dir_name = 'config' if hasattr(sys.implementation, '_multiarch'): - config_dir_name += '-%s' % sys.implementation._multiarch + config_dir_name += f'-{sys.implementation._multiarch}' return os.path.join(get_path('stdlib'), config_dir_name, 'Makefile') def _get_sysconfigdata_name(): - return os.environ.get('_PYTHON_SYSCONFIGDATA_NAME', - '_sysconfigdata_{abi}_{platform}_{multiarch}'.format( - abi=sys.abiflags, - platform=sys.platform, - multiarch=getattr(sys.implementation, '_multiarch', ''), - )) + multiarch = getattr(sys.implementation, '_multiarch', '') + return os.environ.get( + '_PYTHON_SYSCONFIGDATA_NAME', + f'_sysconfigdata_{sys.abiflags}_{sys.platform}_{multiarch}', + ) def _generate_posix_vars(): @@ -380,9 +404,9 @@ def _generate_posix_vars(): try: _parse_makefile(makefile, vars) except OSError as e: - msg = "invalid Python installation: unable to open %s" % makefile + msg = f"invalid Python installation: unable to open {makefile}" if hasattr(e, "strerror"): - msg = msg + " (%s)" % e.strerror + msg = f"{msg} ({e.strerror})" raise OSError(msg) # load the installed pyconfig.h: config_h = get_config_h_filename() @@ -390,9 +414,9 @@ def _generate_posix_vars(): with open(config_h, encoding="utf-8") as f: parse_config_h(f, vars) except OSError as e: - msg = "invalid Python installation: unable to open %s" % config_h + msg = f"invalid Python installation: unable to open {config_h}" if hasattr(e, "strerror"): - msg = msg + " (%s)" % e.strerror + msg = f"{msg} ({e.strerror})" raise OSError(msg) # On AIX, there are wrong paths to the linker scripts in the Makefile # -- these paths are relative to the Python source, but when installed @@ -418,7 +442,7 @@ def _generate_posix_vars(): module.build_time_vars = vars sys.modules[name] = module - pybuilddir = 'build/lib.%s-%s' % (get_platform(), _PY_VERSION_SHORT) + pybuilddir = f'build/lib.{get_platform()}-{_PY_VERSION_SHORT}' if hasattr(sys, "gettotalrefcount"): pybuilddir += '-pydebug' os.makedirs(pybuilddir, exist_ok=True) @@ -516,7 +540,7 @@ def get_path_names(): return _SCHEME_KEYS -def get_paths(scheme=_get_default_scheme(), vars=None, expand=True): +def get_paths(scheme=get_default_scheme(), vars=None, expand=True): """Return a mapping containing an install scheme. ``scheme`` is the install scheme name. If not provided, it will @@ -528,7 +552,7 @@ def get_paths(scheme=_get_default_scheme(), vars=None, expand=True): return _INSTALL_SCHEMES[scheme] -def get_path(name, scheme=_get_default_scheme(), vars=None, expand=True): +def get_path(name, scheme=get_default_scheme(), vars=None, expand=True): """Return a path corresponding to the scheme. ``scheme`` is the install scheme name. @@ -682,16 +706,16 @@ def get_platform(): # At least on Linux/Intel, 'machine' is the processor -- # i386, etc. # XXX what about Alpha, SPARC, etc? - return "%s-%s" % (osname, machine) + return f"{osname}-{machine}" elif osname[:5] == "sunos": if release[0] >= "5": # SunOS 5 == Solaris 2 osname = "solaris" - release = "%d.%s" % (int(release[0]) - 3, release[2:]) + release = f"{int(release[0]) - 3}.{release[2:]}" # We can't use "platform.architecture()[0]" because a # bootstrap problem. We use a dict to get an error # if some suspicious happens. bitness = {2147483647:"32bit", 9223372036854775807:"64bit"} - machine += ".%s" % bitness[sys.maxsize] + machine += f".{bitness[sys.maxsize]}" # fall through to standard osname-release-machine representation elif osname[:3] == "aix": from _aix_support import aix_platform @@ -709,7 +733,7 @@ def get_platform(): get_config_vars(), osname, release, machine) - return "%s-%s-%s" % (osname, release, machine) + return f"{osname}-{release}-{machine}" def get_python_version(): @@ -745,8 +769,8 @@ def expand_makefile_vars(s, vars): def _print_dict(title, data): for index, (key, value) in enumerate(sorted(data.items())): if index == 0: - print('%s: ' % (title)) - print('\t%s = "%s"' % (key, value)) + print(f'{title}: ') + print(f'\t{key} = "{value}"') def _main(): @@ -754,9 +778,9 @@ def _main(): if '--generate-posix-vars' in sys.argv: _generate_posix_vars() return - print('Platform: "%s"' % get_platform()) - print('Python version: "%s"' % get_python_version()) - print('Current installation scheme: "%s"' % _get_default_scheme()) + print(f'Platform: "{get_platform()}"') + print(f'Python version: "{get_python_version()}"') + print(f'Current installation scheme: "{get_default_scheme()}"') print() _print_dict('Paths', get_paths()) print() diff --git a/Lib/test/test_sysconfig.py b/Lib/test/test_sysconfig.py index e279957e26c..b8b9add36d7 100644 --- a/Lib/test/test_sysconfig.py +++ b/Lib/test/test_sysconfig.py @@ -14,8 +14,8 @@ from test.support.warnings_helper import check_warnings import sysconfig from sysconfig import (get_paths, get_platform, get_config_vars, get_path, get_path_names, _INSTALL_SCHEMES, - _get_default_scheme, _expand_vars, - get_scheme_names, get_config_var, _main) + get_default_scheme, get_scheme_names, get_config_var, + _expand_vars, _get_preferred_schemes, _main) import _osx_support @@ -94,17 +94,46 @@ class TestSysConfig(unittest.TestCase): def test_get_paths(self): scheme = get_paths() - default_scheme = _get_default_scheme() + default_scheme = get_default_scheme() wanted = _expand_vars(default_scheme, None) wanted = sorted(wanted.items()) scheme = sorted(scheme.items()) self.assertEqual(scheme, wanted) def test_get_path(self): - # XXX make real tests here + config_vars = get_config_vars() for scheme in _INSTALL_SCHEMES: for name in _INSTALL_SCHEMES[scheme]: - res = get_path(name, scheme) + expected = _INSTALL_SCHEMES[scheme][name].format(**config_vars) + self.assertEqual( + os.path.normpath(get_path(name, scheme)), + os.path.normpath(expected), + ) + + def test_get_default_scheme(self): + self.assertIn(get_default_scheme(), _INSTALL_SCHEMES) + + def test_get_preferred_schemes(self): + expected_schemes = {'prefix', 'home', 'user'} + + # Windows. + os.name = 'nt' + schemes = _get_preferred_schemes() + self.assertIsInstance(schemes, dict) + self.assertEqual(set(schemes), expected_schemes) + + # Mac and Linux, shared library build. + os.name = 'posix' + schemes = _get_preferred_schemes() + self.assertIsInstance(schemes, dict) + self.assertEqual(set(schemes), expected_schemes) + + # Mac, framework build. + os.name = 'posix' + sys.platform = 'darwin' + sys._framework = True + self.assertIsInstance(schemes, dict) + self.assertEqual(set(schemes), expected_schemes) def test_get_config_vars(self): cvars = get_config_vars() diff --git a/Misc/NEWS.d/next/Library/2021-02-25-14-43-59.bpo-43312.6dg9_2.rst b/Misc/NEWS.d/next/Library/2021-02-25-14-43-59.bpo-43312.6dg9_2.rst new file mode 100644 index 00000000000..b8def9ceee6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-02-25-14-43-59.bpo-43312.6dg9_2.rst @@ -0,0 +1,3 @@ +New functions :func:`sysconfig.get_preferred_scheme` and +:func:`sysconfig.get_default_scheme` are added to query a platform for its +preferred "user", "home", and "prefix" (default) scheme names.