Skip to content

Commit dd8a93e

Browse files
authored
bpo-23427: Add sys.orig_argv attribute (GH-20729)
Add sys.orig_argv attribute: the list of the original command line arguments passed to the Python executable. Rename also PyConfig._orig_argv to PyConfig.orig_argv and document it.
1 parent 2fb5f03 commit dd8a93e

File tree

9 files changed

+104
-35
lines changed

9 files changed

+104
-35
lines changed

Doc/c-api/init_config.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,8 @@ PyConfig
424424
:c:member:`~PyConfig.argv` is empty, an empty string is added to ensure
425425
that :data:`sys.argv` always exists and is never empty.
426426
427+
See also the :c:member:`~PyConfig.orig_argv` member.
428+
427429
.. c:member:: wchar_t* base_exec_prefix
428430
429431
:data:`sys.base_exec_prefix`.
@@ -586,6 +588,23 @@ PyConfig
586588
* 1: Remove assertions, set ``__debug__`` to ``False``
587589
* 2: Strip docstrings
588590
591+
.. c:member:: PyWideStringList orig_argv
592+
593+
The list of the original command line arguments passed to the Python
594+
executable.
595+
596+
If :c:member:`~PyConfig.orig_argv` list is empty and
597+
:c:member:`~PyConfig.argv` is not a list only containing an empty
598+
string, :c:func:`PyConfig_Read()` copies :c:member:`~PyConfig.argv` into
599+
:c:member:`~PyConfig.orig_argv` before modifying
600+
:c:member:`~PyConfig.argv` (if :c:member:`~PyConfig.parse_argv` is
601+
non-zero).
602+
603+
See also the :c:member:`~PyConfig.argv` member and the
604+
:c:func:`Py_GetArgcArgv` function.
605+
606+
.. versionadded:: 3.10
607+
589608
.. c:member:: int parse_argv
590609
591610
If non-zero, parse :c:member:`~PyConfig.argv` the same way the regular
@@ -982,6 +1001,8 @@ Py_GetArgcArgv()
9821001
9831002
Get the original command line arguments, before Python modified them.
9841003
1004+
See also :c:member:`PyConfig.orig_argv` member.
1005+
9851006
9861007
Multi-Phase Initialization Private Provisional API
9871008
--------------------------------------------------

Doc/library/sys.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ always available.
6666
To loop over the standard input, or the list of files given on the
6767
command line, see the :mod:`fileinput` module.
6868

69+
See also :data:`sys.orig_argv`.
70+
6971
.. note::
7072
On Unix, command line arguments are passed by bytes from OS. Python decodes
7173
them with filesystem encoding and "surrogateescape" error handler.
@@ -1037,6 +1039,16 @@ always available.
10371039
deleting essential items from the dictionary may cause Python to fail.
10381040

10391041

1042+
.. data:: orig_argv
1043+
1044+
The list of the original command line arguments passed to the Python
1045+
executable.
1046+
1047+
See also :data:`sys.argv`.
1048+
1049+
.. versionadded:: 3.10
1050+
1051+
10401052
.. data:: path
10411053

10421054
.. index:: triple: module; search; path

Doc/whatsnew/3.10.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ Added the *root_dir* and *dir_fd* parameters in :func:`~glob.glob` and
110110
:func:`~glob.iglob` which allow to specify the root directory for searching.
111111
(Contributed by Serhiy Storchaka in :issue:`38144`.)
112112

113+
sys
114+
---
115+
116+
Add :data:`sys.orig_argv` attribute: the list of the original command line
117+
arguments passed to the Python executable.
118+
(Contributed by Victor Stinner in :issue:`23427`.)
119+
113120

114121
Optimizations
115122
=============
@@ -150,10 +157,14 @@ C API Changes
150157
New Features
151158
------------
152159

153-
The result of :c:func:`PyNumber_Index` now always has exact type :class:`int`.
160+
* The result of :c:func:`PyNumber_Index` now always has exact type :class:`int`.
154161
Previously, the result could have been an instance of a subclass of ``int``.
155162
(Contributed by Serhiy Storchaka in :issue:`40792`.)
156163

