From ef61090880e6500bfd599eca2376cf8151f51385 Mon Sep 17 00:00:00 2001 From: bw2 Date: Mon, 28 Sep 2026 05:15:30 -0400 Subject: [PATCH 1/4] Added Python 3.14 to CI matrix and classifiers --- .github/workflows/test.yml | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1049d3c..593e7a8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,7 +59,7 @@ jobs: use-docker: true os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ['3.9','3.10','3.11','3.12','3.13'] + python-version: ['3.9','3.10','3.11','3.12','3.13','3.14'] use-docker: [false] fail-fast: false diff --git a/setup.py b/setup.py index 92d595a..677d988 100644 --- a/setup.py +++ b/setup.py @@ -117,6 +117,7 @@ def launch_http_server(directory): "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ], From b15dc912ade7af3cb8bc9f6531a951d221327b9f Mon Sep 17 00:00:00 2001 From: bw2 Date: Mon, 28 Sep 2026 05:18:24 -0400 Subject: [PATCH 2/4] Fixed argparse test harness for Python 3.14 TestProgName and warning filename checks --- tests/test_configargparse.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/test_configargparse.py b/tests/test_configargparse.py index cd4e786..ae9f187 100644 --- a/tests/test_configargparse.py +++ b/tests/test_configargparse.py @@ -4203,6 +4203,7 @@ def __fspath__(self_inner): "============================\n" ) else: + test_argparse_source_file = inspect.getsourcefile(test.test_argparse) test_argparse_source_code = inspect.getsource(test.test_argparse) test_argparse_source_code = ( test_argparse_source_code.replace( @@ -4210,6 +4211,14 @@ def __fspath__(self_inner): ) .replace("TestHelpFormattingMetaclass", "_TestHelpFormattingMetaclass") .replace("test_main", "_test_main") + .replace( + "import argparse\n parser = configargparse.ArgumentParser()", + "import configargparse\n parser = configargparse.ArgumentParser()", + ) + .replace( + "self.assertEqual(cm.filename, __file__)", + "self.assertEqual(cm.filename, test_argparse_source_file)", + ) ) # pytest tries to collect tests from TestHelpFormattingMetaclass, and @@ -4217,6 +4226,12 @@ def __fspath__(self_inner): # nor test function. Renaming TestHelpFormattingMetaclass and test_main # prevents pytest from trying. + # Python 3.14+ TestProgName runs its source in a subprocess, which needs + # to import configargparse itself. Several 3.14+ tests also check that a + # warning's filename is the test file, so the exec'd code is compiled + # under the upstream test file's path (which also gives tracebacks the + # right file and line numbers) and compared against that path. + # run or debug a subset of the argparse tests # test_argparse_source_code = test_argparse_source_code.replace( # "(TestCase)", "").replace( @@ -4227,7 +4242,7 @@ def __fspath__(self_inner): # test_argparse_source_code = test_argparse_source_code.replace( # "class TestMessageContentError", "class TestMessageContentError(TestCase)") - exec(test_argparse_source_code) + exec(compile(test_argparse_source_code, test_argparse_source_file, "exec")) # print argparse unittest source code def print_source_code(source_code, line_numbers, context_lines=10): From b5904d4fa9c1f1b48eb1931cfb30f72eb5a9b617 Mon Sep 17 00:00:00 2001 From: bw2 Date: Mon, 28 Sep 2026 05:21:11 -0400 Subject: [PATCH 3/4] Allowed usage line to wrap before [-h] in help text tests --- tests/test_configargparse.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_configargparse.py b/tests/test_configargparse.py index ae9f187..e1c965c 100644 --- a/tests/test_configargparse.py +++ b/tests/test_configargparse.py @@ -330,7 +330,7 @@ def testBasicCase2(self, use_groups=False): short_f = SHORT_OPT_METAVAR.format(metavar="FRMT") self.assertRegex( self.format_help(), - "usage: .* \\[-h\\] --genome GENOME \\[-v\\] -g MY_CFG_FILE\n?" + "usage: .*\\s+\\[-h\\] --genome GENOME \\[-v\\] -g MY_CFG_FILE\n?" "\\s+\\[-d DBSNP\\]\\s+\\[-f FRMT\\]\\s+vcf \\[vcf ...\\]\n\n" "positional arguments:\n" " vcf \\s+ Variant file\\(s\\)\n\n" @@ -350,7 +350,7 @@ def testBasicCase2(self, use_groups=False): short_f = SHORT_OPT_METAVAR.format(metavar="FRMT") self.assertRegex( self.format_help(), - "usage: .* \\[-h\\] --genome GENOME \\[-v\\] -g MY_CFG_FILE\n?" + "usage: .*\\s+\\[-h\\] --genome GENOME \\[-v\\] -g MY_CFG_FILE\n?" "\\s+\\[-d DBSNP\\]\\s+\\[-f FRMT\\]\\s+vcf \\[vcf ...\\]\n\n" "positional arguments:\n" " vcf \\s+ Variant file\\(s\\)\n\n" @@ -516,7 +516,7 @@ def testMutuallyExclusiveArgs(self): short_f = SHORT_OPT_METAVAR.format(metavar="FRMT") self.assertRegex( self.format_help(), - r"usage: .* \[-h\] --genome GENOME \[-v\]\s+\(-f1 TYPE1_CFG_FILE \|" + r"usage: .*\s+\[-h\] --genome GENOME \[-v\]\s+\(-f1 TYPE1_CFG_FILE \|" r"\s+-f2 TYPE2_CFG_FILE\)\s+\(-f FRMT \| -b\)\n\n" "%s:\n" " -h, --help show this help message and exit\n" @@ -1232,7 +1232,7 @@ def testConstructor_ConfigFileArgs(self): short_c = SHORT_OPT_METAVAR.format(metavar="CONFIG_FILE") self.assertRegex( self.format_help(), - r"usage: .* \[-h\] -c CONFIG_FILE --genome GENOME\n\n" + r"usage: .*\s+\[-h\] -c CONFIG_FILE --genome GENOME\n\n" r"%s:\n" r" -h, --help\s+ show this help message and exit\n" rf" -c{short_c}, --config CONFIG_FILE\s+ my config file\n" @@ -1303,7 +1303,7 @@ def test_FormatHelp(self): short_w = SHORT_OPT_METAVAR.format(metavar="CONFIG_OUTPUT_PATH") self.assertRegex( self.format_help(), - r"usage: .* \[-h\] -c CONFIG_FILE\s+" + r"usage: .*\s+\[-h\] -c CONFIG_FILE\s+" r"\[-w CONFIG_OUTPUT_PATH\]\s* --arg1\s+ARG1\s*\[--flag\]\s*" "%s:\\s*" "-h, --help \\s* show this help message and exit " From f7e66264c3000f1d036780ee6e4f517c8c2198cb Mon Sep 17 00:00:00 2001 From: bw2 Date: Mon, 28 Sep 2026 10:59:37 -0400 Subject: [PATCH 4/4] Made 3.14 TestProgName subprocesses import the configargparse under test --- tests/test_configargparse.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/test_configargparse.py b/tests/test_configargparse.py index e1c965c..52f37dc 100644 --- a/tests/test_configargparse.py +++ b/tests/test_configargparse.py @@ -4205,6 +4205,9 @@ def __fspath__(self_inner): else: test_argparse_source_file = inspect.getsourcefile(test.test_argparse) test_argparse_source_code = inspect.getsource(test.test_argparse) + configargparse_dir = os.path.dirname( + os.path.abspath(configargparse.__file__) + ).replace("\\", "/") test_argparse_source_code = ( test_argparse_source_code.replace( "argparse.ArgumentParser", "configargparse.ArgumentParser" @@ -4213,6 +4216,8 @@ def __fspath__(self_inner): .replace("test_main", "_test_main") .replace( "import argparse\n parser = configargparse.ArgumentParser()", + "import sys; " + f"sys.path.insert(0, {configargparse_dir!r}); " "import configargparse\n parser = configargparse.ArgumentParser()", ) .replace( @@ -4226,11 +4231,15 @@ def __fspath__(self_inner): # nor test function. Renaming TestHelpFormattingMetaclass and test_main # prevents pytest from trying. - # Python 3.14+ TestProgName runs its source in a subprocess, which needs - # to import configargparse itself. Several 3.14+ tests also check that a - # warning's filename is the test file, so the exec'd code is compiled - # under the upstream test file's path (which also gives tracebacks the - # right file and line numbers) and compared against that path. + # Python 3.14+ TestProgName runs its source in an isolated (-I) subprocess, + # which ignores PYTHONPATH and the working directory, so the script puts + # the directory of the configargparse under test on sys.path itself (with + # forward slashes so Windows backslashes aren't read as string escapes), + # on the same line as the import to keep line numbers unchanged. + # Several 3.14+ tests also check that a warning's filename is the test + # file, so the exec'd code is compiled under the upstream test file's path + # (which also gives tracebacks the right file and line numbers) and + # compared against that path. # run or debug a subset of the argparse tests # test_argparse_source_code = test_argparse_source_code.replace(