Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 50e42b9

Browse filesBrowse files
[3.12] GH-126789: fix some sysconfig data on late site initializations
Co-authored-by: Filipe Laíns 🇵🇸 <lains@riseup.net>
1 parent cdc1dff commit 50e42b9
Copy full SHA for 50e42b9

File tree

4 files changed

+163
-4
lines changed
Filter options

4 files changed

+163
-4
lines changed

‎Lib/sysconfig.py

Copy file name to clipboardExpand all lines: Lib/sysconfig.py
+14-4Lines 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

Copy file name to clipboard
+70Lines 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

Copy file name to clipboardExpand all lines: Lib/test/test_sysconfig.py
+75Lines 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'),
+4Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.