|
| 1 | +#!/usr/bin/env python |
| 2 | +""" |
| 3 | +Tests for Python 3.13 / PyInstaller compatibility: |
| 4 | + - __version__ is always defined (even when package metadata is absent) |
| 5 | + - importlib.metadata path is used on Python 3.8+ |
| 6 | + - pkg_resources fallback is used when importlib.metadata is unavailable |
| 7 | + - Hardcoded fallback is used when neither can find the package |
| 8 | + - server.py contains no invalid escape sequences (SyntaxWarning-free) |
| 9 | +""" |
| 10 | + |
| 11 | +import importlib |
| 12 | +import py_compile |
| 13 | +import os |
| 14 | +import sys |
| 15 | +import unittest |
| 16 | +import warnings |
| 17 | +from unittest.mock import patch, MagicMock |
| 18 | + |
| 19 | +import remi |
| 20 | + |
| 21 | +# Absolute path to server.py so tests run from any working directory |
| 22 | +_SERVER_PY = os.path.join(os.path.dirname(__file__), '..', 'remi', 'server.py') |
| 23 | +_SERVER_PY = os.path.normpath(_SERVER_PY) |
| 24 | + |
| 25 | + |
| 26 | +class TestNoSyntaxWarnings(unittest.TestCase): |
| 27 | + """server.py must compile without any SyntaxWarning (e.g. invalid escape sequences).""" |
| 28 | + |
| 29 | + def test_server_py_no_syntax_warnings(self): |
| 30 | + """Compiling server.py raises no SyntaxWarning on Python 3.12+.""" |
| 31 | + with warnings.catch_warnings(): |
| 32 | + warnings.simplefilter("error", SyntaxWarning) |
| 33 | + try: |
| 34 | + py_compile.compile(_SERVER_PY, doraise=True) |
| 35 | + except py_compile.PyCompileError as exc: |
| 36 | + self.fail("server.py has a SyntaxWarning/SyntaxError: %s" % exc) |
| 37 | + |
| 38 | + |
| 39 | +class TestVersionAlwaysDefined(unittest.TestCase): |
| 40 | + """remi.__version__ must always be a non-empty string.""" |
| 41 | + |
| 42 | + def test_version_is_string(self): |
| 43 | + self.assertIsInstance(remi.__version__, str) |
| 44 | + |
| 45 | + def test_version_non_empty(self): |
| 46 | + self.assertTrue(len(remi.__version__) > 0) |
| 47 | + |
| 48 | + |
| 49 | +class TestVersionImportlibMetadataPath(unittest.TestCase): |
| 50 | + """When importlib.metadata.version() succeeds, __version__ is taken from it.""" |
| 51 | + |
| 52 | + def test_importlib_metadata_used(self): |
| 53 | + fake_version = "9999.01.01" |
| 54 | + with patch("importlib.metadata.version", return_value=fake_version): |
| 55 | + importlib.reload(remi) |
| 56 | + self.assertEqual(remi.__version__, fake_version) |
| 57 | + # Restore |
| 58 | + importlib.reload(remi) |
| 59 | + |
| 60 | + |
| 61 | +class TestVersionPkgResourcesFallback(unittest.TestCase): |
| 62 | + """When importlib.metadata raises ImportError, pkg_resources provides the version.""" |
| 63 | + |
| 64 | + def test_pkg_resources_fallback(self): |
| 65 | + fake_version = "8888.06.15" |
| 66 | + |
| 67 | + mock_dist = MagicMock() |
| 68 | + mock_dist.version = fake_version |
| 69 | + mock_pkg = MagicMock() |
| 70 | + mock_pkg.get_distribution.return_value = mock_dist |
| 71 | + mock_pkg.DistributionNotFound = Exception |
| 72 | + |
| 73 | + # Make importlib.metadata unavailable, supply a mock pkg_resources |
| 74 | + with patch.dict(sys.modules, {"importlib.metadata": None, "pkg_resources": mock_pkg}): |
| 75 | + importlib.reload(remi) |
| 76 | + |
| 77 | + self.assertEqual(remi.__version__, fake_version) |
| 78 | + # Restore |
| 79 | + importlib.reload(remi) |
| 80 | + |
| 81 | + |
| 82 | +class TestVersionHardcodedFallback(unittest.TestCase): |
| 83 | + """When both importlib.metadata and pkg_resources cannot find the package, |
| 84 | + __version__ falls back to the hardcoded _FALLBACK_VERSION string.""" |
| 85 | + |
| 86 | + def test_hardcoded_fallback_used(self): |
| 87 | + from importlib.metadata import PackageNotFoundError |
| 88 | + |
| 89 | + # importlib.metadata is importable but raises PackageNotFoundError |
| 90 | + with patch("importlib.metadata.version", side_effect=PackageNotFoundError("remi")): |
| 91 | + importlib.reload(remi) |
| 92 | + |
| 93 | + # __version__ must still be a non-empty string (the hardcoded fallback) |
| 94 | + self.assertIsInstance(remi.__version__, str) |
| 95 | + self.assertTrue(len(remi.__version__) > 0) |
| 96 | + self.assertEqual(remi.__version__, remi._FALLBACK_VERSION) |
| 97 | + # Restore |
| 98 | + importlib.reload(remi) |
| 99 | + |
| 100 | + def test_fallback_version_matches_setup(self): |
| 101 | + """_FALLBACK_VERSION in __init__.py must match the version in setup.py.""" |
| 102 | + setup_py = os.path.join(os.path.dirname(__file__), '..', 'setup.py') |
| 103 | + setup_py = os.path.normpath(setup_py) |
| 104 | + with open(setup_py) as f: |
| 105 | + content = f.read() |
| 106 | + # Extract the version string from setup.py: 'version': '...' |
| 107 | + import re |
| 108 | + match = re.search(r"'version'\s*:\s*'([^']+)'", content) |
| 109 | + self.assertIsNotNone(match, "Could not find 'version' in setup.py") |
| 110 | + setup_version = match.group(1) |
| 111 | + self.assertEqual( |
| 112 | + remi._FALLBACK_VERSION, |
| 113 | + setup_version, |
| 114 | + "_FALLBACK_VERSION in __init__.py (%s) does not match setup.py (%s)" |
| 115 | + % (remi._FALLBACK_VERSION, setup_version), |
| 116 | + ) |
| 117 | + |
| 118 | + |
| 119 | +if __name__ == "__main__": |
| 120 | + unittest.main() |
0 commit comments