From 53bb407664e3cccf169e9ae628201f5bbe77cb78 Mon Sep 17 00:00:00 2001
From: Peter Schuster
Date: Tue, 17 Mar 2026 15:36:21 +0100
Subject: [PATCH 1/6] 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 .
From 9c8d05de92da91bb5608987b341dffd24ae21f2a Mon Sep 17 00:00:00 2001
From: Jan Kowalleck
Date: Mon, 23 Mar 2026 14:50:55 +0100
Subject: [PATCH 2/6] docs
Signed-off-by: Jan Kowalleck
---
tools/run_pyupgrade.py | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/tools/run_pyupgrade.py b/tools/run_pyupgrade.py
index f67ae15d2..d7e8459fe 100644
--- a/tools/run_pyupgrade.py
+++ b/tools/run_pyupgrade.py
@@ -1,9 +1,11 @@
#!/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 --
+"""
+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 e65cb85282a6fcd9ce94290883511679f76fdbea Mon Sep 17 00:00:00 2001
From: Jan Kowalleck
Date: Mon, 23 Mar 2026 14:54:25 +0100
Subject: [PATCH 3/6] executable
Signed-off-by: Jan Kowalleck
---
tools/run_pyupgrade.py | 0
1 file changed, 0 insertions(+), 0 deletions(-)
mode change 100644 => 100755 tools/run_pyupgrade.py
diff --git a/tools/run_pyupgrade.py b/tools/run_pyupgrade.py
old mode 100644
new mode 100755
From 13e9ef15ecf9a7239503380b3cc1fc24fbd6d4b7 Mon Sep 17 00:00:00 2001
From: Jan Kowalleck
Date: Mon, 23 Mar 2026 15:02:10 +0100
Subject: [PATCH 4/6] docs
Signed-off-by: Jan Kowalleck
---
tools/run_pyupgrade.py | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/tools/run_pyupgrade.py b/tools/run_pyupgrade.py
index d7e8459fe..1d936c256 100755
--- a/tools/run_pyupgrade.py
+++ b/tools/run_pyupgrade.py
@@ -1,18 +1,18 @@
#!/usr/bin/env python3
-"""
+import subprocess # nosec - subprocess is used to run pyupgrade and not part of published library
+import sys
+from pathlib import Path
+
+HELP=f"""
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 --
+Usage: {sys.argv[0]} [pyupgrade-args ...] --
"""
-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)
+ print(HELP, file=sys.stderr)
sys.exit(1)
sep = sys.argv.index('--')
@@ -20,8 +20,8 @@
directories = sys.argv[sep + 1:]
if not directories:
- print('Error: at least one directory must be specified after --', file=sys.stderr)
- sys.exit(1)
+ print('Error: at least one directory must be specified after --', '\n', HELP, file=sys.stderr)
+ sys.exit(2)
files = sorted({
str(file)
From cd7e434b4b4e3664d543e4af7dff11de79800b77 Mon Sep 17 00:00:00 2001
From: Jan Kowalleck
Date: Mon, 23 Mar 2026 15:02:50 +0100
Subject: [PATCH 5/6] style
Signed-off-by: Jan Kowalleck
---
tools/run_pyupgrade.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tools/run_pyupgrade.py b/tools/run_pyupgrade.py
index 1d936c256..fb4a48563 100755
--- a/tools/run_pyupgrade.py
+++ b/tools/run_pyupgrade.py
@@ -4,7 +4,7 @@
import sys
from pathlib import Path
-HELP=f"""
+HELP = f"""
Wrapper around pyupgrade to perform a lookup of all *.py/*.pyi files in passed directories
and pass them to pyupgrade in a single invocation.
@@ -30,7 +30,7 @@
for file in Path(directory).rglob(pattern)
})
-result = subprocess.run( # nosec - shell=False is used to prevent injection, all arg passed as a list
+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
)
From 0018b1bfd7602c5671cd66bacb6f0d27a812dc04 Mon Sep 17 00:00:00 2001
From: Jan Kowalleck
Date: Mon, 23 Mar 2026 15:06:07 +0100
Subject: [PATCH 6/6] license-header
Signed-off-by: Jan Kowalleck
---
tools/run_pyupgrade.py | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/tools/run_pyupgrade.py b/tools/run_pyupgrade.py
index fb4a48563..e040c5071 100755
--- a/tools/run_pyupgrade.py
+++ b/tools/run_pyupgrade.py
@@ -1,5 +1,22 @@
#!/usr/bin/env python3
+# This file is part of CycloneDX Python Library
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+# Copyright (c) OWASP Foundation. All Rights Reserved.
+
import subprocess # nosec - subprocess is used to run pyupgrade and not part of published library
import sys
from pathlib import Path