Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 17 additions & 23 deletions remi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,32 +32,26 @@

from .server import App, Server, start

# importlib.metadata is available in Python 3.8+
useForVersionCheck = None
try:
import importlib.metadata
useForVersionCheck = "importlib.metadata"
except ImportError:
try:
import pkg_resources
useForVersionCheck = "pkg_resources"
except ImportError:
pass
# Hardcoded fallback version — used when package metadata is unavailable
# (e.g. PyInstaller frozen builds, editable installs without metadata).
# Keep in sync with setup.py.
_FALLBACK_VERSION = "2026.03.24"
__version__ = _FALLBACK_VERSION

if useForVersionCheck == "importlib.metadata":
# importlib.metadata is available in Python 3.8+; pkg_resources for older.
try:
from importlib.metadata import version, PackageNotFoundError
try:
__version__ = version(__name__)
except PackageNotFoundError:
# package is not installed
pass
elif useForVersionCheck == "pkg_resources":
from pkg_resources import get_distribution, DistributionNotFound
pass # metadata absent (frozen build etc.) — fallback stays
except ImportError:
try:
__version__ = get_distribution(__name__).version
except DistributionNotFound:
# package is not installed
pass
else:
# neither importlib.metadata nor pkg_resources is available
print("WARNING: cannot check remi version, please install importlib-metadata (python >= 3.8) or the pkg_resources module by installing setuptools")
from pkg_resources import get_distribution, DistributionNotFound
try:
__version__ = get_distribution(__name__).version
except DistributionNotFound:
pass # package not installed — fallback stays
except ImportError:
print("WARNING: cannot check remi version, please install "
"importlib-metadata (Python >= 3.8) or setuptools")
52 changes: 45 additions & 7 deletions remi/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,47 @@
from urllib.parse import unquote_to_bytes
from urllib.parse import urlparse
from urllib.parse import parse_qs
import cgi
import io
import weakref

# cgi.FieldStorage was removed in Python 3.13 (deprecated since 3.11).
# Provide a minimal replacement using email.parser when cgi is absent.
try:
from cgi import FieldStorage as _FieldStorage
_HAS_CGI = True
except ImportError:
import email.parser as _email_parser
_HAS_CGI = False

class _FieldItem:
"""Minimal stand-in for a cgi.FieldStorage field (file upload part)."""
def __init__(self, filename, data):
self.filename = filename
self.file = io.BytesIO(data if data is not None else b'')

class _FieldStorage:
"""Minimal stand-in for cgi.FieldStorage, used only in do_POST (file uploads)."""
def __init__(self, fp, headers, environ):
self._fields = {}
content_length = int(headers.get('Content-Length', 0))
body = fp.read(content_length)
content_type = environ.get('CONTENT_TYPE', headers.get('Content-Type', ''))
raw = ('Content-Type: %s\r\n\r\n' % content_type).encode('utf-8') + body
msg = _email_parser.BytesParser().parsebytes(raw)
if msg.is_multipart():
for part in msg.get_payload():
name = part.get_param('name', header='content-disposition')
filename = part.get_param('filename', header='content-disposition')
if name:
data = part.get_payload(decode=True)
self._fields[name] = _FieldItem(filename, data)

def keys(self):
return self._fields.keys()

def __getitem__(self, key):
return self._fields[key]

import zlib

import select
Expand Down Expand Up @@ -574,10 +612,10 @@ def do_POST(self):
filename = self.headers['filename']
listener_widget = runtimeInstances[self.headers['listener']]
listener_function = self.headers['listener_function']
form = cgi.FieldStorage(fp=self.rfile,
headers=self.headers,
environ={'REQUEST_METHOD': 'POST',
'CONTENT_TYPE': self.headers['Content-Type']})
form = _FieldStorage(fp=self.rfile,
headers=self.headers,
environ={'REQUEST_METHOD': 'POST',
'CONTENT_TYPE': self.headers['Content-Type']})
# Echo back information about what was posted in the form
for field in form.keys():
field_item = form[field]
Expand Down Expand Up @@ -762,8 +800,8 @@ def onload(self, emitter):
def onerror(self, message, source, lineno, colno, error):
""" WebPage Event that occurs on webpage errors
"""
self._log.debug("""App.onerror event occurred in webpage:
\nMESSAGE:%s\nSOURCE:%s\nLINENO:%s\nCOLNO:%s\ERROR:%s\n"""%(message, source, lineno, colno, error))
self._log.debug("""App.onerror event occurred in webpage:
\nMESSAGE:%s\nSOURCE:%s\nLINENO:%s\nCOLNO:%s\\ERROR:%s\n"""%(message, source, lineno, colno, error))