164+
* Add a new :c:member:`~PyConfig.orig_argv` member to the :c:type:`PyConfig`
165+
structure: the list of the original command line arguments passed to the
166+
Python executable.
167+
(Contributed by Victor Stinner in :issue:`23427`.)
157168

158169
Porting to Python 3.10
159170
----------------------

Include/cpython/initconfig.h

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -408,13 +408,15 @@ typedef struct {
408408
Default: 0. */
409409
int _isolated_interpreter;
410410

411-
/* Original command line arguments. If _orig_argv is empty and _argv is
412-
not equal to [''], PyConfig_Read() copies the configuration 'argv' list
413-
into '_orig_argv' list before modifying 'argv' list (if parse_argv
414-
is non-zero).
411+
/* The list of the original command line arguments passed to the Python
412+
executable.
413+
414+
If 'orig_argv' list is empty and 'argv' is not a list only containing an
415+
empty string, PyConfig_Read() copies 'argv' into 'orig_argv' before
416+
modifying 'argv' (if 'parse_argv is non-zero).
415417
416418
_PyConfig_Write() initializes Py_GetArgcArgv() to this list. */
417-
PyWideStringList _orig_argv;
419+
PyWideStringList orig_argv;
418420
} PyConfig;
419421

420422
PyAPI_FUNC(void) PyConfig_InitPythonConfig(PyConfig *config);
@@ -445,7 +447,7 @@ PyAPI_FUNC(PyStatus) PyConfig_SetWideStringList(PyConfig *config,
445447

446448
/* Get the original command line arguments, before Python modified them.
447449
448-
See also PyConfig._orig_argv. */
450+
See also PyConfig.orig_argv. */
449451
PyAPI_FUNC(void) Py_GetArgcArgv(int *argc, wchar_t ***argv);
450452

451453
#endif /* !Py_LIMITED_API */

Lib/test/test_embed.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ class InitConfigTests(EmbeddingTestsMixin, unittest.TestCase):
365365
'program_name': GET_DEFAULT_CONFIG,
366366
'parse_argv': 0,
367367
'argv': [""],
368-
'_orig_argv': [],
368+
'orig_argv': [],
369369

370370
'xoptions': [],
371371
'warnoptions': [],
@@ -739,11 +739,11 @@ def test_init_from_config(self):
739739
'pycache_prefix': 'conf_pycache_prefix',
740740
'program_name': './conf_program_name',
741741
'argv': ['-c', 'arg2'],
742-
'_orig_argv': ['python3',
743-
'-W', 'cmdline_warnoption',
744-
'-X', 'cmdline_xoption',
745-
'-c', 'pass',
746-
'arg2'],
742+
'orig_argv': ['python3',
743+
'-W', 'cmdline_warnoption',
744+
'-X', 'cmdline_xoption',
745+
'-c', 'pass',
746+
'arg2'],
747747
'parse_argv': 1,
748748
'xoptions': [
749749
'config_xoption1=3',
@@ -874,7 +874,7 @@ def test_preinit_parse_argv(self):
874874
}
875875
config = {
876876
'argv': ['script.py'],
877-
'_orig_argv': ['python3', '-X', 'dev', 'script.py'],
877+
'orig_argv': ['python3', '-X', 'dev', 'script.py'],
878878
'run_filename': os.path.abspath('script.py'),
879879
'dev_mode': 1,
880880
'faulthandler': 1,
@@ -896,7 +896,7 @@ def test_preinit_dont_parse_argv(self):
896896
"script.py"]
897897
config = {
898898
'argv': argv,
899-
'_orig_argv': argv,
899+
'orig_argv': argv,
900900
'isolated': 0,
901901
}
902902
self.check_all_configs("test_preinit_dont_parse_argv", config, preconfig,
@@ -975,9 +975,9 @@ def test_init_sys_add(self):
975975
'ignore:::sysadd_warnoption',
976976
'ignore:::config_warnoption',
977977
],
978-
'_orig_argv': ['python3',
979-
'-W', 'ignore:::cmdline_warnoption',
980-
'-X', 'cmdline_xoption'],
978+
'orig_argv': ['python3',
979+
'-W', 'ignore:::cmdline_warnoption',
980+
'-X', 'cmdline_xoption'],
981981
}
982982
self.check_all_configs("test_init_sys_add", config, api=API_PYTHON)
983983

