From 53bb407664e3cccf169e9ae628201f5bbe77cb78 Mon Sep 17 00:00:00 2001
From: Peter Schuster
Date: Tue, 17 Mar 2026 15:36:21 +0100
Subject: [PATCH] chore: extract glob for pyupgrade to separate script for
cross-platform compatibility
'sh' in tox.ini does not work on Windows in PowerShell.
Signed-off-by: Peter Schuster
---
tools/run_pyupgrade.py | 35 +++++++++++++++++++++++++++++++++++
tox.ini | 6 ++----
2 files changed, 37 insertions(+), 4 deletions(-)
create mode 100644 tools/run_pyupgrade.py
diff --git a/tools/run_pyupgrade.py b/tools/run_pyupgrade.py
new file mode 100644
index 000000000..f67ae15d2
--- /dev/null
+++ b/tools/run_pyupgrade.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python3
+
+# Wrapper around pyupgrade to perform a lookup of all *.py/*.pyi files in passed directories
+# and pass them to pyupgrade in a single invocation.
+#
+# Usage: run_pyupgrade.py --
+
+import subprocess # nosec - subprocess is used to run pyupgrade and not part of published library
+import sys
+from pathlib import Path
+
+if '--' not in sys.argv:
+ print('Usage: run_pyupgrade.py -- ', file=sys.stderr)
+ sys.exit(1)
+
+sep = sys.argv.index('--')
+pyupgrade_args = sys.argv[1:sep]
+directories = sys.argv[sep + 1:]
+
+if not directories:
+ print('Error: at least one directory must be specified after --', file=sys.stderr)
+ sys.exit(1)
+
+files = sorted({
+ str(file)
+ for directory in directories
+ for pattern in ['*.py', '*.pyi']
+ for file in Path(directory).rglob(pattern)
+})
+
+result = subprocess.run( # nosec - shell=False is used to prevent injection, all arg passed as a list
+ [sys.executable, '-m', 'pyupgrade', *pyupgrade_args, *files],
+ shell=False # w/o shell all args are passed directly to the process without the need for quotes or escaping
+)
+sys.exit(result.returncode)
diff --git a/tox.ini b/tox.ini
index af228b75a..8afcf3aa0 100644
--- a/tox.ini
+++ b/tox.ini
@@ -52,10 +52,8 @@ commands =
poetry run deptry -v .
[testenv:pyupgrade]
-allowlist_externals = poetry, sh
-commands = sh -c "\
- find cyclonedx typings tests tools examples -type f \( -name '*.py' -or -name '*.pyi' \) -print0 \
- | xargs -0 poetry run pyupgrade --py39-plus {posargs} "
+# first -- stops command parsing by poetry run, the second -- splits pyupgrade args from args for glob patterns
+commands = poetry run -- python tools/run_pyupgrade.py --py39-plus {posargs} -- cyclonedx typings tests tools examples
[testenv:isort]
commands = poetry run isort .