From fb5a1af6666a88a9d4dc6d0ee887d85937a19137 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 10 Jan 2025 13:16:04 +0000 Subject: [PATCH 1/3] bump min required Python version to 3.10 --- .cirrus.yml | 18 ++++++++---------- README.md | 2 +- contrib/freeze_packages.sh | 2 +- run_electrum | 2 +- setup.py | 2 +- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index b53ee7039..103c4cb00 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -10,8 +10,6 @@ task: TOXENV: py3 ELECTRUM_PYTHON_NAME: python3 matrix: - - env: - ELECTRUM_PYTHON_VERSION: 3.9 - env: ELECTRUM_PYTHON_VERSION: 3.10 - env: @@ -85,11 +83,11 @@ task: locale_script: - contrib/push_locale env: - ELECTRUM_IMAGE: python:3.9 + ELECTRUM_IMAGE: python:3.10 ELECTRUM_REQUIREMENTS_CI: contrib/requirements/requirements-ci.txt # in addition, crowdin_api_key is set as an "override" in https://cirrus-ci.com/settings/... depends_on: - - "unittests: Tox Python 3.9" + - "unittests: Tox Python 3.10" only_if: $CIRRUS_BRANCH == 'master' task: @@ -156,7 +154,7 @@ task: flake8_script: - flake8 . --count --select="$ELECTRUM_LINTERS" --ignore="$ELECTRUM_LINTERS_IGNORE" --show-source --statistics --exclude "*_pb2.py,electrum/_vendor/" env: - ELECTRUM_IMAGE: python:3.9 + ELECTRUM_IMAGE: python:3.10 ELECTRUM_REQUIREMENTS: contrib/requirements/requirements.txt matrix: - name: "linter: Flake8 Mandatory" @@ -212,7 +210,7 @@ task: CIRRUS_WORKING_DIR: /opt/wine64/drive_c/electrum CIRRUS_DOCKER_CONTEXT: contrib/build-wine depends_on: - - "unittests: Tox Python 3.9" + - "unittests: Tox Python 3.10" task: name: "build: Android (QML $APK_ARCH)" @@ -246,7 +244,7 @@ task: binaries_artifacts: path: "dist/*" depends_on: - - "unittests: Tox Python 3.9" + - "unittests: Tox Python 3.10" ## mac build disabled, as Cirrus CI no longer supports Intel-based mac builds #task: @@ -309,7 +307,7 @@ task: env: CIRRUS_DOCKER_CONTEXT: contrib/build-linux/appimage depends_on: - - "unittests: Tox Python 3.9" + - "unittests: Tox Python 3.10" task: container: @@ -332,12 +330,12 @@ task: env: OMIT_UNCLEAN_FILES: 1 depends_on: - - "unittests: Tox Python 3.9" + - "unittests: Tox Python 3.10" task: name: "check submodules" container: - image: python:3.9 + image: python:3.10 cpu: 1 memory: 1G fetch_script: diff --git a/README.md b/README.md index f8142e6ca..afc7c889f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ``` Licence: MIT Licence Author: Thomas Voegtlin -Language: Python (>= 3.8) +Language: Python (>= 3.10) Homepage: https://electrum.org/ ``` diff --git a/contrib/freeze_packages.sh b/contrib/freeze_packages.sh index c0eeda6e8..e29572018 100755 --- a/contrib/freeze_packages.sh +++ b/contrib/freeze_packages.sh @@ -8,7 +8,7 @@ contrib=$(dirname "$0") # note: we should not use a higher version of python than what the binaries bundle if [[ ! "$SYSTEM_PYTHON" ]] ; then - SYSTEM_PYTHON=$(which python3.8) || printf "" + SYSTEM_PYTHON=$(which python3.10) || printf "" else SYSTEM_PYTHON=$(which $SYSTEM_PYTHON) || printf "" fi diff --git a/run_electrum b/run_electrum index cc582f2c0..439e71b9c 100755 --- a/run_electrum +++ b/run_electrum @@ -27,7 +27,7 @@ import os import sys -MIN_PYTHON_VERSION = "3.8.0" # FIXME duplicated from setup.py +MIN_PYTHON_VERSION = "3.10.0" # FIXME duplicated from setup.py _min_python_version_tuple = tuple(map(int, (MIN_PYTHON_VERSION.split(".")))) diff --git a/setup.py b/setup.py index ddf604286..a004490f6 100755 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ import subprocess from setuptools import setup, find_packages from setuptools.command.install import install -MIN_PYTHON_VERSION = "3.8.0" +MIN_PYTHON_VERSION = "3.10.0" _min_python_version_tuple = tuple(map(int, (MIN_PYTHON_VERSION.split(".")))) From be2cd02e54f96aec127e5f101e7b54063fbac6ec Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 10 Jan 2025 13:26:39 +0000 Subject: [PATCH 2/3] some clean-ups now that we require python 3.10 --- electrum/daemon.py | 4 ++-- electrum/network.py | 3 ++- electrum/plugin.py | 13 +++---------- electrum/util.py | 30 +----------------------------- tests/qt_util.py | 4 +--- 5 files changed, 9 insertions(+), 45 deletions(-) diff --git a/electrum/daemon.py b/electrum/daemon.py index 825f2b058..f399d01fe 100644 --- a/electrum/daemon.py +++ b/electrum/daemon.py @@ -46,7 +46,7 @@ from .network import Network from .util import (json_decode, to_bytes, to_string, profiler, standardize_path, constant_time_compare, InvalidPassword) from .invoices import PR_PAID, PR_EXPIRED from .util import log_exceptions, ignore_exceptions, randrange, OldTaskGroup, UserFacingException, JsonRPCError -from .util import EventListener, event_listener, traceback_format_exception +from .util import EventListener, event_listener from .wallet import Wallet, Abstract_Wallet from .storage import WalletStorage from .wallet_db import WalletDB, WalletRequiresSplit, WalletRequiresUpgrade, WalletUnfinished @@ -264,7 +264,7 @@ class AuthenticatedServer(Logger): 'message': "internal error while executing RPC", 'data': { "exception": repr(e), - "traceback": "".join(traceback_format_exception(e)), + "traceback": "".join(traceback.format_exception(e)), }, } return web.json_response(response) diff --git a/electrum/network.py b/electrum/network.py index 2b10b6ad3..48b824e82 100644 --- a/electrum/network.py +++ b/electrum/network.py @@ -38,6 +38,7 @@ from concurrent import futures import copy import functools from enum import IntEnum +from contextlib import nullcontext import aiorpcx from aiorpcx import ignore_after, NetAddress @@ -47,7 +48,7 @@ from . import util from .util import (log_exceptions, ignore_exceptions, OldTaskGroup, bfh, make_aiohttp_session, send_exception_to_crash_reporter, is_hash256_str, is_non_negative_integer, MyEncoder, NetworkRetryManager, - nullcontext, error_text_str_to_safe_str) + error_text_str_to_safe_str) from .bitcoin import COIN, DummyAddress, DummyAddressUsedInTxException from . import constants from . import blockchain diff --git a/electrum/plugin.py b/electrum/plugin.py index 67eb45867..f9eca84b8 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -127,10 +127,7 @@ class Plugins(DaemonThread): # sys.modules needs to be modified for relative imports to work # see https://stackoverflow.com/a/50395128 sys.modules[path] = module - if sys.version_info >= (3, 10): - spec.loader.exec_module(module) - else: - module = spec.loader.load_module(path) + spec.loader.exec_module(module) except Exception as e: raise Exception(f"Error pre-loading {path}: {repr(e)}") from e return module @@ -209,12 +206,8 @@ class Plugins(DaemonThread): if name in self.external_plugin_metadata: raise Exception(f"duplicate plugins for name={name}") module_path = f'electrum_external_plugins.{name}' - if sys.version_info >= (3, 10): - spec = zipfile.find_spec(name) - module = self.exec_module_from_spec(spec, module_path) - else: - module = zipfile.load_module(name) - sys.modules[module_path] = module + spec = zipfile.find_spec(name) + module = self.exec_module_from_spec(spec, module_path) d = module.__dict__ gui_good = self.gui_name in d.get('available_for', []) if not gui_good: diff --git a/electrum/util.py b/electrum/util.py index 4571a6e0e..cbaefd8d6 100644 --- a/electrum/util.py +++ b/electrum/util.py @@ -52,6 +52,7 @@ from functools import partial from abc import abstractmethod, ABC import socket import enum +from contextlib import nullcontext import attr import aiohttp @@ -2067,35 +2068,6 @@ def test_read_write_permissions(path) -> None: raise IOError('echo sanity-check failed') -class nullcontext: - """Context manager that does no additional processing. - This is a ~backport of contextlib.nullcontext from Python 3.10 - """ - - def __init__(self, enter_result=None): - self.enter_result = enter_result - - def __enter__(self): - return self.enter_result - - def __exit__(self, *excinfo): - pass - - async def __aenter__(self): - return self.enter_result - - async def __aexit__(self, *excinfo): - pass - - -def traceback_format_exception(exc: BaseException) -> Sequence[str]: - """Compatibility wrapper for stdlib traceback.format_exception using python 3.10+ API.""" - if sys.version_info[:3] >= (3, 10): - return traceback.format_exception(exc) - else: - return traceback.format_exception(type(exc), value=exc, tb=exc.__traceback__) - - class classproperty(property): """~read-only class-level @property from https://stackoverflow.com/a/13624858 by denis-ryzhkov diff --git a/tests/qt_util.py b/tests/qt_util.py index 2863425ce..cad939ed6 100644 --- a/tests/qt_util.py +++ b/tests/qt_util.py @@ -6,8 +6,6 @@ from unittest import SkipTest from PyQt6.QtCore import QCoreApplication, QMetaObject, Qt, pyqtSlot, QObject -from electrum.util import traceback_format_exception - class TestQCoreApplication(QCoreApplication): @pyqtSlot() @@ -78,7 +76,7 @@ def qt_test(func): if not res: self._e = Exception('testcase timed out') if self._e: - print("".join(traceback_format_exception(self._e))) + print("".join(traceback.format_exception(self._e))) # deallocate stored exception from qt thread otherwise we SEGV garbage collector # instead, re-create using the exception message, special casing AssertionError and SkipTest e = None From 450768ee6c270d89bbc2ea868a76d2494e131db5 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 10 Jan 2025 14:08:10 +0000 Subject: [PATCH 3/3] sdist build: bump base image to debian 12 to have py3.10+ - debian 11 only has python 3.9, deb12 has py3.11 - pip install pip is no longer needed, atm apt has new enough pip - and on deb12, started getting "error: externally-managed-environment" - faketime does not seem to work properly on debian 12 (getting reproducibility issues for the tarball) - so instead we untar, fix the timestamps manually, and re-tar --- contrib/build-linux/sdist/Dockerfile | 3 +-- contrib/build-linux/sdist/make_sdist.sh | 35 ++++++++++++++++++------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/contrib/build-linux/sdist/Dockerfile b/contrib/build-linux/sdist/Dockerfile index 1a7a468a9..d397e85a7 100644 --- a/contrib/build-linux/sdist/Dockerfile +++ b/contrib/build-linux/sdist/Dockerfile @@ -1,4 +1,4 @@ -FROM debian:bullseye@sha256:43ef0c6c3585d5b406caa7a0f232ff5a19c1402aeb415f68bcd1cf9d10180af8 +FROM debian:bookworm@sha256:b877a1a3fdf02469440f1768cf69c9771338a875b7add5e80c45b756c92ac20a ENV LC_ALL=C.UTF-8 LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractive @@ -11,7 +11,6 @@ RUN apt-get update -q && \ python3-pip \ python3-setuptools \ python3-venv \ - faketime \ && \ rm -rf /var/lib/apt/lists/* && \ apt-get autoremove -y && \ diff --git a/contrib/build-linux/sdist/make_sdist.sh b/contrib/build-linux/sdist/make_sdist.sh index 4daff180e..66e91bee7 100755 --- a/contrib/build-linux/sdist/make_sdist.sh +++ b/contrib/build-linux/sdist/make_sdist.sh @@ -6,21 +6,20 @@ PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/../../.." CONTRIB="$PROJECT_ROOT/contrib" CONTRIB_SDIST="$CONTRIB/build-linux/sdist" DISTDIR="$PROJECT_ROOT/dist" +BUILDDIR="$CONTRIB_SDIST/build" LOCALE="$PROJECT_ROOT/electrum/locale" . "$CONTRIB"/build_tools_util.sh git -C "$PROJECT_ROOT" rev-parse 2>/dev/null || fail "Building outside a git clone is not supported." -# note that at least py3.7 is needed, to have https://bugs.python.org/issue30693 +rm -rf "$BUILDDIR" +mkdir -p "$BUILDDIR" "$DISTDIR" + python3 --version || fail "python interpreter not found" break_legacy_easy_install -# upgrade to modern pip so that it knows the flags we need. -# (make_packages.sh will later install a pinned version of pip in a venv) -python3 -m pip install --upgrade pip - rm -rf "$PROJECT_ROOT/packages/" if ([ "$OMIT_UNCLEAN_FILES" != 1 ]); then "$CONTRIB"/make_packages.sh || fail "make_packages failed" @@ -51,16 +50,32 @@ fi # note: .zip sdists would not be reproducible due to https://bugs.python.org/issue40963 if ([ "$OMIT_UNCLEAN_FILES" = 1 ]); then - PY_DISTDIR="dist/_sourceonly" # The DISTDIR variable of this script is only used to find where the output is *finally* placed. + PY_DISTDIR="$BUILDDIR/dist1/_sourceonly" # The DISTDIR variable of this script is only used to find where the output is *finally* placed. else - PY_DISTDIR="dist" + PY_DISTDIR="$BUILDDIR/dist1" fi - TZ=UTC faketime -f '2000-11-11 11:11:11' python3 setup.py --quiet sdist --format=gztar --dist-dir="$PY_DISTDIR" + # build initial tar.gz + python3 setup.py --quiet sdist --format=gztar --dist-dir="$PY_DISTDIR" + + VERSION=$("$CONTRIB"/print_electrum_version.py) if ([ "$OMIT_UNCLEAN_FILES" = 1 ]); then - VERSION=$("$CONTRIB"/print_electrum_version.py) - mv "dist/_sourceonly/Electrum-$VERSION.tar.gz" "dist/Electrum-sourceonly-$VERSION.tar.gz" + FINAL_DISTNAME="Electrum-sourceonly-$VERSION.tar.gz" + else + FINAL_DISTNAME="Electrum-$VERSION.tar.gz" + fi + if ([ "$OMIT_UNCLEAN_FILES" = 1 ]); then + mv "$PY_DISTDIR/Electrum-$VERSION.tar.gz" "$PY_DISTDIR/../$FINAL_DISTNAME" rmdir "$PY_DISTDIR" fi + + # the initial tar.gz is not reproducible, see https://github.com/pypa/setuptools/issues/2133 + # so we untar, fix timestamps, and then re-tar + mkdir -p "$BUILDDIR/dist2" + cd "$BUILDDIR/dist2" + tar -xzf "$BUILDDIR/dist1/$FINAL_DISTNAME" + find -exec touch -h -d '2000-11-11T11:11:11+00:00' {} + + GZIP=-n tar --sort=name -czf "$FINAL_DISTNAME" "Electrum-$VERSION/" + mv "$FINAL_DISTNAME" "$DISTDIR/$FINAL_DISTNAME" )