def ononline(self, emitter):
""" WebPage Event that occurs on webpage goes online after a disconnection
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
'packages':setuptools.find_packages(),
'include_package_data':True,
'setup_requires':['setuptools_scm'],
'version': '2026.02.04',
'version': '2026.03.24',
}
try:
setup(**params)
Expand Down
120 changes: 120 additions & 0 deletions test/test_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/env python
"""
Tests for Python 3.13 / PyInstaller compatibility:
- __version__ is always defined (even when package metadata is absent)
- importlib.metadata path is used on Python 3.8+
- pkg_resources fallback is used when importlib.metadata is unavailable
- Hardcoded fallback is used when neither can find the package
- server.py contains no invalid escape sequences (SyntaxWarning-free)
"""

import importlib
import py_compile
import os
import sys
import unittest
import warnings
from unittest.mock import patch, MagicMock

import remi

# Absolute path to server.py so tests run from any working directory
_SERVER_PY = os.path.join(os.path.dirname(__file__), '..', 'remi', 'server.py')
_SERVER_PY = os.path.normpath(_SERVER_PY)


class TestNoSyntaxWarnings(unittest.TestCase):
"""server.py must compile without any SyntaxWarning (e.g. invalid escape sequences)."""

def test_server_py_no_syntax_warnings(self):
"""Compiling server.py raises no SyntaxWarning on Python 3.12+."""
with warnings.catch_warnings():
warnings.simplefilter("error", SyntaxWarning)
try:
py_compile.compile(_SERVER_PY, doraise=True)
except py_compile.PyCompileError as exc:
self.fail("server.py has a SyntaxWarning/SyntaxError: %s" % exc)


class TestVersionAlwaysDefined(unittest.TestCase):
"""remi.__version__ must always be a non-empty string."""

def test_version_is_string(self):
self.assertIsInstance(remi.__version__, str)

def test_version_non_empty(self):
self.assertTrue(len(remi.__version__) > 0)


class TestVersionImportlibMetadataPath(unittest.TestCase):
"""When importlib.metadata.version() succeeds, __version__ is taken from it."""

def test_importlib_metadata_used(self):
fake_version = "9999.01.01"
with patch("importlib.metadata.version", return_value=fake_version):
importlib.reload(remi)
self.assertEqual(remi.__version__, fake_version)
# Restore
importlib.reload(remi)


class TestVersionPkgResourcesFallback(unittest.TestCase):
"""When importlib.metadata raises ImportError, pkg_resources provides the version."""

def test_pkg_resources_fallback(self):
fake_version = "8888.06.15"

mock_dist = MagicMock()
mock_dist.version = fake_version
mock_pkg = MagicMock()
mock_pkg.get_distribution.return_value = mock_dist
mock_pkg.DistributionNotFound = Exception

# Make importlib.metadata unavailable, supply a mock pkg_resources
with patch.dict(sys.modules, {"importlib.metadata": None, "pkg_resources": mock_pkg}):
importlib.reload(remi)

self.assertEqual(remi.__version__, fake_version)
# Restore
importlib.reload(remi)


class TestVersionHardcodedFallback(unittest.TestCase):
"""When both importlib.metadata and pkg_resources cannot find the package,
__version__ falls back to the hardcoded _FALLBACK_VERSION string."""

def test_hardcoded_fallback_used(self):
from importlib.metadata import PackageNotFoundError

# importlib.metadata is importable but raises PackageNotFoundError
with patch("importlib.metadata.version", side_effect=PackageNotFoundError("remi")):
importlib.reload(remi)

# __version__ must still be a non-empty string (the hardcoded fallback)
self.assertIsInstance(remi.__version__, str)
self.assertTrue(len(remi.__version__) > 0)
self.assertEqual(remi.__version__, remi._FALLBACK_VERSION)
# Restore
importlib.reload(remi)

def test_fallback_version_matches_setup(self):
"""_FALLBACK_VERSION in __init__.py must match the version in setup.py."""
setup_py = os.path.join(os.path.dirname(__file__), '..', 'setup.py')
setup_py = os.path.normpath(setup_py)
with open(setup_py) as f:
content = f.read()
# Extract the version string from setup.py: 'version': '...'
import re
match = re.search(r"'version'\s*:\s*'([^']+)'", content)
self.assertIsNotNone(match, "Could not find 'version' in setup.py")
setup_version = match.group(1)
self.assertEqual(
remi._FALLBACK_VERSION,
setup_version,
"_FALLBACK_VERSION in __init__.py (%s) does not match setup.py (%s)"
% (remi._FALLBACK_VERSION, setup_version),
)


if __name__ == "__main__":
unittest.main()