@@ -986,7 +986,7 @@ def test_init_run_main(self):
986986
'print(json.dumps(_testinternalcapi.get_configs()))')
987987
config = {
988988
'argv': ['-c', 'arg2'],
989-
'_orig_argv': ['python3', '-c', code, 'arg2'],
989+
'orig_argv': ['python3', '-c', code, 'arg2'],
990990
'program_name': './python3',
991991
'run_command': code + '\n',
992992
'parse_argv': 1,
@@ -998,9 +998,9 @@ def test_init_main(self):
998998
'print(json.dumps(_testinternalcapi.get_configs()))')
999999
config = {
10001000
'argv': ['-c', 'arg2'],
1001-
'_orig_argv': ['python3',
1002-
'-c', code,
1003-
'arg2'],
1001+
'orig_argv': ['python3',
1002+
'-c', code,
1003+
'arg2'],
10041004
'program_name': './python3',
10051005
'run_command': code + '\n',
10061006
'parse_argv': 1,
@@ -1014,7 +1014,7 @@ def test_init_parse_argv(self):
10141014
config = {
10151015
'parse_argv': 1,
10161016
'argv': ['-c', 'arg1', '-v', 'arg3'],
1017-
'_orig_argv': ['./argv0', '-E', '-c', 'pass', 'arg1', '-v', 'arg3'],
1017+
'orig_argv': ['./argv0', '-E', '-c', 'pass', 'arg1', '-v', 'arg3'],
10181018
'program_name': './argv0',
10191019
'run_command': 'pass\n',
10201020
'use_environment': 0,
@@ -1028,7 +1028,7 @@ def test_init_dont_parse_argv(self):
10281028
config = {
10291029
'parse_argv': 0,
10301030
'argv': ['./argv0', '-E', '-c', 'pass', 'arg1', '-v', 'arg3'],
1031-
'_orig_argv': ['./argv0', '-E', '-c', 'pass', 'arg1', '-v', 'arg3'],
1031+
'orig_argv': ['./argv0', '-E', '-c', 'pass', 'arg1', '-v', 'arg3'],
10321032
'program_name': './argv0',
10331033
}
10341034
self.check_all_configs("test_init_dont_parse_argv", config, pre_config,
@@ -1316,9 +1316,9 @@ def test_init_warnoptions(self):
13161316
'faulthandler': 1,
13171317
'bytes_warning': 1,
13181318
'warnoptions': warnoptions,
1319-
'_orig_argv': ['python3',
1320-
'-Wignore:::cmdline1',
1321-
'-Wignore:::cmdline2'],
1319+
'orig_argv': ['python3',
1320+
'-Wignore:::cmdline1',
1321+
'-Wignore:::cmdline2'],
13221322
}
13231323
self.check_all_configs("test_init_warnoptions", config, preconfig,
13241324
api=API_PYTHON)

Lib/test/test_sys.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,11 @@ def g456():
434434
def test_attributes(self):
435435
self.assertIsInstance(sys.api_version, int)
436436
self.assertIsInstance(sys.argv, list)
437+
for arg in sys.argv:
438+
self.assertIsInstance(arg, str)
439+
self.assertIsInstance(sys.orig_argv, list)
440+
for arg in sys.orig_argv:
441+
self.assertIsInstance(arg, str)
437442
self.assertIn(sys.byteorder, ("little", "big"))
438443
self.assertIsInstance(sys.builtin_module_names, tuple)
439444
self.assertIsInstance(sys.copyright, str)
@@ -930,6 +935,21 @@ def test__enablelegacywindowsfsencoding(self):
930935
out = out.decode('ascii', 'replace').rstrip()
931936
self.assertEqual(out, 'mbcs replace')
932937

938+
def test_orig_argv(self):
939+
code = textwrap.dedent('''
940+
import sys
941+
print(sys.argv)
942+
print(sys.orig_argv)
943+
''')
944+
args = [sys.executable, '-I', '-X', 'utf8', '-c', code, 'arg']
945+
proc = subprocess.run(args, check=True, capture_output=True, text=True)
946+
expected = [
947+
repr(['-c', 'arg']), # sys.argv
948+
repr(args), # sys.orig_argv
949+
]
950+
self.assertEqual(proc.stdout.rstrip().splitlines(), expected,
951+
proc)
952+
933953

934954
@test.support.cpython_only
935955
class UnraisableHookTest(unittest.TestCase):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :data:`sys.orig_argv` attribute: the list of the original command line
2+
arguments passed to the Python executable.

Python/initconfig.c

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ PyConfig_Clear(PyConfig *config)
601601
CLEAR(config->run_filename);
602602
CLEAR(config->check_hash_pycs_mode);
603603

604-
_PyWideStringList_Clear(&config->_orig_argv);
604+
_PyWideStringList_Clear(&config->orig_argv);
605605
#undef CLEAR
606606
}
607607

@@ -856,7 +856,7 @@ _PyConfig_Copy(PyConfig *config, const PyConfig *config2)
856856
COPY_ATTR(pathconfig_warnings);
857857
COPY_ATTR(_init_main);
858858
COPY_ATTR(_isolated_interpreter);
859-
COPY_WSTRLIST(_orig_argv);
859+
COPY_WSTRLIST(orig_argv);
860860

861861
#undef COPY_ATTR
862862
#undef COPY_WSTR_ATTR
@@ -957,7 +957,7 @@ config_as_dict(const PyConfig *config)
957957
SET_ITEM_INT(pathconfig_warnings);
958958
SET_ITEM_INT(_init_main);
959959
SET_ITEM_INT(_isolated_interpreter);
960-
SET_ITEM_WSTRLIST(_orig_argv);
960+
SET_ITEM_WSTRLIST(orig_argv);
961961

962962
return dict;
963963

@@ -1864,8 +1864,8 @@ _PyConfig_Write(const PyConfig *config, _PyRuntimeState *runtime)
18641864
preconfig->use_environment = config->use_environment;
18651865
preconfig->dev_mode = config->dev_mode;
18661866

1867-
if (_Py_SetArgcArgv(config->_orig_argv.length,
1868-
config->_orig_argv.items) < 0)
1867+
if (_Py_SetArgcArgv(config->orig_argv.length,
1868+
config->orig_argv.items) < 0)
18691869
{
18701870
return _PyStatus_NO_MEMORY();
18711871
}
@@ -2501,11 +2501,11 @@ PyConfig_Read(PyConfig *config)
25012501

25022502
config_get_global_vars(config);
25032503

2504-
if (config->_orig_argv.length == 0
2504+
if (config->orig_argv.length == 0
25052505
&& !(config->argv.length == 1
25062506
&& wcscmp(config->argv.items[0], L"") == 0))
25072507
{
2508-
if (_PyWideStringList_Copy(&config->_orig_argv, &config->argv) < 0) {
2508+
if (_PyWideStringList_Copy(&config->orig_argv, &config->argv) < 0) {
25092509
return _PyStatus_NO_MEMORY();
25102510
}
25112511
}
@@ -2589,7 +2589,7 @@ PyConfig_Read(PyConfig *config)
25892589
assert(config->check_hash_pycs_mode != NULL);
25902590
assert(config->_install_importlib >= 0);
25912591
assert(config->pathconfig_warnings >= 0);
2592-
assert(_PyWideStringList_CheckConsistency(&config->_orig_argv));
2592+
assert(_PyWideStringList_CheckConsistency(&config->orig_argv));
25932593

25942594
status = _PyStatus_OK();
25952595

Python/sysmodule.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2931,6 +2931,7 @@ _PySys_InitMain(PyThreadState *tstate)
29312931
}
29322932

29332933
COPY_LIST("argv", config->argv);
2934+
COPY_LIST("orig_argv", config->orig_argv);
29342935
COPY_LIST("warnoptions", config->warnoptions);
29352936

29362937
PyObject *xoptions = sys_create_xoptions_dict(config);

0 commit comments

Comments
 (0)