From 2bb92862e014bf0d8eb44576860b49c16958043d Mon Sep 17 00:00:00 2001 From: Zhaoqi Xu Date: Mon, 26 Oct 2026 02:44:13 -0400 Subject: [PATCH 1/2] fix(nodeenv): read config values without interpolation --- CHANGES | 4 ++++ nodeenv.py | 3 ++- tests/nodeenv_test.py | 13 +++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 61f86f8..048526b 100644 --- a/CHANGES +++ b/CHANGES @@ -10,6 +10,10 @@ Version [unreleased] recreate them with the same options to get the fix. With `-p`, remove the old nodeenv part from the virtualenv's `activate.fish` first `#399 `_ +- A ``%`` in a ``[nodeenv]`` config value, such as a percent-encoded + mirror login, no longer crashes nodeenv; values are read as written, + so a ``%%`` that worked around the crash is now read as ``%%`` + `#440 `_ - `nodeenv -p` no longer puts the python venv's `bin` directory on `PATH` a second time: the venv's own `activate` and `activate.fish` put it there already. Envs without those scripts, such as conda envs, and the git-bash diff --git a/nodeenv.py b/nodeenv.py index 5ab0e3d..e9a83c5 100644 --- a/nodeenv.py +++ b/nodeenv.py @@ -96,7 +96,8 @@ def _load(cls, configfiles, verbose=False): if not os.path.exists(configfile): continue - ini_file = ConfigParser() + # No interpolation: a percent-encoded mirror URL is valid, #440 + ini_file = ConfigParser(interpolation=None) ini_file.read(configfile) section = "nodeenv" if not ini_file.has_section(section): diff --git a/tests/nodeenv_test.py b/tests/nodeenv_test.py index 83c5422..83087b8 100644 --- a/tests/nodeenv_test.py +++ b/tests/nodeenv_test.py @@ -614,6 +614,19 @@ def test_config_strips_quotes(tmpdir): nodeenv.Config.mirror = nodeenv.Config._default['mirror'] +def test_config_keeps_percent_signs(tmpdir): + """A % in a value is read as is, not as interpolation, see #440""" + rc = tmpdir.join('nodeenvrc') + rc.write('[nodeenv]\n' + 'mirror = https://user:p%40ss@mirror.example/nodejs\n') + try: + nodeenv.Config._load([str(rc)]) + assert nodeenv.Config.mirror == \ + 'https://user:p%40ss@mirror.example/nodejs' + finally: + nodeenv.Config.mirror = nodeenv.Config._default['mirror'] + + @pytest.fixture def auth_mirror(monkeypatch): """ From f7f6a479b26d23436c646079b29f6e4c4ae41ecb Mon Sep 17 00:00:00 2001 From: Zhaoqi Xu Date: Fri, 30 Oct 2026 06:06:32 +0800 Subject: [PATCH 2/2] fix(nodeenv): read the raw config value only when interpolation fails --- CHANGES | 5 +++-- nodeenv.py | 11 +++++++---- tests/nodeenv_test.py | 16 ++++++++++------ 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/CHANGES b/CHANGES index 048526b..9275241 100644 --- a/CHANGES +++ b/CHANGES @@ -11,8 +11,9 @@ Version [unreleased] old nodeenv part from the virtualenv's `activate.fish` first `#399 `_ - A ``%`` in a ``[nodeenv]`` config value, such as a percent-encoded - mirror login, no longer crashes nodeenv; values are read as written, - so a ``%%`` that worked around the crash is now read as ``%%`` + mirror login, no longer crashes nodeenv: a value that can't be + interpolated is read as written. ``%%`` still gives ``%``, which + ``setup.cfg`` needs, as setuptools reads that file too `#440 `_ - `nodeenv -p` no longer puts the python venv's `bin` directory on `PATH` a second time: the venv's own `activate` and `activate.fish` put it there diff --git a/nodeenv.py b/nodeenv.py index e9a83c5..1d14b86 100644 --- a/nodeenv.py +++ b/nodeenv.py @@ -33,7 +33,7 @@ import sysconfig import glob -from configparser import ConfigParser +from configparser import ConfigParser, InterpolationError from http.client import IncompleteRead import urllib.request as urllib2 from urllib.parse import unquote, urlsplit, urlunsplit @@ -96,8 +96,7 @@ def _load(cls, configfiles, verbose=False): if not os.path.exists(configfile): continue - # No interpolation: a percent-encoded mirror URL is valid, #440 - ini_file = ConfigParser(interpolation=None) + ini_file = ConfigParser() ini_file.read(configfile) section = "nodeenv" if not ini_file.has_section(section): @@ -111,7 +110,11 @@ def _load(cls, configfiles, verbose=False): if isinstance(val, bool): val = ini_file.getboolean(section, attr) else: - val = ini_file.get(section, attr) + try: + val = ini_file.get(section, attr) + except InterpolationError: + # A percent-encoded mirror login, #440 + val = ini_file.get(section, attr, raw=True) # ConfigParser keeps the quotes the README shows, #321 if len(val) > 1 and val[0] == val[-1] and val[0] in '\'"': val = val[1:-1] diff --git a/tests/nodeenv_test.py b/tests/nodeenv_test.py index 83087b8..a73171f 100644 --- a/tests/nodeenv_test.py +++ b/tests/nodeenv_test.py @@ -614,15 +614,19 @@ def test_config_strips_quotes(tmpdir): nodeenv.Config.mirror = nodeenv.Config._default['mirror'] -def test_config_keeps_percent_signs(tmpdir): - """A % in a value is read as is, not as interpolation, see #440""" +@pytest.mark.parametrize('mirror, expected', [ + ('https://user:p%40ss@mirror.example/nodejs', + 'https://user:p%40ss@mirror.example/nodejs'), + ('https://user:p%%40ss@mirror.example/nodejs', + 'https://user:p%40ss@mirror.example/nodejs'), +]) +def test_config_keeps_percent_signs(tmpdir, mirror, expected): + """A % that ConfigParser can't interpolate is read as is, see #440""" rc = tmpdir.join('nodeenvrc') - rc.write('[nodeenv]\n' - 'mirror = https://user:p%40ss@mirror.example/nodejs\n') + rc.write('[nodeenv]\nmirror = %s\n' % mirror) try: nodeenv.Config._load([str(rc)]) - assert nodeenv.Config.mirror == \ - 'https://user:p%40ss@mirror.example/nodejs' + assert nodeenv.Config.mirror == expected finally: nodeenv.Config.mirror = nodeenv.Config._default['mirror']