Skip to content

Commit 50e42b9

Browse files
[3.12] GH-126789: fix some sysconfig data on late site initializations
Co-authored-by: Filipe Laíns 🇵🇸 <[email protected]>
1 parent cdc1dff commit 50e42b9

File tree

4 files changed

+163
-4
lines changed

4 files changed

+163
-4
lines changed

Lib/sysconfig.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,7 @@ def joinuser(*args):
169169
_PY_VERSION = sys.version.split()[0]
170170
_PY_VERSION_SHORT = f'{sys.version_info[0]}.{sys.version_info[1]}'
171171
_PY_VERSION_SHORT_NO_DOT = f'{sys.version_info[0]}{sys.version_info[1]}'
172-
_PREFIX = os.path.normpath(sys.prefix)
173172
_BASE_PREFIX = os.path.normpath(sys.base_prefix)
174-
_EXEC_PREFIX = os.path.normpath(sys.exec_prefix)
175173
_BASE_EXEC_PREFIX = os.path.normpath(sys.base_exec_prefix)
176174
# Mutex guarding initialization of _CONFIG_VARS.
177175
_CONFIG_VARS_LOCK = threading.RLock()
@@ -642,8 +640,10 @@ def _init_config_vars():
642640
# Normalized versions of prefix and exec_prefix are handy to have;
643641
# in fact, these are the standard versions used most places in the
644642
# Distutils.
645-
_CONFIG_VARS['prefix'] = _PREFIX
646-
_CONFIG_VARS['exec_prefix'] = _EXEC_PREFIX
643+
_PREFIX = os.path.normpath(sys.prefix)
644+
_EXEC_PREFIX = os.path.normpath(sys.exec_prefix)
645+
_CONFIG_VARS['prefix'] = _PREFIX # FIXME: This gets overwriten by _init_posix.
646+
_CONFIG_VARS['exec_prefix'] = _EXEC_PREFIX # FIXME: This gets overwriten by _init_posix.
647647
_CONFIG_VARS['py_version'] = _PY_VERSION
648648
_CONFIG_VARS['py_version_short'] = _PY_VERSION_SHORT
649649
_CONFIG_VARS['py_version_nodot'] = _PY_VERSION_SHORT_NO_DOT
@@ -711,6 +711,7 @@ def get_config_vars(*args):
711711
With arguments, return a list of values that result from looking up
712712
each argument in the configuration variable dictionary.
713713
"""
714+
global _CONFIG_VARS_INITIALIZED
714715

715716
# Avoid claiming the lock once initialization is complete.
716717
if not _CONFIG_VARS_INITIALIZED:
@@ -721,6 +722,15 @@ def get_config_vars(*args):
721722
# don't re-enter init_config_vars().
722723
if _CONFIG_VARS is None:
723724
_init_config_vars()
725+
else:
726+
# If the site module initialization happened after _CONFIG_VARS was
727+
# initialized, a virtual environment might have been activated, resulting in
728+
# variables like sys.prefix changing their value, so we need to re-init the
729+
# config vars (see GH-126789).
730+
if _CONFIG_VARS['base'] != os.path.normpath(sys.prefix):
731+
with _CONFIG_VARS_LOCK:
732+
_CONFIG_VARS_INITIALIZED = False
733+
_init_config_vars()
724734

725735
if args:
726736
vals = []

Lib/test/support/venv.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import contextlib
2+
import logging
3+
import os
4+
import subprocess
5+
import shlex
6+
import sys
7+
import sysconfig
8+
import tempfile
9+
import venv
10+
11+
12+
class VirtualEnvironment:
13+
def __init__(self, prefix, **venv_create_args):
14+
self._logger = logging.getLogger(self.__class__.__name__)
15+
venv.create(prefix, **venv_create_args)
16+
self._prefix = prefix
17+
self._paths = sysconfig.get_paths(
18+
scheme='venv',
19+
vars={'base': self.prefix},
20+
expand=True,
21+
)
22+
23+
@classmethod
24+
@contextlib.contextmanager
25+
def from_tmpdir(cls, *, prefix=None, dir=None, **venv_create_args):
26+
delete = not bool(os.environ.get('PYTHON_TESTS_KEEP_VENV'))
27+
with tempfile.TemporaryDirectory(prefix=prefix, dir=dir, delete=delete) as tmpdir:
28+
yield cls(tmpdir, **venv_create_args)
29+
30+
@property
31+
def prefix(self):
32+
return self._prefix
33+
34+
@property
35+
def paths(self):
36+
return self._paths
37+
38+
@property
39+
def interpreter(self):
40+
return os.path.join(self.paths['scripts'], os.path.basename(sys.executable))
41+
42+
def _format_output(self, name, data, indent='\t'):
43+
if not data:
44+
return indent + f'{name}: (none)'
45+
if len(data.splitlines()) == 1:
46+
return indent + f'{name}: {data}'
47+
else:
48+
prefixed_lines = '\n'.join(indent + '> ' + line for line in data.splitlines())
49+
return indent + f'{name}:\n' + prefixed_lines
50+
51+
def run(self, *args, **subprocess_args):
52+
if subprocess_args.get('shell'):
53+
raise ValueError('Running the subprocess in shell mode is not supported.')
54+
default_args = {
55+
'capture_output': True,
56+
'check': True,
57+
}
58+
try:
59+
result = subprocess.run([self.interpreter, *args], **default_args | subprocess_args)
60+
except subprocess.CalledProcessError as e:
61+
if e.returncode != 0:
62+
self._logger.error(
63+
f'Interpreter returned non-zero exit status {e.returncode}.\n'
64+
+ self._format_output('COMMAND', shlex.join(e.cmd)) + '\n'
65+
+ self._format_output('STDOUT', e.stdout.decode()) + '\n'
66+
+ self._format_output('STDERR', e.stderr.decode()) + '\n'
67+
)
68+
raise
69+
else:
70+
return result

Lib/test/test_sysconfig.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import os
44
import subprocess
55
import shutil
6+
import json
7+
import textwrap
68
from copy import copy
79

810
from test.support import (
@@ -11,6 +13,7 @@
1113
from test.support.import_helper import import_module
1214
from test.support.os_helper import (TESTFN, unlink, skip_unless_symlink,
1315
change_cwd)
16+
from test.support.venv import VirtualEnvironment
1417

1518
import sysconfig
1619
from sysconfig import (get_paths, get_platform, get_config_vars,
@@ -90,6 +93,12 @@ def _cleanup_testfn(self):
9093
elif os.path.isdir(path):
9194
shutil.rmtree(path)
9295

96+
def venv(self, **venv_create_args):
97+
return VirtualEnvironment.from_tmpdir(
98+
prefix=f'{self.id()}-venv-',
99+
**venv_create_args,
100+
)
101+
93102
def test_get_path_names(self):
94103
self.assertEqual(get_path_names(), sysconfig._SCHEME_KEYS)
95104

@@ -511,6 +520,72 @@ def test_osx_ext_suffix(self):
511520
suffix = sysconfig.get_config_var('EXT_SUFFIX')
512521
self.assertTrue(suffix.endswith('-darwin.so'), suffix)
513522

523+
@unittest.skipIf(sys.platform == 'wasi', 'venv is unsupported on WASI')
524+
def test_config_vars_depend_on_site_initialization(self):
525+
script = textwrap.dedent("""
526+
import sysconfig
527+
528+
config_vars = sysconfig.get_config_vars()
529+
530+
import json
531+
print(json.dumps(config_vars, indent=2))
532+
""")
533+
534+
with self.venv() as venv:
535+
site_config_vars = json.loads(venv.run('-c', script).stdout)
536+
no_site_config_vars = json.loads(venv.run('-S', '-c', script).stdout)
537+
538+
self.assertNotEqual(site_config_vars, no_site_config_vars)
539+
# With the site initialization, the virtual environment should be enabled.
540+
self.assertEqual(site_config_vars['base'], venv.prefix)
541+
self.assertEqual(site_config_vars['platbase'], venv.prefix)
542+
#self.assertEqual(site_config_vars['prefix'], venv.prefix) # # FIXME: prefix gets overwriten by _init_posix
543+
# Without the site initialization, the virtual environment should be disabled.
544+
self.assertEqual(no_site_config_vars['base'], site_config_vars['installed_base'])
545+
self.assertEqual(no_site_config_vars['platbase'], site_config_vars['installed_platbase'])
546+
547+
@unittest.skipIf(sys.platform == 'wasi', 'venv is unsupported on WASI')
548+
def test_config_vars_recalculation_after_site_initialization(self):
549+
script = textwrap.dedent("""
550+
import sysconfig
551+
552+
before = sysconfig.get_config_vars()
553+
554+
import site
555+
site.main()
556+
557+
after = sysconfig.get_config_vars()
558+
559+
import json
560+
print(json.dumps({'before': before, 'after': after}, indent=2))
561+
""")
562+
563+
with self.venv() as venv:
564+
config_vars = json.loads(venv.run('-S', '-c', script).stdout)
565+
566+
self.assertNotEqual(config_vars['before'], config_vars['after'])
567+
self.assertEqual(config_vars['after']['base'], venv.prefix)
568+
#self.assertEqual(config_vars['after']['prefix'], venv.prefix) # FIXME: prefix gets overwriten by _init_posix
569+
#self.assertEqual(config_vars['after']['exec_prefix'], venv.prefix) # FIXME: exec_prefix gets overwriten by _init_posix
570+
571+
@unittest.skipIf(sys.platform == 'wasi', 'venv is unsupported on WASI')
572+
def test_paths_depend_on_site_initialization(self):
573+
script = textwrap.dedent("""
574+
import sysconfig
575+
576+
paths = sysconfig.get_paths()
577+
578+
import json
579+
print(json.dumps(paths, indent=2))
580+
""")
581+
582+
with self.venv() as venv:
583+
site_paths = json.loads(venv.run('-c', script).stdout)
584+
no_site_paths = json.loads(venv.run('-S', '-c', script).stdout)
585+
586+
self.assertNotEqual(site_paths, no_site_paths)
587+
588+
514589
class MakefileTests(unittest.TestCase):
515590

516591
@unittest.skipIf(sys.platform.startswith('win'),
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fixed the values of :py:func:`sysconfig.get_config_vars`,
2+
:py:func:`sysconfig.get_paths`, and their siblings when the :py:mod:`site`
3+
initialization happens after :py:mod:`sysconfig` has built a cache for
4+
:py:func:`sysconfig.get_config_vars`.

0 commit comments

Comments
 (0)