Runnable with both Python3 and Python2, with PyQt4 #2249

Open
kashikoibumi wants to merge 60 commits from kashikoibumi/py3 into v0.6
28 changed files with 312 additions and 122 deletions
Showing only changes of commit da58e00130 - Show all commits

View File

@ -4,7 +4,11 @@ export APPIMAGE_EXTRACT_AND_RUN=1
BUILDER=appimage-builder-x86_64.AppImage BUILDER=appimage-builder-x86_64.AppImage
RECIPE=packages/AppImage/AppImageBuilder.yml RECIPE=packages/AppImage/AppImageBuilder.yml
git remote add -f upstream https://github.com/Bitmessage/PyBitmessage.git
HEAD="$(git rev-parse HEAD)"
UPSTREAM="$(git merge-base --fork-point upstream/v0.6)"
export APP_VERSION=$(git describe --tags | cut -d- -f1,3 | tr -d v) export APP_VERSION=$(git describe --tags | cut -d- -f1,3 | tr -d v)
[ $HEAD != $UPSTREAM ] && APP_VERSION="${APP_VERSION}-alpha"
function set_sourceline { function set_sourceline {
if [ ${ARCH} == amd64 ]; then if [ ${ARCH} == amd64 ]; then

View File

@ -1,12 +1,43 @@
# PyBitmessage Installation Instructions # PyBitmessage Installation Instructions
- Binary (64bit, no separate installation of dependencies required) - Binary (64bit, no separate installation of dependencies required)
- Windows: https://download.bitmessage.org/snapshots/ - Windows: https://artifacts.bitmessage.at/winebuild/
- Linux AppImages: https://artifacts.bitmessage.at/appimage/ - Linux AppImages: https://artifacts.bitmessage.at/appimage/
- Linux snaps: https://artifacts.bitmessage.at/snap/ - Linux snaps: https://artifacts.bitmessage.at/snap/
- Mac (not up to date): https://github.com/Bitmessage/PyBitmessage/releases/tag/v0.6.1 - Mac (not up to date): https://github.com/Bitmessage/PyBitmessage/releases/tag/v0.6.1
- Source - Source
`git clone git://github.com/Bitmessage/PyBitmessage.git` `git clone git://github.com/Bitmessage/PyBitmessage.git`
## Notes on the AppImages
The [AppImage](https://docs.appimage.org/introduction/index.html)
is a bundle, built by the
[appimage-builder](https://github.com/AppImageCrafters/appimage-builder) from
the Ubuntu Bionic deb files, the sources and `bitmsghash.so`, precompiled for
3 architectures, using the `packages/AppImage/AppImageBuilder.yml` recipe.
When you run the appimage the bundle is loop mounted to a location like
`/tmp/.mount_PyBitm97wj4K` with `squashfs-tools`.
The appimage name has several informational filds:
```
PyBitmessage-<VERSION>-g<COMMITHASH>[-alpha]-<ARCH>.AppImage
```
E.g. `PyBitmessage-0.6.3.2-ge571ba8a-x86_64.AppImage` is an appimage, built from
the `v0.6` for x86_64 and `PyBitmessage-0.6.3.2-g9de2aaf1-alpha-aarch64.AppImage`
is one, built from some development branch for arm64.
You can also build the appimage with local code. For that you need installed
docker:
```
$ docker build -t bm-appimage -f .buildbot/appimage/Dockerfile .
$ docker run -t --rm -v "$(pwd)"/dist:/out bm-appimage .buildbot/appimage/build.sh
```
The appimages should be in the dist dir.
## Helper Script for building from source ## Helper Script for building from source
Go to the directory with PyBitmessage source code and run: Go to the directory with PyBitmessage source code and run:
``` ```

View File

@ -12,6 +12,7 @@ The PyBitmessage startup script
import os import os
import sys import sys
try: try:
import pathmagic import pathmagic
except ImportError: except ImportError:
@ -156,13 +157,6 @@ class Main(object):
set_thread_name("PyBitmessage") set_thread_name("PyBitmessage")
state.dandelion_enabled = config.safeGetInt('network', 'dandelion')
# dandelion requires outbound connections, without them,
# stem objects will get stuck forever
if state.dandelion_enabled and not config.safeGetBoolean(
'bitmessagesettings', 'sendoutgoingconnections'):
state.dandelion_enabled = 0
if state.testmode or config.safeGetBoolean( if state.testmode or config.safeGetBoolean(
'bitmessagesettings', 'extralowdifficulty'): 'bitmessagesettings', 'extralowdifficulty'):
defaults.networkDefaultProofOfWorkNonceTrialsPerByte = int( defaults.networkDefaultProofOfWorkNonceTrialsPerByte = int(

View File

@ -69,6 +69,9 @@ except ImportError:
get_plugins = False get_plugins = False
is_windows = sys.platform.startswith('win')
# TODO: rewrite # TODO: rewrite
def powQueueSize(): def powQueueSize():
"""Returns the size of queues.workerQueue including current unfinished work""" """Returns the size of queues.workerQueue including current unfinished work"""
@ -87,7 +90,7 @@ def openKeysFile():
keysfile = os.path.join(state.appdata, 'keys.dat') keysfile = os.path.join(state.appdata, 'keys.dat')
if 'linux' in sys.platform: if 'linux' in sys.platform:
subprocess.call(["xdg-open", keysfile]) subprocess.call(["xdg-open", keysfile])
elif sys.platform.startswith('win'): elif is_windows:
os.startfile(keysfile) # pylint: disable=no-member os.startfile(keysfile) # pylint: disable=no-member
@ -885,7 +888,7 @@ class MyForm(settingsmixin.SMainWindow):
""" """
startonlogon = config.safeGetBoolean( startonlogon = config.safeGetBoolean(
'bitmessagesettings', 'startonlogon') 'bitmessagesettings', 'startonlogon')
if sys.platform.startswith('win'): # Auto-startup for Windows if is_windows: # Auto-startup for Windows
RUN_PATH = "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" RUN_PATH = "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"
settings = QtCore.QSettings( settings = QtCore.QSettings(
RUN_PATH, QtCore.QSettings.NativeFormat) RUN_PATH, QtCore.QSettings.NativeFormat)
@ -4328,6 +4331,14 @@ class BitmessageQtApplication(QtGui.QApplication):
# Unique identifier for this application # Unique identifier for this application
uuid = '6ec0149b-96e1-4be1-93ab-1465fb3ebf7c' uuid = '6ec0149b-96e1-4be1-93ab-1465fb3ebf7c'
@staticmethod
def get_windowstyle():
"""Get window style set in config or default"""
return config.safeGet(
'bitmessagesettings', 'windowstyle',
'Windows' if is_windows else 'GTK+'
)
def __init__(self, *argv): def __init__(self, *argv):
super(BitmessageQtApplication, self).__init__(*argv) super(BitmessageQtApplication, self).__init__(*argv)
id = BitmessageQtApplication.uuid id = BitmessageQtApplication.uuid
@ -4336,6 +4347,14 @@ class BitmessageQtApplication(QtGui.QApplication):
QtCore.QCoreApplication.setOrganizationDomain("bitmessage.org") QtCore.QCoreApplication.setOrganizationDomain("bitmessage.org")
QtCore.QCoreApplication.setApplicationName("pybitmessageqt") QtCore.QCoreApplication.setApplicationName("pybitmessageqt")
self.setStyle(self.get_windowstyle())
font = config.safeGet('bitmessagesettings', 'font')
if font:
# family, size, weight = font.split(',')
family, size = font.split(',')
self.setFont(QtGui.QFont(family, int(size)))
self.server = None self.server = None
self.is_running = False self.is_running = False

View File

@ -61,7 +61,8 @@ class Ui_MainWindow(object):
self.tabWidget.setMinimumSize(QtCore.QSize(0, 0)) self.tabWidget.setMinimumSize(QtCore.QSize(0, 0))
self.tabWidget.setBaseSize(QtCore.QSize(0, 0)) self.tabWidget.setBaseSize(QtCore.QSize(0, 0))
font = QtGui.QFont() font = QtGui.QFont()
font.setPointSize(9) base_size = QtGui.QApplication.instance().font().pointSize()
font.setPointSize(int(base_size * 0.75))
self.tabWidget.setFont(font) self.tabWidget.setFont(font)
self.tabWidget.setTabPosition(QtGui.QTabWidget.North) self.tabWidget.setTabPosition(QtGui.QTabWidget.North)
self.tabWidget.setTabShape(QtGui.QTabWidget.Rounded) self.tabWidget.setTabShape(QtGui.QTabWidget.Rounded)

View File

@ -48,14 +48,17 @@ def getSOCKSProxyType(config):
class SettingsDialog(QtGui.QDialog): class SettingsDialog(QtGui.QDialog):
"""The "Settings" dialog""" """The "Settings" dialog"""
# pylint: disable=too-many-instance-attributes
def __init__(self, parent=None, firstrun=False): def __init__(self, parent=None, firstrun=False):
super(SettingsDialog, self).__init__(parent) super(SettingsDialog, self).__init__(parent)
widgets.load('settings.ui', self) widgets.load('settings.ui', self)
self.app = QtGui.QApplication.instance()
self.parent = parent self.parent = parent
self.firstrun = firstrun self.firstrun = firstrun
self.config = config_obj self.config = config_obj
self.net_restart_needed = False self.net_restart_needed = False
self.font_setting = None
self.timer = QtCore.QTimer() self.timer = QtCore.QTimer()
if self.config.safeGetBoolean('bitmessagesettings', 'dontconnect'): if self.config.safeGetBoolean('bitmessagesettings', 'dontconnect'):
@ -92,6 +95,15 @@ class SettingsDialog(QtGui.QDialog):
def adjust_from_config(self, config): def adjust_from_config(self, config):
"""Adjust all widgets state according to config settings""" """Adjust all widgets state according to config settings"""
# pylint: disable=too-many-branches,too-many-statements # pylint: disable=too-many-branches,too-many-statements
current_style = self.app.get_windowstyle()
for i, sk in enumerate(QtGui.QStyleFactory.keys()):
self.comboBoxStyle.addItem(sk)
if sk == current_style:
self.comboBoxStyle.setCurrentIndex(i)
self.save_font_setting(self.app.font())
if not self.parent.tray.isSystemTrayAvailable(): if not self.parent.tray.isSystemTrayAvailable():
self.groupBoxTray.setEnabled(False) self.groupBoxTray.setEnabled(False)
self.groupBoxTray.setTitle(_translate( self.groupBoxTray.setTitle(_translate(
@ -144,7 +156,7 @@ class SettingsDialog(QtGui.QDialog):
"MainWindow", "MainWindow",
"Tray notifications not yet supported on your OS.")) "Tray notifications not yet supported on your OS."))
if 'win' not in sys.platform and not self.parent.desktop: if not sys.platform.startswith('win') and not self.parent.desktop:
self.checkBoxStartOnLogon.setDisabled(True) self.checkBoxStartOnLogon.setDisabled(True)
self.checkBoxStartOnLogon.setText(_translate( self.checkBoxStartOnLogon.setText(_translate(
"MainWindow", "Start-on-login not yet supported on your OS.")) "MainWindow", "Start-on-login not yet supported on your OS."))
@ -329,6 +341,18 @@ class SettingsDialog(QtGui.QDialog):
if status == 'success': if status == 'success':
self.parent.namecoin = nc self.parent.namecoin = nc
def save_font_setting(self, font):
"""Save user font setting and set the buttonFont text"""
font_setting = (font.family(), font.pointSize())
self.buttonFont.setText('{} {}'.format(*font_setting))
self.font_setting = '{},{}'.format(*font_setting)
def choose_font(self):
"""Show the font selection dialog"""
font, valid = QtGui.QFontDialog.getFont()
if valid:
self.save_font_setting(font)
def accept(self): def accept(self):
"""A callback for accepted event of buttonBox (OK button pressed)""" """A callback for accepted event of buttonBox (OK button pressed)"""
# pylint: disable=too-many-branches,too-many-statements # pylint: disable=too-many-branches,too-many-statements
@ -355,6 +379,20 @@ class SettingsDialog(QtGui.QDialog):
self.config.set('bitmessagesettings', 'replybelow', str( self.config.set('bitmessagesettings', 'replybelow', str(
self.checkBoxReplyBelow.isChecked())) self.checkBoxReplyBelow.isChecked()))
window_style = str(self.comboBoxStyle.currentText())
if self.app.get_windowstyle() != window_style or self.config.safeGet(
'bitmessagesettings', 'font'
) != self.font_setting:
self.config.set('bitmessagesettings', 'windowstyle', window_style)
self.config.set('bitmessagesettings', 'font', self.font_setting)
queues.UISignalQueue.put((
'updateStatusBar', (
_translate(
"MainWindow",
"You need to restart the application to apply"
" the window style or default font."), 1)
))
lang = str(self.languageComboBox.itemData( lang = str(self.languageComboBox.itemData(
self.languageComboBox.currentIndex())) self.languageComboBox.currentIndex()))
self.config.set('bitmessagesettings', 'userlocale', lang) self.config.set('bitmessagesettings', 'userlocale', lang)

View File

@ -147,6 +147,32 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="9" column="0">
<widget class="QGroupBox" name="groupBoxStyle">
<property name="title">
<string>Custom Style</string>
</property>
<layout class="QHBoxLayout">
<item>
<widget class="QComboBox" name="comboBoxStyle">
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="buttonFont">
<property name="text">
<string>Font</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="9" column="1"> <item row="9" column="1">
<widget class="QGroupBox" name="groupBox"> <widget class="QGroupBox" name="groupBox">
<property name="title"> <property name="title">
@ -1202,5 +1228,11 @@
</hint> </hint>
</hints> </hints>
</connection> </connection>
<connection>
<sender>buttonFont</sender>
<signal>clicked()</signal>
<receiver>settingsDialog</receiver>
<slot>choose_font</slot>
</connection>
</connections> </connections>
</ui> </ui>

View File

@ -5,15 +5,20 @@ import sys
import unittest import unittest
from PyQt4 import QtCore, QtGui from PyQt4 import QtCore, QtGui
from six.moves import queue
import bitmessageqt import bitmessageqt
import queues from bitmessageqt import _translate, config, queues
from tr import _translate
class TestBase(unittest.TestCase): class TestBase(unittest.TestCase):
"""Base class for bitmessageqt test case""" """Base class for bitmessageqt test case"""
@classmethod
def setUpClass(cls):
"""Provide the UI test cases with common settings"""
cls.config = config
def setUp(self): def setUp(self):
self.app = ( self.app = (
QtGui.QApplication.instance() QtGui.QApplication.instance()
@ -24,14 +29,21 @@ class TestBase(unittest.TestCase):
self.window.appIndicatorInit(self.app) self.window.appIndicatorInit(self.app)
def tearDown(self): def tearDown(self):
"""Search for exceptions in closures called by timer and fail if any"""
# self.app.deleteLater() # self.app.deleteLater()
concerning = []
while True: while True:
try: try:
thread, exc = queues.excQueue.get(block=False) thread, exc = queues.excQueue.get(block=False)
except Queue.Empty: except queue.Empty:
return break
if thread == 'tests': if thread == 'tests':
self.fail('Exception in the main thread: %s' % exc) concerning.append(exc)
if concerning:
self.fail(
'Exceptions found in the main thread:\n%s' % '\n'.join((
str(e) for e in concerning
)))
class TestMain(unittest.TestCase): class TestMain(unittest.TestCase):

View File

@ -1,10 +1,14 @@
"""Tests for PyBitmessage settings"""
import threading import threading
import time import time
from .main import TestBase from PyQt4 import QtCore, QtGui, QtTest
from bmconfigparser import config from bmconfigparser import config
from bitmessageqt import settings from bitmessageqt import settings
from .main import TestBase
class TestSettings(TestBase): class TestSettings(TestBase):
"""A test case for the "Settings" dialog""" """A test case for the "Settings" dialog"""
@ -14,8 +18,7 @@ class TestSettings(TestBase):
def test_udp(self): def test_udp(self):
"""Test the effect of checkBoxUDP""" """Test the effect of checkBoxUDP"""
udp_setting = config.safeGetBoolean( udp_setting = config.safeGetBoolean('bitmessagesettings', 'udp')
'bitmessagesettings', 'udp')
self.assertEqual(udp_setting, self.dialog.checkBoxUDP.isChecked()) self.assertEqual(udp_setting, self.dialog.checkBoxUDP.isChecked())
self.dialog.checkBoxUDP.setChecked(not udp_setting) self.dialog.checkBoxUDP.setChecked(not udp_setting)
self.dialog.accept() self.dialog.accept()
@ -32,3 +35,44 @@ class TestSettings(TestBase):
else: else:
if not udp_setting: if not udp_setting:
self.fail('No Announcer thread found while udp set to True') self.fail('No Announcer thread found while udp set to True')
def test_styling(self):
"""Test custom windows style and font"""
style_setting = config.safeGet('bitmessagesettings', 'windowstyle')
font_setting = config.safeGet('bitmessagesettings', 'font')
self.assertIs(style_setting, None)
self.assertIs(font_setting, None)
style_control = self.dialog.comboBoxStyle
self.assertEqual(
style_control.currentText(), self.app.get_windowstyle())
def call_font_dialog():
"""A function to get the open font dialog and accept it"""
font_dialog = QtGui.QApplication.activeModalWidget()
self.assertTrue(isinstance(font_dialog, QtGui.QFontDialog))
selected_font = font_dialog.currentFont()
self.assertEqual(
config.safeGet('bitmessagesettings', 'font'), '{},{}'.format(
selected_font.family(), selected_font.pointSize()))
font_dialog.accept()
self.dialog.accept()
self.assertEqual(
config.safeGet('bitmessagesettings', 'windowstyle'),
style_control.currentText())
def click_font_button():
"""Use QtTest to click the button"""
QtTest.QTest.mouseClick(
self.dialog.buttonFont, QtCore.Qt.LeftButton)
style_count = style_control.count()
self.assertGreater(style_count, 1)
for i in range(style_count):
if i != style_control.currentIndex():
style_control.setCurrentIndex(i)
break
QtCore.QTimer.singleShot(30, click_font_button)
QtCore.QTimer.singleShot(60, call_font_dialog)
time.sleep(2)

View File

@ -1,9 +1,10 @@
""" """
Network subsystem package Network subsystem package
""" """
from .dandelion import Dandelion
from .threads import StoppableThread from .threads import StoppableThread
dandelion_ins = Dandelion()
__all__ = ["StoppableThread"] __all__ = ["StoppableThread"]
@ -21,6 +22,11 @@ def start(config, state):
from .receivequeuethread import ReceiveQueueThread from .receivequeuethread import ReceiveQueueThread
from .uploadthread import UploadThread from .uploadthread import UploadThread
# check and set dandelion enabled value at network startup
dandelion_ins.init_dandelion_enabled(config)
# pass pool instance into dandelion class instance
dandelion_ins.init_pool(connectionpool.pool)
readKnownNodes() readKnownNodes()
connectionpool.pool.connectToStream(1) connectionpool.pool.connectToStream(1)
for thread in ( for thread in (

View File

@ -1,11 +1,11 @@
""" """
Announce addresses as they are received from other hosts Announce addresses as they are received from other hosts
""" """
import random
from six.moves import queue from six.moves import queue
# magic imports! # magic imports!
from network import connectionpool from network import connectionpool
from helper_random import randomshuffle
from protocol import assembleAddrMessage from protocol import assembleAddrMessage
from queues import addrQueue # FIXME: init with queue from queues import addrQueue # FIXME: init with queue
@ -29,9 +29,9 @@ class AddrThread(StoppableThread):
if chunk: if chunk:
# Choose peers randomly # Choose peers randomly
connections = connectionpool.pool.establishedConnections() connections = connectionpool.pool.establishedConnections()
randomshuffle(connections) random.shuffle(connections)
for i in connections: for i in connections:
randomshuffle(chunk) random.shuffle(chunk)
filtered = [] filtered = []
for stream, peer, seen, destination in chunk: for stream, peer, seen, destination in chunk:
# peer's own address or address received from peer # peer's own address or address received from peer

View File

@ -9,6 +9,7 @@ Basic infrastructure for asynchronous socket service clients and servers.
import os import os
import select import select
import socket import socket
import random
import sys import sys
import time import time
import warnings import warnings
@ -20,7 +21,6 @@ from errno import (
from threading import current_thread from threading import current_thread
from six.moves.reprlib import repr from six.moves.reprlib import repr
import helper_random
try: try:
from errno import WSAEWOULDBLOCK from errno import WSAEWOULDBLOCK
@ -234,13 +234,13 @@ def select_poller(timeout=0.0, map=None):
if err.args[0] in (WSAENOTSOCK, ): if err.args[0] in (WSAENOTSOCK, ):
return return
for fd in helper_random.randomsample(r, len(r)): for fd in random.sample(r, len(r)):
obj = map.get(fd) obj = map.get(fd)
if obj is None: if obj is None:
continue continue
read(obj) read(obj)
for fd in helper_random.randomsample(w, len(w)): for fd in random.sample(w, len(w)):
obj = map.get(fd) obj = map.get(fd)
if obj is None: if obj is None:
continue continue
@ -298,7 +298,7 @@ def poll_poller(timeout=0.0, map=None):
except socket.error as err: except socket.error as err:
if err.args[0] in (EBADF, WSAENOTSOCK, EINTR): if err.args[0] in (EBADF, WSAENOTSOCK, EINTR):
return return
for fd, flags in helper_random.randomsample(r, len(r)): for fd, flags in random.sample(r, len(r)):
obj = map.get(fd) obj = map.get(fd)
if obj is None: if obj is None:
continue continue
@ -358,7 +358,7 @@ def epoll_poller(timeout=0.0, map=None):
if err.args[0] != EINTR: if err.args[0] != EINTR:
raise raise
r = [] r = []
for fd, flags in helper_random.randomsample(r, len(r)): for fd, flags in random.sample(r, len(r)):
obj = map.get(fd) obj = map.get(fd)
if obj is None: if obj is None:
continue continue
@ -421,7 +421,7 @@ def kqueue_poller(timeout=0.0, map=None):
events = kqueue_poller.pollster.control(updates, selectables, timeout) events = kqueue_poller.pollster.control(updates, selectables, timeout)
if len(events) > 1: if len(events) > 1:
events = helper_random.randomsample(events, len(events)) events = random.sample(events, len(events))
for event in events: for event in events:
fd = event.ident fd = event.ident

View File

@ -7,7 +7,7 @@ import time
import protocol import protocol
import state import state
import network.connectionpool # use long name to address recursive import import network.connectionpool # use long name to address recursive import
from network import dandelion from network import dandelion_ins
from highlevelcrypto import calculateInventoryHash from highlevelcrypto import calculateInventoryHash
logger = logging.getLogger('default') logger = logging.getLogger('default')
@ -113,7 +113,7 @@ class BMObject(object): # pylint: disable=too-many-instance-attributes
or advertise it unnecessarily) or advertise it unnecessarily)
""" """
# if it's a stem duplicate, pretend we don't have it # if it's a stem duplicate, pretend we don't have it
if dandelion.instance.hasHash(self.inventoryHash): if dandelion_ins.hasHash(self.inventoryHash):
return return
if self.inventoryHash in state.Inventory: if self.inventoryHash in state.Inventory:
raise BMObjectAlreadyHaveError() raise BMObjectAlreadyHaveError()

View File

@ -17,7 +17,6 @@ from network import knownnodes
import protocol import protocol
import state import state
import network.connectionpool # use long name to address recursive import import network.connectionpool # use long name to address recursive import
from network import dandelion
from bmconfigparser import config from bmconfigparser import config
from queues import invQueue, objectProcessorQueue, portCheckerQueue from queues import invQueue, objectProcessorQueue, portCheckerQueue
from randomtrackingdict import RandomTrackingDict from randomtrackingdict import RandomTrackingDict
@ -29,6 +28,7 @@ from network.bmobject import (
) )
from network.proxy import ProxyError from network.proxy import ProxyError
from network import dandelion_ins
from .node import Node, Peer from .node import Node, Peer
from .objectracker import ObjectTracker, missingObjects from .objectracker import ObjectTracker, missingObjects
@ -364,14 +364,14 @@ class BMProto(AdvancedDispatcher, ObjectTracker):
raise BMProtoExcessiveDataError() raise BMProtoExcessiveDataError()
# ignore dinv if dandelion turned off # ignore dinv if dandelion turned off
if extend_dandelion_stem and not state.dandelion_enabled: if extend_dandelion_stem and not dandelion_ins.enabled:
return True return True
for i in items: for i in items:
if i in state.Inventory and not dandelion.instance.hasHash(i): if i in state.Inventory and not dandelion_ins.hasHash(i):
continue continue
if extend_dandelion_stem and not dandelion.instance.hasHash(i): if extend_dandelion_stem and not dandelion_ins.hasHash(i):
dandelion.instance.addHash(i, self) dandelion_ins.addHash(i, self)
self.handleReceivedInventory(i) self.handleReceivedInventory(i)
return True return True
@ -437,9 +437,9 @@ class BMProto(AdvancedDispatcher, ObjectTracker):
except KeyError: except KeyError:
pass pass
if self.object.inventoryHash in state.Inventory and dandelion.instance.hasHash( if self.object.inventoryHash in state.Inventory and dandelion_ins.hasHash(
self.object.inventoryHash): self.object.inventoryHash):
dandelion.instance.removeHash( dandelion_ins.removeHash(
self.object.inventoryHash, "cycle detection") self.object.inventoryHash, "cycle detection")
if six.PY2: if six.PY2:
@ -563,7 +563,7 @@ class BMProto(AdvancedDispatcher, ObjectTracker):
if not self.isOutbound: if not self.isOutbound:
self.append_write_buf(protocol.assembleVersionMessage( self.append_write_buf(protocol.assembleVersionMessage(
self.destination.host, self.destination.port, self.destination.host, self.destination.port,
network.connectionpool.pool.streams, True, network.connectionpool.pool.streams, dandelion_ins.enabled, True,
nodeid=self.nodeid)) nodeid=self.nodeid))
logger.debug( logger.debug(
'%(host)s:%(port)i sending version', '%(host)s:%(port)i sending version',

View File

@ -7,9 +7,9 @@ import re
import socket import socket
import sys import sys
import time import time
import random
from network import asyncore_pollchoose as asyncore from network import asyncore_pollchoose as asyncore
import helper_random
from network import knownnodes from network import knownnodes
import protocol import protocol
import state import state
@ -216,7 +216,7 @@ class BMConnectionPool(object):
connection_base = TCPConnection connection_base = TCPConnection
elif proxy_type == 'SOCKS5': elif proxy_type == 'SOCKS5':
connection_base = Socks5BMConnection connection_base = Socks5BMConnection
hostname = helper_random.randomchoice([ hostname = random.choice([ # nosec B311
'quzwelsuziwqgpt2.onion', None 'quzwelsuziwqgpt2.onion', None
]) ])
elif proxy_type == 'SOCKS4a': elif proxy_type == 'SOCKS4a':
@ -228,7 +228,7 @@ class BMConnectionPool(object):
bootstrapper = bootstrap(connection_base) bootstrapper = bootstrap(connection_base)
if not hostname: if not hostname:
port = helper_random.randomchoice([8080, 8444]) port = random.choice([8080, 8444]) # nosec B311
hostname = 'bootstrap%s.bitmessage.org' % port hostname = 'bootstrap%s.bitmessage.org' % port
else: else:
port = 8444 port = 8444
@ -295,7 +295,7 @@ class BMConnectionPool(object):
state.maximumNumberOfHalfOpenConnections - pending): state.maximumNumberOfHalfOpenConnections - pending):
try: try:
chosen = self.trustedPeer or chooseConnection( chosen = self.trustedPeer or chooseConnection(
helper_random.randomchoice(self.streams)) random.choice(self.streams)) # nosec B311
except ValueError: except ValueError:
continue continue
if chosen in self.outboundConnections: if chosen in self.outboundConnections:

View File

@ -9,9 +9,6 @@ from time import time
import six import six
from binascii import hexlify from binascii import hexlify
import network.connectionpool # use long name to address recursive import
import state
from queues import invQueue
# randomise routes after 600 seconds # randomise routes after 600 seconds
REASSIGN_INTERVAL = 600 REASSIGN_INTERVAL = 600
@ -39,6 +36,8 @@ class Dandelion: # pylint: disable=old-style-class
# when to rerandomise routes # when to rerandomise routes
self.refresh = time() + REASSIGN_INTERVAL self.refresh = time() + REASSIGN_INTERVAL
self.lock = RLock() self.lock = RLock()
self.enabled = None
self.pool = None
@staticmethod @staticmethod
def poissonTimeout(start=None, average=0): def poissonTimeout(start=None, average=0):
@ -49,10 +48,23 @@ class Dandelion: # pylint: disable=old-style-class
average = FLUFF_TRIGGER_MEAN_DELAY average = FLUFF_TRIGGER_MEAN_DELAY
return start + expovariate(1.0 / average) + FLUFF_TRIGGER_FIXED_DELAY return start + expovariate(1.0 / average) + FLUFF_TRIGGER_FIXED_DELAY
def init_pool(self, pool):
"""pass pool instance"""
self.pool = pool
def init_dandelion_enabled(self, config):
"""Check if Dandelion is enabled and set value in enabled attribute"""
dandelion_enabled = config.safeGetInt('network', 'dandelion')
# dandelion requires outbound connections, without them,
# stem objects will get stuck forever
if not config.safeGetBoolean(
'bitmessagesettings', 'sendoutgoingconnections'):
dandelion_enabled = 0
self.enabled = dandelion_enabled
def addHash(self, hashId, source=None, stream=1): def addHash(self, hashId, source=None, stream=1):
"""Add inventory vector to dandelion stem""" """Add inventory vector to dandelion stem return status of dandelion enabled"""
if not state.dandelion_enabled: assert self.enabled is not None
return
with self.lock: with self.lock:
self.hashMap[bytes(hashId)] = Stem( self.hashMap[bytes(hashId)] = Stem(
self.getNodeStem(source), self.getNodeStem(source),
@ -92,7 +104,7 @@ class Dandelion: # pylint: disable=old-style-class
"""Child (i.e. next) node for an inventory vector during stem mode""" """Child (i.e. next) node for an inventory vector during stem mode"""
return self.hashMap[bytes(hashId)].child return self.hashMap[bytes(hashId)].child
def maybeAddStem(self, connection): def maybeAddStem(self, connection, invQueue):
""" """
If we had too few outbound connections, add the current one to the If we had too few outbound connections, add the current one to the
current stem list. Dandelion as designed by the authors should current stem list. Dandelion as designed by the authors should
@ -166,7 +178,7 @@ class Dandelion: # pylint: disable=old-style-class
self.nodeMap[node] = self.pickStem(node) self.nodeMap[node] = self.pickStem(node)
return self.nodeMap[node] return self.nodeMap[node]
def expire(self): def expire(self, invQueue):
"""Switch expired objects from stem to fluff mode""" """Switch expired objects from stem to fluff mode"""
with self.lock: with self.lock:
deadline = time() deadline = time()
@ -182,19 +194,18 @@ class Dandelion: # pylint: disable=old-style-class
def reRandomiseStems(self): def reRandomiseStems(self):
"""Re-shuffle stem mapping (parent <-> child pairs)""" """Re-shuffle stem mapping (parent <-> child pairs)"""
assert self.pool is not None
if self.refresh > time():
return
with self.lock: with self.lock:
try: try:
# random two connections # random two connections
self.stem = sample( self.stem = sample(
sorted(network.connectionpool.BMConnectionPool( sorted(self.pool.outboundConnections.values()), MAX_STEMS)
).outboundConnections.values()), MAX_STEMS)
# not enough stems available # not enough stems available
except ValueError: except ValueError:
self.stem = list(network.connectionpool.BMConnectionPool( self.stem = list(self.pool.outboundConnections.values())
).outboundConnections.values())
self.nodeMap = {} self.nodeMap = {}
# hashMap stays to cater for pending stems # hashMap stays to cater for pending stems
self.refresh = time() + REASSIGN_INTERVAL self.refresh = time() + REASSIGN_INTERVAL
instance = Dandelion()

View File

@ -2,15 +2,15 @@
`DownloadThread` class definition `DownloadThread` class definition
""" """
import time import time
import random
import state import state
import six import six
import addresses import addresses
import helper_random
import protocol import protocol
from network import connectionpool from network import connectionpool
from network import dandelion_ins
from .objectracker import missingObjects from .objectracker import missingObjects
from .threads import StoppableThread from .threads import StoppableThread
from network import dandelion
class DownloadThread(StoppableThread): class DownloadThread(StoppableThread):
@ -44,7 +44,7 @@ class DownloadThread(StoppableThread):
requested = 0 requested = 0
# Choose downloading peers randomly # Choose downloading peers randomly
connections = connectionpool.pool.establishedConnections() connections = connectionpool.pool.establishedConnections()
helper_random.randomshuffle(connections) random.shuffle(connections)
requestChunk = max(int( requestChunk = max(int(
min(self.maxRequestChunk, len(missingObjects)) min(self.maxRequestChunk, len(missingObjects))
/ len(connections)), 1) if connections else 1 / len(connections)), 1) if connections else 1
@ -61,7 +61,7 @@ class DownloadThread(StoppableThread):
payload = bytearray() payload = bytearray()
chunkCount = 0 chunkCount = 0
for chunk in request: for chunk in request:
if chunk in state.Inventory and not dandelion.instance.hasHash(chunk): if chunk in state.Inventory and not dandelion_ins.hasHash(chunk):
try: try:
del i.objectsNewToMe[chunk] del i.objectsNewToMe[chunk]
except KeyError: except KeyError:

View File

@ -9,7 +9,7 @@ import addresses
import protocol import protocol
import state import state
from network import connectionpool from network import connectionpool
from network import dandelion from network import dandelion_ins
from queues import invQueue from queues import invQueue
from .threads import StoppableThread from .threads import StoppableThread
@ -40,10 +40,10 @@ class InvThread(StoppableThread):
@staticmethod @staticmethod
def handleLocallyGenerated(stream, hashId): def handleLocallyGenerated(stream, hashId):
"""Locally generated inventory items require special handling""" """Locally generated inventory items require special handling"""
dandelion.instance.addHash(hashId, stream=stream) dandelion_ins.addHash(hashId, stream=stream)
for connection in connectionpool.pool.connections(): for connection in connectionpool.pool.connections():
if state.dandelion_enabled and connection != \ if dandelion_ins.enabled and connection != \
dandelion.instance.objectChildStem(hashId): dandelion_ins.objectChildStem(hashId):
continue continue
connection.objectsNewToThem[hashId] = time() connection.objectsNewToThem[hashId] = time()
@ -52,7 +52,7 @@ class InvThread(StoppableThread):
chunk = [] chunk = []
while True: while True:
# Dandelion fluff trigger by expiration # Dandelion fluff trigger by expiration
handleExpiredDandelion(dandelion.instance.expire()) handleExpiredDandelion(dandelion_ins.expire(invQueue))
try: try:
data = invQueue.get(False) data = invQueue.get(False)
chunk.append((data[0], data[1])) chunk.append((data[0], data[1]))
@ -75,10 +75,10 @@ class InvThread(StoppableThread):
except KeyError: except KeyError:
continue continue
try: try:
if connection == dandelion.instance.objectChildStem(inv[1]): if connection == dandelion_ins.objectChildStem(inv[1]):
# Fluff trigger by RNG # Fluff trigger by RNG
# auto-ignore if config set to 0, i.e. dandelion is off # auto-ignore if config set to 0, i.e. dandelion is off
if random.randint(1, 100) >= state.dandelion_enabled: # nosec B311 if random.randint(1, 100) >= dandelion_ins.enabled: # nosec B311
fluffs.append(inv[1]) fluffs.append(inv[1])
# send a dinv only if the stem node supports dandelion # send a dinv only if the stem node supports dandelion
elif connection.services & protocol.NODE_DANDELION > 0: elif connection.services & protocol.NODE_DANDELION > 0:
@ -105,7 +105,6 @@ class InvThread(StoppableThread):
for _ in range(len(chunk)): for _ in range(len(chunk)):
invQueue.task_done() invQueue.task_done()
if dandelion.instance.refresh < time(): dandelion_ins.reRandomiseStems()
dandelion.instance.reRandomiseStems()
self.stop.wait(1) self.stop.wait(1)

View File

@ -6,7 +6,7 @@ from threading import RLock
import six import six
import network.connectionpool # use long name to address recursive import import network.connectionpool # use long name to address recursive import
from network import dandelion from network import dandelion_ins
from randomtrackingdict import RandomTrackingDict from randomtrackingdict import RandomTrackingDict
haveBloom = False haveBloom = False
@ -111,14 +111,14 @@ class ObjectTracker(object):
del i.objectsNewToMe[hashid] del i.objectsNewToMe[hashid]
except KeyError: except KeyError:
if streamNumber in i.streams and ( if streamNumber in i.streams and (
not dandelion.instance.hasHash(hashid) not dandelion_ins.hasHash(hashid)
or dandelion.instance.objectChildStem(hashid) == i): or dandelion_ins.objectChildStem(hashid) == i):
with i.objectsNewToThemLock: with i.objectsNewToThemLock:
i.objectsNewToThem[hashid_bytes] = time.time() i.objectsNewToThem[hashid_bytes] = time.time()
# update stream number, # update stream number,
# which we didn't have when we just received the dinv # which we didn't have when we just received the dinv
# also resets expiration of the stem mode # also resets expiration of the stem mode
dandelion.instance.setHashStream(hashid, streamNumber) dandelion_ins.setHashStream(hashid, streamNumber)
if i == self: if i == self:
try: try:

View File

@ -12,14 +12,13 @@ import six
# magic imports! # magic imports!
import addresses import addresses
import helper_random
import l10n import l10n
import protocol import protocol
import state import state
import network.connectionpool # use long name to address recursive import import network.connectionpool # use long name to address recursive import
from network import dandelion
from bmconfigparser import config from bmconfigparser import config
from highlevelcrypto import randomBytes from highlevelcrypto import randomBytes
from network import dandelion_ins
from queues import invQueue, receiveDataQueue, UISignalQueue from queues import invQueue, receiveDataQueue, UISignalQueue
from tr import _translate from tr import _translate
@ -176,7 +175,7 @@ class TCPConnection(BMProto, TLSDispatcher):
knownnodes.increaseRating(self.destination) knownnodes.increaseRating(self.destination)
knownnodes.addKnownNode( knownnodes.addKnownNode(
self.streams, self.destination, time.time()) self.streams, self.destination, time.time())
dandelion.instance.maybeAddStem(self) dandelion_ins.maybeAddStem(self, invQueue)
self.sendAddr() self.sendAddr()
self.sendBigInv() self.sendBigInv()
@ -208,7 +207,7 @@ class TCPConnection(BMProto, TLSDispatcher):
elemCount = min( elemCount = min(
len(filtered), len(filtered),
maxAddrCount / 2 if n else maxAddrCount) maxAddrCount / 2 if n else maxAddrCount)
addrs[s] = helper_random.randomsample(filtered, elemCount) addrs[s] = random.sample(filtered, elemCount)
for substream in addrs: for substream in addrs:
for peer, params in addrs[substream]: for peer, params in addrs[substream]:
templist.append((substream, peer, params["lastseen"])) templist.append((substream, peer, params["lastseen"]))
@ -238,7 +237,7 @@ class TCPConnection(BMProto, TLSDispatcher):
with self.objectsNewToThemLock: with self.objectsNewToThemLock:
for objHash in state.Inventory.unexpired_hashes_by_stream(stream): for objHash in state.Inventory.unexpired_hashes_by_stream(stream):
# don't advertise stem objects on bigInv # don't advertise stem objects on bigInv
if dandelion.instance.hasHash(objHash): if dandelion_ins.hasHash(objHash):
continue continue
bigInvList[objHash] = 0 bigInvList[objHash] = 0
objectCount = 0 objectCount = 0
@ -275,7 +274,7 @@ class TCPConnection(BMProto, TLSDispatcher):
self.append_write_buf( self.append_write_buf(
protocol.assembleVersionMessage( protocol.assembleVersionMessage(
self.destination.host, self.destination.port, self.destination.host, self.destination.port,
network.connectionpool.pool.streams, network.connectionpool.pool.streams, dandelion_ins.enabled,
False, nodeid=self.nodeid)) False, nodeid=self.nodeid))
self.connectedAt = time.time() self.connectedAt = time.time()
receiveDataQueue.put(self.destination) receiveDataQueue.put(self.destination)
@ -300,7 +299,7 @@ class TCPConnection(BMProto, TLSDispatcher):
if host_is_global: if host_is_global:
knownnodes.addKnownNode( knownnodes.addKnownNode(
self.streams, self.destination, time.time()) self.streams, self.destination, time.time())
dandelion.instance.maybeRemoveStem(self) dandelion_ins.maybeRemoveStem(self)
else: else:
self.checkTimeOffsetNotification() self.checkTimeOffsetNotification()
if host_is_global: if host_is_global:
@ -326,7 +325,7 @@ class Socks5BMConnection(Socks5Connection, TCPConnection):
self.append_write_buf( self.append_write_buf(
protocol.assembleVersionMessage( protocol.assembleVersionMessage(
self.destination.host, self.destination.port, self.destination.host, self.destination.port,
network.connectionpool.pool.streams, network.connectionpool.pool.streams, dandelion_ins.enabled,
False, nodeid=self.nodeid)) False, nodeid=self.nodeid))
self.set_state("bm_header", expectBytes=protocol.Header.size) self.set_state("bm_header", expectBytes=protocol.Header.size)
return True return True
@ -350,7 +349,7 @@ class Socks4aBMConnection(Socks4aConnection, TCPConnection):
self.append_write_buf( self.append_write_buf(
protocol.assembleVersionMessage( protocol.assembleVersionMessage(
self.destination.host, self.destination.port, self.destination.host, self.destination.port,
network.connectionpool.pool.streams, network.connectionpool.pool.streams, dandelion_ins.enabled,
False, nodeid=self.nodeid)) False, nodeid=self.nodeid))
self.set_state("bm_header", expectBytes=protocol.Header.size) self.set_state("bm_header", expectBytes=protocol.Header.size)
return True return True

View File

@ -3,12 +3,12 @@
""" """
import time import time
import helper_random import random
import protocol import protocol
import state import state
from network import connectionpool from network import connectionpool
from network import dandelion
from randomtrackingdict import RandomTrackingDict from randomtrackingdict import RandomTrackingDict
from network import dandelion_ins
from .threads import StoppableThread from .threads import StoppableThread
@ -24,7 +24,7 @@ class UploadThread(StoppableThread):
uploaded = 0 uploaded = 0
# Choose uploading peers randomly # Choose uploading peers randomly
connections = connectionpool.pool.establishedConnections() connections = connectionpool.pool.establishedConnections()
helper_random.randomshuffle(connections) random.shuffle(connections)
for i in connections: for i in connections:
now = time.time() now = time.time()
# avoid unnecessary delay # avoid unnecessary delay
@ -41,8 +41,8 @@ class UploadThread(StoppableThread):
chunk_count = 0 chunk_count = 0
for chunk in request: for chunk in request:
del i.pendingUpload[chunk] del i.pendingUpload[chunk]
if dandelion.instance.hasHash(chunk) and \ if dandelion_ins.hasHash(chunk) and \
i != dandelion.instance.objectChildStem(chunk): i != dandelion_ins.objectChildStem(chunk):
i.antiIntersectionDelay() i.antiIntersectionDelay()
self.logger.info( self.logger.info(
'%s asked for a stem object we didn\'t offer to it.', '%s asked for a stem object we didn\'t offer to it.',

View File

@ -339,8 +339,8 @@ def assembleAddrMessage(peerList):
return retval return retval
def assembleVersionMessage( def assembleVersionMessage( # pylint: disable=too-many-arguments
remoteHost, remotePort, participatingStreams, server=False, nodeid=None remoteHost, remotePort, participatingStreams, dandelion_enabled=True, server=False, nodeid=None,
): ):
""" """
Construct the payload of a version message, Construct the payload of a version message,
@ -353,7 +353,7 @@ def assembleVersionMessage(
'>q', '>q',
NODE_NETWORK NODE_NETWORK
| (NODE_SSL if haveSSL(server) else 0) | (NODE_SSL if haveSSL(server) else 0)
| (NODE_DANDELION if state.dandelion_enabled else 0) | (NODE_DANDELION if dandelion_enabled else 0)
) )
payload += pack('>q', int(time.time())) payload += pack('>q', int(time.time()))
@ -377,7 +377,7 @@ def assembleVersionMessage(
'>q', '>q',
NODE_NETWORK NODE_NETWORK
| (NODE_SSL if haveSSL(server) else 0) | (NODE_SSL if haveSSL(server) else 0)
| (NODE_DANDELION if state.dandelion_enabled else 0) | (NODE_DANDELION if dandelion_enabled else 0)
) )
# = 127.0.0.1. This will be ignored by the remote host. # = 127.0.0.1. This will be ignored by the remote host.
# The actual remote connected IP will be used. # The actual remote connected IP will be used.

View File

@ -43,8 +43,6 @@ ownAddresses = {}
discoveredPeers = {} discoveredPeers = {}
dandelion_enabled = 0
kivy = False kivy = False
kivyapp = None kivyapp = None

View File

@ -322,16 +322,17 @@ class TestCore(unittest.TestCase):
def test_version(self): def test_version(self):
"""check encoding/decoding of the version message""" """check encoding/decoding of the version message"""
dandelion_enabled = True
# with single stream # with single stream
msg = protocol.assembleVersionMessage('127.0.0.1', 8444, [1]) msg = protocol.assembleVersionMessage('127.0.0.1', 8444, [1], dandelion_enabled)
decoded = self._decode_msg(msg, "IQQiiQlsLv") decoded = self._decode_msg(msg, "IQQiiQlsLv")
peer, _, ua, streams = self._decode_msg(msg, "IQQiiQlsLv")[4:] peer, _, ua, streams = self._decode_msg(msg, "IQQiiQlsLv")[4:]
self.assertEqual( self.assertEqual(
peer, Node(11 if state.dandelion_enabled else 3, '127.0.0.1', 8444)) peer, Node(11 if dandelion_enabled else 3, '127.0.0.1', 8444))
self.assertEqual(ua, '/PyBitmessage:' + softwareVersion + '/') self.assertEqual(ua, '/PyBitmessage:' + softwareVersion + '/')
self.assertEqual(streams, [1]) self.assertEqual(streams, [1])
# with multiple streams # with multiple streams
msg = protocol.assembleVersionMessage('127.0.0.1', 8444, [1, 2, 3]) msg = protocol.assembleVersionMessage('127.0.0.1', 8444, [1, 2, 3], dandelion_enabled)
decoded = self._decode_msg(msg, "IQQiiQlslv") decoded = self._decode_msg(msg, "IQQiiQlslv")
peer, _, ua = decoded[4:7] peer, _, ua = decoded[4:7]
streams = decoded[7:] streams = decoded[7:]

View File

@ -2,8 +2,8 @@
from binascii import unhexlify from binascii import unhexlify
from six.moves import queue
import six import six
from six.moves import queue
from .partial import TestPartialRun from .partial import TestPartialRun
from .samples import ( from .samples import (

View File

@ -5,12 +5,11 @@ Tests using API.
import base64 import base64
import json import json
import time import time
from binascii import hexlify from binascii import hexlify
from six.moves import xmlrpc_client # nosec
import six
import psutil import psutil
import six
from six.moves import xmlrpc_client # nosec
from .samples import ( from .samples import (
sample_deterministic_addr3, sample_deterministic_addr4, sample_seed, sample_deterministic_addr3, sample_deterministic_addr4, sample_seed,
@ -181,29 +180,29 @@ class TestAPI(TestAPIProto):
self.assertEqual( self.assertEqual(
self.api.getDeterministicAddress(self._seed, 3, 1), self.api.getDeterministicAddress(self._seed, 3, 1),
sample_deterministic_addr3) sample_deterministic_addr3)
six.assertRegex(self, six.assertRegex(
self.api.getDeterministicAddress(self._seed, 2, 1), self, self.api.getDeterministicAddress(self._seed, 2, 1),
r'^API Error 0002:') r'^API Error 0002:')
# This is here until the streams will be implemented # This is here until the streams will be implemented
six.assertRegex(self, six.assertRegex(
self.api.getDeterministicAddress(self._seed, 3, 2), self, self.api.getDeterministicAddress(self._seed, 3, 2),
r'API Error 0003:') r'API Error 0003:')
six.assertRegex(self, six.assertRegex(
self.api.createDeterministicAddresses(self._seed, 1, 4, 2), self, self.api.createDeterministicAddresses(self._seed, 1, 4, 2),
r'API Error 0003:') r'API Error 0003:')
six.assertRegex(self, six.assertRegex(
self.api.createDeterministicAddresses('', 1), self, self.api.createDeterministicAddresses('', 1),
r'API Error 0001:') r'API Error 0001:')
six.assertRegex(self, six.assertRegex(
self.api.createDeterministicAddresses(self._seed, 1, 2), self, self.api.createDeterministicAddresses(self._seed, 1, 2),
r'API Error 0002:') r'API Error 0002:')
six.assertRegex(self, six.assertRegex(
self.api.createDeterministicAddresses(self._seed, 0), self, self.api.createDeterministicAddresses(self._seed, 0),
r'API Error 0004:') r'API Error 0004:')
six.assertRegex(self, six.assertRegex(
self.api.createDeterministicAddresses(self._seed, 1000), self, self.api.createDeterministicAddresses(self._seed, 1000),
r'API Error 0005:') r'API Error 0005:')
addresses = json.loads( addresses = json.loads(
@ -442,7 +441,7 @@ class TestAPI(TestAPIProto):
self.assertEqual(self.api.joinChan(self._seed, addr), 'success') self.assertEqual(self.api.joinChan(self._seed, addr), 'success')
self.assertEqual(self.api.leaveChan(addr), 'success') self.assertEqual(self.api.leaveChan(addr), 'success')
# Joining with wrong address should fail # Joining with wrong address should fail
six.assertRegex(self, six.assertRegex(
self.api.joinChan(self._seed, 'BM-2cWzSnwjJ7yRP3nLEW'), self, self.api.joinChan(self._seed, 'BM-2cWzSnwjJ7yRP3nLEW'),
r'^API Error 0008:' r'^API Error 0008:'
) )

View File

@ -6,6 +6,7 @@ import struct
import tempfile import tempfile
import time import time
import unittest import unittest
import six import six
from pybitmessage import highlevelcrypto from pybitmessage import highlevelcrypto
@ -51,8 +52,8 @@ class TestStorageAbstract(unittest.TestCase):
def test_inventory_storage(self): def test_inventory_storage(self):
"""Check inherited abstract methods""" """Check inherited abstract methods"""
with six.assertRaisesRegex(self, with six.assertRaisesRegex(
TypeError, "^Can't instantiate abstract class.*" self, TypeError, "^Can't instantiate abstract class.*"
"methods __contains__, __delitem__, __getitem__, __iter__," "methods __contains__, __delitem__, __getitem__, __iter__,"
" __len__, __setitem__" " __len__, __setitem__"
): # pylint: disable=abstract-class-instantiated ): # pylint: disable=abstract-class-instantiated

View File

@ -4,6 +4,7 @@ Testing the logger configuration
import os import os
import tempfile import tempfile
import six import six
from .test_process import TestProcessProto from .test_process import TestProcessProto