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 acbd5c9

Browse filesBrowse files
authored
GH-126789: fix some sysconfig data on late site initializations
1 parent ed81971 commit acbd5c9
Copy full SHA for acbd5c9

File tree

4 files changed

+163
-4
lines changed
Filter options

4 files changed

+163
-4
lines changed

‎Lib/sysconfig/__init__.py

Copy file name to clipboardExpand all lines: Lib/sysconfig/__init__.py
+14-4Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,7 @@ def joinuser(*args):
173173
_PY_VERSION = sys.version.split()[0]
174174
_PY_VERSION_SHORT = f'{sys.version_info[0]}.{sys.version_info[1]}'
175175
_PY_VERSION_SHORT_NO_DOT = f'{sys.version_info[0]}{sys.version_info[1]}'
176-
_PREFIX = os.path.normpath(sys.prefix)
177176
_BASE_PREFIX = os.path.normpath(sys.base_prefix)
178-
_EXEC_PREFIX = os.path.normpath(sys.exec_prefix)
179177
_BASE_EXEC_PREFIX = os.path.normpath(sys.base_exec_prefix)
180178
# Mutex guarding initialization of _CONFIG_VARS.
181179
_CONFIG_VARS_LOCK = threading.RLock()
@@ -466,8 +464,10 @@ def _init_config_vars():
466464
# Normalized versions of prefix and exec_prefix are handy to have;
467465
# in fact, these are the standard versions used most places in the
468466
# Distutils.
469-
_CONFIG_VARS['prefix'] = _PREFIX
470-
_CONFIG_VARS['exec_prefix'] = _EXEC_PREFIX
467+
_PREFIX = os.path.normpath(sys.prefix)
468+
_EXEC_PREFIX = os.path.normpath(sys.exec_prefix)
469+
_CONFIG_VARS['prefix'] = _PREFIX # FIXME: This gets overwriten by _init_posix.
470+
_CONFIG_VARS['exec_prefix'] = _EXEC_PREFIX # FIXME: This gets overwriten by _init_posix.
471471
_CONFIG_VARS['py_version'] = _PY_VERSION
472472
_CONFIG_VARS['py_version_short'] = _PY_VERSION_SHORT
473473
_CONFIG_VARS['py_version_nodot'] = _PY_VERSION_SHORT_NO_DOT
@@ -540,6 +540,7 @@ def get_config_vars(*args):
540540
With arguments, return a list of values that result from looking up
541541
each argument in the configuration variable dictionary.
542542
"""
543+
global _CONFIG_VARS_INITIALIZED
543544

544545
# Avoid claiming the lock once initialization is complete.
545546
if not _CONFIG_VARS_INITIALIZED:
@@ -550,6 +551,15 @@ def get_config_vars(*args):
550551
# don't re-enter init_config_vars().
551552
if _CONFIG_VARS is None:
552553
_init_config_vars()
554+
else:
555+
# If the site module initialization happened after _CONFIG_VARS was
556+
# initialized, a virtual environment might have been activated, resulting in
557+
# variables like sys.prefix changing their value, so we need to re-init the
558+
# config vars (see GH-126789).
559+
if _CONFIG_VARS['base'] != os.path.normpath(sys.prefix):
560+
with _CONFIG_VARS_LOCK:
561+
_CONFIG_VARS_INITIALIZED = False
562+
_init_config_vars()
553563

554564
if args:
555565
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
@@ -5,6 +5,8 @@
55
import os
66
import subprocess
77
import shutil
8+
import json
9+
import textwrap
810
from copy import copy
911

1012
from test.support import (
@@ -17,6 +19,7 @@
1719
from test.support.import_helper import import_module
1820
from test.support.os_helper import (TESTFN, unlink, skip_unless_symlink,
1921
change_cwd)
22+
from test.support.venv import VirtualEnvironment
2023

2124
import sysconfig
2225
from sysconfig import (get_paths, get_platform, get_config_vars,
@@ -101,6 +104,12 @@ def _cleanup_testfn(self):
101104
elif os.path.isdir(path):
102105
shutil.rmtree(path)
103106

107+
def venv(self, **venv_create_args):
108+
return VirtualEnvironment.from_tmpdir(
109+
prefix=f'{self.id()}-venv-',
110+
**venv_create_args,
111+
)
112+
104113
def test_get_path_names(self):
105114
self.assertEqual(get_path_names(), sysconfig._SCHEME_KEYS)
106115

@@ -582,6 +591,72 @@ def test_osx_ext_suffix(self):
582591
suffix = sysconfig.get_config_var('EXT_SUFFIX')
583592
self.assertTrue(suffix.endswith('-darwin.so'), suffix)
584593

594+
@unittest.skipIf(sys.platform == 'wasi', 'venv is unsupported on WASI')
595+
def test_config_vars_depend_on_site_initialization(self):
596+
script = textwrap.dedent("""
597+
import sysconfig
598+
599+
config_vars = sysconfig.get_config_vars()
600+
601+
import json
602+
print(json.dumps(config_vars, indent=2))
603+
""")
604+
605+
with self.venv() as venv:
606+
site_config_vars = json.loads(venv.run('-c', script).stdout)
607+
no_site_config_vars = json.loads(venv.run('-S', '-c', script).stdout)
608+
609+
self.assertNotEqual(site_config_vars, no_site_config_vars)
610+
# With the site initialization, the virtual environment should be enabled.
611+
self.assertEqual(site_config_vars['base'], venv.prefix)
612+
self.assertEqual(site_config_vars['platbase'], venv.prefix)
613+
#self.assertEqual(site_config_vars['prefix'], venv.prefix) # # FIXME: prefix gets overwriten by _init_posix
614+
# Without the site initialization, the virtual environment should be disabled.
615+
self.assertEqual(no_site_config_vars['base'], site_config_vars['installed_base'])
616+
self.assertEqual(no_site_config_vars['platbase'], site_config_vars['installed_platbase'])
617+
618+
@unittest.skipIf(sys.platform == 'wasi', 'venv is unsupported on WASI')
619+
def test_config_vars_recalculation_after_site_initialization(self):
620+
script = textwrap.dedent("""
621+
import sysconfig
622+
623+
before = sysconfig.get_config_vars()
624+
625+
import site
626+
site.main()
627+
628+
after = sysconfig.get_config_vars()
629+
630+
import json
631+
print(json.dumps({'before': before, 'after': after}, indent=2))
632+
""")
633+
634+
with self.venv() as venv:
635+
config_vars = json.loads(venv.run('-S', '-c', script).stdout)
636+
637+
self.assertNotEqual(config_vars['before'], config_vars['after'])
638+
self.assertEqual(config_vars['after']['base'], venv.prefix)
639+
#self.assertEqual(config_vars['after']['prefix'], venv.prefix) # FIXME: prefix gets overwriten by _init_posix
640+
#self.assertEqual(config_vars['after']['exec_prefix'], venv.prefix) # FIXME: exec_prefix gets overwriten by _init_posix
641+
642+
@unittest.skipIf(sys.platform == 'wasi', 'venv is unsupported on WASI')
643+
def test_paths_depend_on_site_initialization(self):
644+
script = textwrap.dedent("""
645+
import sysconfig
646+
647+
paths = sysconfig.get_paths()
648+
649+
import json
650+
print(json.dumps(paths, indent=2))
651+
""")
652+
653+
with self.venv() as venv:
654+
site_paths = json.loads(venv.run('-c', script).stdout)
655+
no_site_paths = json.loads(venv.run('-S', '-c', script).stdout)
656+
657+
self.assertNotEqual(site_paths, no_site_paths)
658+
659+
585660
class MakefileTests(unittest.TestCase):
586661

587662
@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.