tests: minimum python version validation for user facing scripts

Adds [`Vermin`](https://github.com/netromdk/vermin) to make sure our
user facing Python scripts conform to the proper minimum Python version
requirements.

The tool parses Python code into an AST and checks it against a database
of rules covering Python 2.0-3.13.

Testing Python version compatibility is tricky because most issues only
show up at runtime. Type annotations like `str | None` (Python 3.10+)
compile just fine (under pyc) and they don't throw an exception until
you actually call the relevant function.

This makes it hard to catch compatibility bugs without running the code
(through all possible execution paths) on every Python version.

Signed-off-by: seg6 <hi@seg6.space>

Closes #12044
This commit is contained in:
seg6
2025-11-10 16:14:30 +01:00
committed by Johannes Altmanninger
parent 4ef1f993a1
commit 29024806e5
3 changed files with 33 additions and 0 deletions

View File

@@ -10,6 +10,8 @@ runs:
run: |
set -x
sudo pip install uv --break-system-packages
command -v uv
command -v uvx
# Check that pyproject.toml and the lock file are in sync.
# TODO Use "uv" to install Python as well.
: 'Note that --no-managed-python below would be implied but be explicit'

View File

@@ -295,6 +295,7 @@ class Deroffer:
g_re_word = re.compile(r"[a-zA-Z_]+") # equivalent to the word() method
g_re_number = re.compile(r"[+-]?\d+") # equivalent to the number() method
g_re_esc_char = re.compile(
# novermin
r"""([a-zA-Z_]) | # Word
([+-]?\d) | # Number
\\ # Backslash (for escape seq)
@@ -309,6 +310,7 @@ class Deroffer:
g_re_newline_collapse = re.compile(r"\n{3,}")
g_re_font = re.compile(
# novermin
r"""\\f( # Starts with backslash f
(\(\S{2}) | # Open paren, then two printable chars
(\[\S*?\]) | # Open bracket, zero or more printable characters, then close bracket
@@ -2090,6 +2092,7 @@ def get_paths_from_man_locations():
with open("/etc/man.conf", "r") as file:
data = file.read()
for key in ["MANPATH", "_default"]:
# novermin
for match in re.findall(r"^%s\s+(.*)$" % key, data, re.I | re.M):
parent_paths.append(match)
except FileNotFoundError:

View File

@@ -0,0 +1,28 @@
#RUN: %fish %s
#REQUIRES: command -v uvx
set -l webconfig (status dirname)/../../share/tools/web_config/webconfig.py
set -l create_manpage (status dirname)/../../share/tools/create_manpage_completions.py
set -l min_version 3.5
# use vermin to detect minimum Python version violations
# features enabled:
# - union-types: catch `str | None` syntax (3.10+)
# - fstring-self-doc: catch f'{var=}' syntax (3.8+)
set -l output (uvx vermin \
--no-tips \
--violations \
--feature union-types \
--feature fstring-self-doc \
--target=$min_version- \
$webconfig \
$create_manpage \
2>&1 | string collect)
set -l exit_code $pipestatus[1]
echo $exit_code
# CHECK: 0
if test $exit_code -ne 0
echo $output >&2
end