From a8d19f6229f4a3ee7ea7f5c6f9333086aac89470 Mon Sep 17 00:00:00 2001 From: Huanyi Xie <74809554+xiehuanyi@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:26:44 +0300 Subject: [PATCH] Preserve prototype-named namespace attributes --- lib/argparse.js | 15 +++++++++++++-- test/test_port.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/lib/argparse.js b/lib/argparse.js index 22a4fb6..07428d5 100644 --- a/lib/argparse.js +++ b/lib/argparse.js @@ -109,7 +109,13 @@ function getattr (object, name, value) { } function setattr (object, name, value) { - object[name] = value + if (name === '__proto__') { + Object.defineProperty(object, name, { + value, enumerable: true, configurable: true, writable: true + }) + } else { + object[name] = value + } } function setdefault (object, name, value) { @@ -2053,7 +2059,12 @@ const Namespace = _callable(class Namespace extends _AttributeHolder() { constructor (options = {}) { super() - Object.assign(this, options) + const source = Object(options) + for (const key of Reflect.ownKeys(source)) { + if (Object.prototype.propertyIsEnumerable.call(source, key)) { + setattr(this, key, source[key]) + } + } } }) diff --git a/test/test_port.js b/test/test_port.js index af1925b..dd5ef59 100644 --- a/test/test_port.js +++ b/test/test_port.js @@ -81,3 +81,49 @@ for (const type of ['int', 'float']) { } }) } + +// A Python destination named __proto__ is an ordinary namespace attribute. +// The inherited JavaScript setter must not consume it or change the prototype. +describe('prototype-named destinations', () => { + it('stores explicit values and defaults as own attributes', () => { + const parser = new argparse.ArgumentParser({ add_help: false }) + const defaultValue = { value: 'default' } + parser.add_argument('--value', { dest: '__proto__', default: defaultValue }) + const defaults = parser.parse_args([]) + const explicit = parser.parse_args(['--value', 'parsed']) + assert.strictEqual(Object.getOwnPropertyDescriptor(defaults, '__proto__').value, defaultValue) + assert.strictEqual(Object.getOwnPropertyDescriptor(explicit, '__proto__').value, 'parsed') + assert.ok(Object.prototype.hasOwnProperty.call(explicit, '__proto__')) + assert.strictEqual(Object.getPrototypeOf(defaults), argparse.Namespace.prototype) + assert.strictEqual(Object.getPrototypeOf(explicit), argparse.Namespace.prototype) + }) + + it('supports append and count actions without changing a supplied namespace', () => { + for (const [action, expected] of [['append', ['first', 'second']], ['count', 2]]) { + const parser = new argparse.ArgumentParser({ add_help: false }) + parser.add_argument('--value', { dest: '__proto__', action }) + const namespace = {} + const args = action === 'append' + ? ['--value', 'first', '--value', 'second'] + : ['--value', '--value'] + assert.strictEqual(parser.parse_args(args, namespace), namespace) + assert.deepStrictEqual(Object.getOwnPropertyDescriptor(namespace, '__proto__').value, expected) + assert.strictEqual(Object.getPrototypeOf(namespace), Object.prototype) + } + }) + + it('keeps null and boxed primitive namespace source behavior', () => { + assert.deepStrictEqual(Object.keys(new argparse.Namespace(null)), []) + assert.strictEqual(new argparse.Namespace('x')[0], 'x') + }) + + it('copies namespace values including enumerable symbols safely', () => { + const symbol = Symbol('value') + const options = { ['__proto__']: 'initial', [symbol]: 42 } + const namespace = new argparse.Namespace(options) + assert.strictEqual(Object.getOwnPropertyDescriptor(namespace, '__proto__').value, 'initial') + assert.strictEqual(namespace[symbol], 42) + assert.strictEqual(Object.getPrototypeOf(namespace), argparse.Namespace.prototype) + assert.deepStrictEqual(Object.keys(namespace), ['__proto__']) + }) +})