From c03ff01c3207b99d4af060435514a28469515dfe Mon Sep 17 00:00:00 2001 From: imSp4rky Date: Fri, 5 Jun 2026 15:21:39 -0600 Subject: [PATCH] fix(core): replace deprecated ast.Num visitor in FPS parser ast.Num/node.n deprecated since Python 3.8; NodeVisitor falls back to visit_Num with a DeprecationWarning per dispatch. Under -W error this surfaced as a misleading fps ValueError in Video.init. Replace with visit_Constant, reject non-numeric constants, and pin parse results for int/fraction/float inputs in a regression test. --- tests/core/__init__.py | 0 tests/core/test_fps.py | 29 +++++++++++++++++++++++++++++ unshackle/core/utilities.py | 6 ++++-- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 tests/core/__init__.py create mode 100644 tests/core/test_fps.py diff --git a/tests/core/__init__.py b/tests/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/core/test_fps.py b/tests/core/test_fps.py new file mode 100644 index 0000000..d8af6cd --- /dev/null +++ b/tests/core/test_fps.py @@ -0,0 +1,29 @@ +import warnings + +import pytest + +from unshackle.core.utilities import FPS + + +@pytest.mark.parametrize( + ("expr", "expected"), + [ + ("24", 24), + ("23.976", pytest.approx(23.976)), + ("30000/1001", pytest.approx(29.97, abs=0.001)), + ], +) +def test_parse_pins_results_without_deprecation_warnings(expr: str, expected: object) -> None: + with warnings.catch_warnings(): + warnings.simplefilter("error", DeprecationWarning) + assert FPS.parse(expr) == expected + + +def test_parse_rejects_non_numeric_constant() -> None: + with pytest.raises(ValueError, match="Invalid fps value"): + FPS.parse("'24'") + + +def test_parse_rejects_non_division_operation() -> None: + with pytest.raises(ValueError, match="Invalid operation"): + FPS.parse("24+1") diff --git a/unshackle/core/utilities.py b/unshackle/core/utilities.py index 62de660..69ed7c8 100644 --- a/unshackle/core/utilities.py +++ b/unshackle/core/utilities.py @@ -726,8 +726,10 @@ class FPS(ast.NodeVisitor): return self.visit(node.left) / self.visit(node.right) raise ValueError(f"Invalid operation: {node.op}") - def visit_Num(self, node: ast.Num) -> complex: - return node.n + def visit_Constant(self, node: ast.Constant) -> float: + if not isinstance(node.value, (int, float)): + raise ValueError(f"Invalid fps value: {node.value!r}") + return node.value def visit_Expr(self, node: ast.Expr) -> float: return self.visit(node.value)