diff options
| author | Damian Johnson <atagar@torproject.org> | 2020-01-05 13:28:37 -0800 |
|---|---|---|
| committer | Damian Johnson <atagar@torproject.org> | 2020-01-05 13:28:37 -0800 |
| commit | a396f57c3a55f34bb9f9d5976fc54fcc6561a8c4 (patch) | |
| tree | f34f4715f5e7c5b2edf19558e0a683b984394d1b | |
| parent | 4e21371e88662992fa21bc1f845c05c16ccee4f4 (diff) | |
| parent | bdd376bd9e25dcee54d43a47e10329d92f83cafd (diff) | |
Drop python 2.6 -> 3.5 support
As stated in our release announcement Stem 2.x will discontinue Python 2.x
support now that the PSF has dropped it...
https://blog.atagar.com/stem-release-1-8/
https://www.python.org/doc/sunset-python-2/
Presuming our next release is a year out, Python 3.6 is the oldest non-EOL
version so picking it as our new minimum requirement...
https://devguide.python.org/#status-of-python-branches
No doubt this compatibility change will let us make far more simplifications,
but for the moment simply addressing low hanging fruit.
98 files changed, 475 insertions, 1623 deletions
diff --git a/cache_fallback_directories.py b/cache_fallback_directories.py index cb413c07..91ad40c0 100755 --- a/cache_fallback_directories.py +++ b/cache_fallback_directories.py @@ -8,22 +8,17 @@ Caches tor's latest fallback directories. import re import sys +import urllib.request import stem.directory import stem.util.system -try: - # account for urllib's change between python 2.x and 3.x - import urllib.request as urllib -except ImportError: - import urllib2 as urllib - GITWEB_FALLBACK_LOG = 'https://gitweb.torproject.org/tor.git/log/src/app/config/fallback_dirs.inc' FALLBACK_DIR_LINK = "href='/tor.git/commit/src/app/config/fallback_dirs.inc\\?id=([^']*)'" if __name__ == '__main__': try: - fallback_dir_page = urllib.urlopen(GITWEB_FALLBACK_LOG).read() + fallback_dir_page = urllib.request.urlopen(GITWEB_FALLBACK_LOG).read() fallback_dir_commit = re.search(FALLBACK_DIR_LINK, fallback_dir_page).group(1) except: print("Unable to determine the latest commit to edit tor's fallback directories: %s" % sys.exc_info()[1]) diff --git a/cache_manual.py b/cache_manual.py index 7fe6ea70..5bc68b57 100755 --- a/cache_manual.py +++ b/cache_manual.py @@ -8,22 +8,17 @@ Caches tor's latest manual content. Run this to pick new man page changes. import re import sys +import urllib.request import stem.manual import stem.util.system -try: - # account for urllib's change between python 2.x and 3.x - import urllib.request as urllib -except ImportError: - import urllib2 as urllib - GITWEB_MAN_LOG = 'https://gitweb.torproject.org/tor.git/log/doc/tor.1.txt' MAN_LOG_LINK = "href='/tor.git/commit/doc/tor.1.txt\\?id=([^']*)'" if __name__ == '__main__': try: - man_log_page = urllib.urlopen(GITWEB_MAN_LOG).read() + man_log_page = urllib.request.urlopen(GITWEB_MAN_LOG).read() man_commit = re.search(MAN_LOG_LINK, man_log_page).group(1) except: print("Unable to determine the latest commit to edit tor's man page: %s" % sys.exc_info()[1]) diff --git a/docs/faq.rst b/docs/faq.rst index 63800b36..6bbc2f9a 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -86,7 +86,10 @@ Ubuntu you can install these with... What Python versions is Stem compatible with? --------------------------------------------- -Stem works with **Python 2.6 and greater**, including the Python 3.x series. +Stem works with **Python 3.6 and greater**. + +If you require a deprecated Python version then please use Stem 1.8, which was +compatible with **Python 2.6 and above**. .. _can_i_interact_with_tors_controller_interface_directly: diff --git a/run_tests.py b/run_tests.py index 8d7ea45e..d6ced384 100755 --- a/run_tests.py +++ b/run_tests.py @@ -7,6 +7,7 @@ Runs unit and integration tests. For usage information run this with '--help'. """ import errno +import io import importlib import logging import multiprocessing @@ -18,11 +19,6 @@ import time import traceback import unittest -try: - from StringIO import StringIO -except ImportError: - from io import StringIO - import stem.prereq import stem.util.conf import stem.util.log @@ -194,20 +190,6 @@ def main(): println('Nothing to run (for usage provide --help)\n') sys.exit() - if not stem.prereq.is_mock_available(): - try: - import mock - println(MOCK_OUT_OF_DATE_MSG % mock.__version__) - except ImportError: - println(MOCK_UNAVAILABLE_MSG) - - if stem.util.system.is_available('pip'): - println("You can get it by running 'sudo pip install mock'.") - elif stem.util.system.is_available('apt-get'): - println("You can get it by running 'sudo apt-get install python-mock'.") - - sys.exit(1) - test.task.run( 'INITIALISING', test.task.STEM_VERSION, @@ -215,7 +197,6 @@ def main(): test.task.PYTHON_VERSION, test.task.PLATFORM_VERSION, test.task.CRYPTO_VERSION, - test.task.MOCK_VERSION, test.task.PYFLAKES_VERSION, test.task.PYCODESTYLE_VERSION, test.task.CLEAN_PYC, @@ -442,7 +423,7 @@ def _run_test(args, test_class, output_filters): traceback.print_exc(exc) return None - test_results = StringIO() + test_results = io.StringIO() run_result = stem.util.test_tools.TimedTestRunner(test_results, verbosity = 2).run(suite) if args.verbose: @@ -7,7 +7,7 @@ # # * Recache latest information (cache_manual.py and cache_fallback_directories.py) # -# * Test with python2.6, python2.7, python3, and pypy. +# * Test with python3 and pypy. # |- If using tox run... # | # | % tox -- --all --target RUN_ALL,ONLINE @@ -92,11 +92,7 @@ To install you can either use... pip install stem -... or install from the source tarball. Stem supports both the python 2.x and 3.x series. To use its python3 counterpart you simply need to install using that version of python. - -:: - - python3 setup.py install +... or install from the source tarball. Stem supports Python 3.6 and above. After that, give some `tutorials <https://stem.torproject.org/tutorials.html>`_ a try! For questions or to discuss project ideas we're available on `irc <https://www.torproject.org/about/contact.html.en#irc>`_ and the `tor-dev@ email list <https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-dev>`_. """.strip() diff --git a/stem/__init__.py b/stem/__init__.py index 66bc24b5..3600e920 100644 --- a/stem/__init__.py +++ b/stem/__init__.py @@ -722,7 +722,7 @@ class SocketClosed(SocketError): class DownloadFailed(IOError): """ Inability to download a resource. Python's urllib module raises - a wide variety of undocumented exceptions (urllib2.URLError, + a wide variety of undocumented exceptions (urllib.request.URLError, socket.timeout, and others). This wraps lower level failures in a common exception type that diff --git a/stem/client/__init__.py b/stem/client/__init__.py index 307bd4e4..2abeac88 100644 --- a/stem/client/__init__.py +++ b/stem/client/__init__.py @@ -64,14 +64,6 @@ class Relay(object): """ def __init__(self, orport, link_protocol): - # TODO: Python 3.x adds a getbuffer() method which - # lets us get the size... - # - # https://stackoverflow.com/questions/26827055/python-how-to-get-iobytes-allocated-memory-length - # - # When we drop python 2.x support we should replace - # self._orport_buffer with an io.BytesIO. - self.link_protocol = LinkProtocol(link_protocol) self._orport = orport self._orport_buffer = b'' # unread bytes diff --git a/stem/client/cell.py b/stem/client/cell.py index 57a71749..83888556 100644 --- a/stem/client/cell.py +++ b/stem/client/cell.py @@ -354,10 +354,10 @@ class RelayCell(CircuitCell): digest_packed = digest.digest()[:RELAY_DIGEST_SIZE.size] digest = RELAY_DIGEST_SIZE.unpack(digest_packed) - elif stem.util._is_str(digest): + elif isinstance(digest, (bytes, str)): digest_packed = digest[:RELAY_DIGEST_SIZE.size] digest = RELAY_DIGEST_SIZE.unpack(digest_packed) - elif stem.util._is_int(digest): + elif isinstance(digest, int): pass else: raise ValueError('RELAY cell digest must be a hash, string, or int but was a %s' % type(digest).__name__) diff --git a/stem/client/datatype.py b/stem/client/datatype.py index 5de5e445..1db46a28 100644 --- a/stem/client/datatype.py +++ b/stem/client/datatype.py @@ -138,7 +138,6 @@ users.** See our :class:`~stem.client.Relay` the API you probably want. import binascii import collections import hashlib -import struct import stem.client.cell import stem.prereq @@ -183,7 +182,7 @@ class _IntegerEnum(stem.util.enum.Enum): Provides the (enum, int_value) tuple for a given value. """ - if stem.util._is_int(val): + if isinstance(val, int): return self._int_to_enum.get(val, self.UNKNOWN), val elif val in self: return val, self._enum_to_int.get(val, val) @@ -367,7 +366,7 @@ class Field(object): class Size(Field): """ Unsigned `struct.pack format - <https://docs.python.org/2/library/struct.html#format-characters>` for + <https://docs.python.org/3/library/struct.html#format-characters>` for network-order fields. ==================== =========== @@ -380,68 +379,30 @@ class Size(Field): ==================== =========== """ - def __init__(self, name, size, pack_format): + def __init__(self, name, size): self.name = name self.size = size - self.format = pack_format @staticmethod def pop(packed): raise NotImplementedError("Use our constant's unpack() and pop() instead") def pack(self, content): - # TODO: Python 2.6's struct module behaves a little differently in a couple - # respsects... - # - # * Invalid types raise a TypeError rather than a struct.error. - # - # * Negative values are happily packed despite being unsigned fields with - # a message printed to stdout (!) that says... - # - # stem/client/datatype.py:362: DeprecationWarning: struct integer overflow masking is deprecated - # packed = struct.pack(self.format, content) - # stem/client/datatype.py:362: DeprecationWarning: 'B' format requires 0 <= number <= 255 - # packed = struct.pack(self.format, content) - # - # Rather than adjust this method to account for these differences doing - # duplicate upfront checks just for python 2.6. When we drop 2.6 support - # this can obviously be dropped. - - if stem.prereq._is_python_26(): - if not stem.util._is_int(content): - raise ValueError('Size.pack encodes an integer, but was a %s' % type(content).__name__) - elif content < 0: - raise ValueError('Packed values must be positive (attempted to pack %i as a %s)' % (content, self.name)) - - # TODO: When we drop python 2.x support this can be simplified via - # integer's to_bytes() method. For example... - # - # struct.pack('>Q', my_number) - # - # ... is the same as... - # - # my_number.to_bytes(8, 'big') - try: - packed = struct.pack(self.format, content) - except struct.error: - if not stem.util._is_int(content): + return content.to_bytes(self.size, 'big') + except: + if not isinstance(content, int): raise ValueError('Size.pack encodes an integer, but was a %s' % type(content).__name__) elif content < 0: raise ValueError('Packed values must be positive (attempted to pack %i as a %s)' % (content, self.name)) else: - raise # some other struct exception - - if self.size != len(packed): - raise ValueError('%s is the wrong size for a %s field' % (repr(packed), self.name)) - - return packed + raise def unpack(self, packed): if self.size != len(packed): raise ValueError('%s is the wrong size for a %s field' % (repr(packed), self.name)) - return struct.unpack(self.format, packed)[0] + return int.from_bytes(packed, 'big') def pop(self, packed): to_unpack, remainder = split(packed, self.size) @@ -449,7 +410,7 @@ class Size(Field): return self.unpack(to_unpack), remainder def __hash__(self): - return stem.util._hash_attr(self, 'name', 'size', 'format', cache = True) + return stem.util._hash_attr(self, 'name', 'size', cache = True) class Address(Field): @@ -745,7 +706,7 @@ def _unpack_ipv6_address(value): return ':'.join(['%04x' % Size.SHORT.unpack(value[i * 2:(i + 1) * 2]) for i in range(8)]) -setattr(Size, 'CHAR', Size('CHAR', 1, '!B')) -setattr(Size, 'SHORT', Size('SHORT', 2, '!H')) -setattr(Size, 'LONG', Size('LONG', 4, '!L')) -setattr(Size, 'LONG_LONG', Size('LONG_LONG', 8, '!Q')) +setattr(Size, 'CHAR', Size('CHAR', 1)) +setattr(Size, 'SHORT', Size('SHORT', 2)) +setattr(Size, 'LONG', Size('LONG', 4)) +setattr(Size, 'LONG_LONG', Size('LONG_LONG', 8)) diff --git a/stem/control.py b/stem/control.py index 56567467..4adec330 100644 --- a/stem/control.py +++ b/stem/control.py @@ -253,21 +253,10 @@ import functools import inspect import io import os +import queue import threading import time -try: - # Added in 2.7 - from collections import OrderedDict -except ImportError: - from stem.util.ordereddict import OrderedDict - -try: - # Added in 3.x - import queue -except ImportError: - import Queue as queue - import stem.descriptor.microdescriptor import stem.descriptor.reader import stem.descriptor.router_status_entry @@ -1166,7 +1155,7 @@ class Controller(BaseController): start_time = time.time() reply = {} - if stem.util._is_str(params): + if isinstance(params, (bytes, str)): is_multiple = False params = set([params]) else: @@ -1211,7 +1200,7 @@ class Controller(BaseController): # usually we want unicode values under python 3.x - if stem.prereq.is_python_3() and not get_bytes: + if not get_bytes: response.entries = dict((k, stem.util.str_tools._to_unicode(v)) for (k, v) in response.entries.items()) reply.update(response.entries) @@ -2314,7 +2303,7 @@ class Controller(BaseController): start_time = time.time() reply = {} - if stem.util._is_str(params): + if isinstance(params, (bytes, str)): params = [params] # remove strings which contain only whitespace @@ -2623,7 +2612,7 @@ class Controller(BaseController): log.debug('GETCONF HiddenServiceOptions (failed: %s)' % exc) raise - service_dir_map = OrderedDict() + service_dir_map = collections.OrderedDict() directory = None for status_code, divider, content in response.content(): @@ -2779,7 +2768,7 @@ class Controller(BaseController): if path in conf and (port, target_address, target_port) in conf[path]['HiddenServicePort']: return None - conf.setdefault(path, OrderedDict()).setdefault('HiddenServicePort', []).append((port, target_address, target_port)) + conf.setdefault(path, collections.OrderedDict()).setdefault('HiddenServicePort', []).append((port, target_address, target_port)) if auth_type and client_names: hsac = "%s %s" % (auth_type, ','.join(client_names)) @@ -3482,7 +3471,7 @@ class Controller(BaseController): * :class:`stem.InvalidArguments` if features passed were invalid """ - if stem.util._is_str(features): + if isinstance(features, (bytes, str)): features = [features] response = self.msg('USEFEATURE %s' % ' '.join(features)) @@ -3637,7 +3626,7 @@ class Controller(BaseController): args = [circuit_id] - if stem.util._is_str(path): + if isinstance(path, (bytes, str)): path = [path] if path: diff --git a/stem/descriptor/__init__.py b/stem/descriptor/__init__.py index c97db183..fff08910 100644 --- a/stem/descriptor/__init__.py +++ b/stem/descriptor/__init__.py @@ -106,12 +106,6 @@ import stem.util.enum import stem.util.str_tools import stem.util.system -try: - # added in python 2.7 - from collections import OrderedDict -except ImportError: - from stem.util.ordereddict import OrderedDict - __all__ = [ 'bandwidth_file', 'certificate', @@ -363,7 +357,7 @@ def parse_file(descriptor_file, descriptor_type = None, validate = False, docume handler = None - if stem.util._is_str(descriptor_file): + if isinstance(descriptor_file, (bytes, str)): if stem.util.system.is_tarfile(descriptor_file): handler = _parse_file_for_tar_path else: @@ -377,14 +371,7 @@ def parse_file(descriptor_file, descriptor_type = None, validate = False, docume return - # Not all files are seekable. If unseekable then advising the user. - # - # Python 3.x adds an io.seekable() method, but not an option with python 2.x - # so using an experimental call to tell() to determine this. - - try: - descriptor_file.tell() - except IOError: + if not descriptor_file.seekable(): raise IOError(UNSEEKABLE_MSG) # The tor descriptor specifications do not provide a reliable method for @@ -453,16 +440,10 @@ def _parse_file_for_path(descriptor_file, *args, **kwargs): def _parse_file_for_tar_path(descriptor_file, *args, **kwargs): - # TODO: use 'with' for tarfile after dropping python 2.6 support - tar_file = tarfile.open(descriptor_file) - - try: + with tarfile.open(descriptor_file) as tar_file: for desc in parse_file(tar_file, *args, **kwargs): desc._set_path(os.path.abspath(descriptor_file)) yield desc - finally: - if tar_file: - tar_file.close() def _parse_file_for_tarfile(descriptor_file, *args, **kwargs): @@ -589,7 +570,7 @@ def _descriptor_content(attr = None, exclude = (), header_template = (), footer_ """ header_content, footer_content = [], [] - attr = {} if attr is None else OrderedDict(attr) # shallow copy since we're destructive + attr = {} if attr is None else collections.OrderedDict(attr) # shallow copy since we're destructive for content, template in ((header_content, header_template), (footer_content, footer_template)): @@ -707,7 +688,7 @@ def _parse_protocol_line(keyword, attribute): # parses 'protocol' entries like: Cons=1-2 Desc=1-2 DirCache=1 HSDir=1 value = _value(keyword, entries) - protocols = OrderedDict() + protocols = collections.OrderedDict() for k, v in _mappings_for(keyword, value): versions = [] @@ -1163,10 +1144,7 @@ class Descriptor(object): return super(Descriptor, self).__getattribute__(name) def __str__(self): - if stem.prereq.is_python_3(): - return stem.util.str_tools._to_unicode(self._raw_contents) - else: - return self._raw_contents + return stem.util.str_tools._to_unicode(self._raw_contents) def _compare(self, other, method): if type(self) != type(other): @@ -1240,7 +1218,7 @@ def _read_until_keywords(keywords, descriptor_file, inclusive = False, ignore_fi content = None if skip else [] ending_keyword = None - if stem.util._is_str(keywords): + if isinstance(keywords, (bytes, str)): keywords = (keywords,) if ignore_first: @@ -1468,7 +1446,7 @@ def _descriptor_components(raw_contents, validate, extra_keywords = (), non_asci if isinstance(raw_contents, bytes): raw_contents = stem.util.str_tools._to_unicode(raw_contents) - entries = OrderedDict() + entries = collections.OrderedDict() extra_entries = [] # entries with a keyword in extra_keywords remaining_lines = raw_contents.split('\n') diff --git a/stem/descriptor/bandwidth_file.py b/stem/descriptor/bandwidth_file.py index 658f02b8..43c43aff 100644 --- a/stem/descriptor/bandwidth_file.py +++ b/stem/descriptor/bandwidth_file.py @@ -14,6 +14,7 @@ Parsing for Bandwidth Authority metrics as described in Tor's .. versionadded:: 1.8.0 """ +import collections import datetime import io import time @@ -25,12 +26,6 @@ from stem.descriptor import ( Descriptor, ) -try: - # added in python 2.7 - from collections import OrderedDict -except ImportError: - from stem.util.ordereddict import OrderedDict - # Four character dividers are allowed for backward compatability, but five is # preferred. @@ -175,7 +170,7 @@ def _parse_file(descriptor_file, validate = False, **kwargs): def _parse_header(descriptor, entries): - header = OrderedDict() + header = collections.OrderedDict() content = io.BytesIO(descriptor.get_bytes()) content.readline() # skip the first line, which should be the timestamp @@ -332,7 +327,7 @@ class BandwidthFile(Descriptor): if sign: raise NotImplementedError('Signing of %s not implemented' % cls.__name__) - header = OrderedDict(attr) if attr is not None else OrderedDict() + header = collections.OrderedDict(attr) if attr is not None else collections.OrderedDict() timestamp = header.pop('timestamp', str(int(time.time()))) content = header.pop('content', []) version = header.get('version', HEADER_DEFAULT.get('version')) diff --git a/stem/descriptor/collector.py b/stem/descriptor/collector.py index c2bf8179..7aeb298b 100644 --- a/stem/descriptor/collector.py +++ b/stem/descriptor/collector.py @@ -55,7 +55,6 @@ import hashlib import json import os import re -import shutil import tempfile import time @@ -264,15 +263,9 @@ class File(object): if self._downloaded_to and os.path.exists(self._downloaded_to): directory = os.path.dirname(self._downloaded_to) else: - # TODO: The following can be replaced with simpler usage of - # tempfile.TemporaryDirectory when we drop python 2.x support. - - tmp_directory = tempfile.mkdtemp() - - for desc in self.read(tmp_directory, descriptor_type, start, end, document_handler, timeout, retries): - yield desc - - shutil.rmtree(tmp_directory) + with tempfile.TemporaryDirectory() as tmp_directory: + for desc in self.read(tmp_directory, descriptor_type, start, end, document_handler, timeout, retries): + yield desc return diff --git a/stem/descriptor/export.py b/stem/descriptor/export.py index 48699ca4..35835d7c 100644 --- a/stem/descriptor/export.py +++ b/stem/descriptor/export.py @@ -17,13 +17,9 @@ Toolkit for exporting descriptors to other formats. use this modle please `let me know <https://www.atagar.com/contact/>`_. """ +import io import csv -try: - from cStringIO import StringIO -except ImportError: - from io import StringIO - import stem.descriptor import stem.prereq @@ -44,13 +40,13 @@ def export_csv(descriptors, included_fields = (), excluded_fields = (), header = :param list included_fields: attributes to include in the csv :param list excluded_fields: attributes to exclude from the csv :param bool header: if **True** then the first line will be a comma separated - list of the attribute names (**only supported in python 2.7 and higher**) + list of the attribute names :returns: **str** of the CSV for the descriptors, one per line :raises: **ValueError** if descriptors contain more than one descriptor type """ - output_buffer = StringIO() + output_buffer = io.StringIO() export_csv_file(output_buffer, descriptors, included_fields, excluded_fields, header) return output_buffer.getvalue() @@ -66,7 +62,7 @@ def export_csv_file(output_file, descriptors, included_fields = (), excluded_fie :param list included_fields: attributes to include in the csv :param list excluded_fields: attributes to exclude from the csv :param bool header: if **True** then the first line will be a comma separated - list of the attribute names (**only supported in python 2.7 and higher**) + list of the attribute names :returns: **str** of the CSV for the descriptors, one per line :raises: **ValueError** if descriptors contain more than one descriptor type @@ -103,7 +99,7 @@ def export_csv_file(output_file, descriptors, included_fields = (), excluded_fie writer = csv.DictWriter(output_file, included_fields, dialect = _ExportDialect(), extrasaction='ignore') - if header and not stem.prereq._is_python_26(): + if header: writer.writeheader() for desc in descriptors: diff --git a/stem/descriptor/extrainfo_descriptor.py b/stem/descriptor/extrainfo_descriptor.py index 17082f88..f53d9502 100644 --- a/stem/descriptor/extrainfo_descriptor.py +++ b/stem/descriptor/extrainfo_descriptor.py @@ -100,11 +100,6 @@ from stem.descriptor import ( _random_crypto_blob, ) -if stem.prereq._is_lru_cache_available(): - from functools import lru_cache -else: - from stem.util.lru_cache import lru_cache - # known statuses for dirreq-v2-resp and dirreq-v3-resp... DirResponse = stem.util.enum.Enum( ('OK', 'ok'), @@ -950,7 +945,7 @@ class RelayExtraInfoDescriptor(ExtraInfoDescriptor): def create(cls, attr = None, exclude = (), validate = True, sign = False, signing_key = None): return cls(cls.content(attr, exclude, sign, signing_key), validate = validate) - @lru_cache() + @functools.lru_cache() def digest(self, hash_type = DigestHash.SHA1, encoding = DigestEncoding.HEX): if hash_type == DigestHash.SHA1: # our digest is calculated from everything except our signature diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py index e5f82861..7994bffb 100644 --- a/stem/descriptor/hidden_service.py +++ b/stem/descriptor/hidden_service.py @@ -35,6 +35,7 @@ import base64 import binascii import collections import datetime +import functools import hashlib import io import os @@ -70,11 +71,6 @@ from stem.descriptor import ( _random_crypto_blob, ) -if stem.prereq._is_lru_cache_available(): - from functools import lru_cache -else: - from stem.util.lru_cache import lru_cache - try: from cryptography.hazmat.backends.openssl.backend import backend X25519_AVAILABLE = hasattr(backend, 'x25519_supported') and backend.x25519_supported() @@ -745,7 +741,7 @@ class HiddenServiceDescriptorV2(BaseHiddenServiceDescriptor): else: self._entries = entries - @lru_cache() + @functools.lru_cache() def introduction_points(self, authentication_cookie = None): """ Provided this service's introduction points. diff --git a/stem/descriptor/microdescriptor.py b/stem/descriptor/microdescriptor.py index 81bcb43c..17d06d90 100644 --- a/stem/descriptor/microdescriptor.py +++ b/stem/descriptor/microdescriptor.py @@ -64,6 +64,7 @@ Doing the same is trivial with server descriptors... Microdescriptor - Tor microdescriptor. """ +import functools import hashlib import stem.exit_policy @@ -88,11 +89,6 @@ from stem.descriptor.router_status_entry import ( _parse_p_line, ) -if stem.prereq._is_lru_cache_available(): - from functools import lru_cache -else: - from stem.util.lru_cache import lru_cache - REQUIRED_FIELDS = ( 'onion-key', ) @@ -305,7 +301,7 @@ class Microdescriptor(Descriptor): else: raise NotImplementedError('Microdescriptor digests are only available in sha1 and sha256, not %s' % hash_type) - @lru_cache() + @functools.lru_cache() def get_annotations(self): """ Provides content that appeared prior to the descriptor. If this comes from diff --git a/stem/descriptor/reader.py b/stem/descriptor/reader.py index b4cc4279..e75cdb7e 100644 --- a/stem/descriptor/reader.py +++ b/stem/descriptor/reader.py @@ -84,14 +84,10 @@ and picks up where it left off if run again... import mimetypes import os +import queue import tarfile import threading -try: - import queue -except ImportError: - import Queue as queue - import stem.descriptor import stem.prereq import stem.util @@ -259,7 +255,7 @@ class DescriptorReader(object): :param bool validate: checks the validity of the descriptor's content if **True**, skips these checks otherwise :param bool follow_links: determines if we'll follow symlinks when traversing - directories (requires python 2.6) + directories :param int buffer_size: descriptors we'll buffer before waiting for some to be read, this is unbounded if zero :param str persistence_path: if set we will load and save processed file @@ -270,7 +266,7 @@ class DescriptorReader(object): """ def __init__(self, target, validate = False, follow_links = False, buffer_size = 100, persistence_path = None, document_handler = stem.descriptor.DocumentHandler.ENTRIES, **kwargs): - self._targets = [target] if stem.util._is_str(target) else target + self._targets = [target] if isinstance(target, (bytes, str)) else target # expand any relative paths we got @@ -525,41 +521,31 @@ class DescriptorReader(object): self._notify_skip_listeners(target, ReadFailed(exc)) def _handle_archive(self, target): - # TODO: When dropping python 2.6 support go back to using 'with' for - # tarfiles... - # - # http://bugs.python.org/issue7232 - - tar_file = None - try: - self._notify_read_listeners(target) - tar_file = tarfile.open(target) - - for tar_entry in tar_file: - if tar_entry.isfile(): - entry = tar_file.extractfile(tar_entry) - - try: - for desc in stem.descriptor.parse_file(entry, validate = self._validate, document_handler = self._document_handler, **self._kwargs): - if self._is_stopped.is_set(): - return - - desc._set_path(os.path.abspath(target)) - desc._set_archive_path(tar_entry.name) - self._unreturned_descriptors.put(desc) - self._iter_notice.set() - except TypeError as exc: - self._notify_skip_listeners(target, ParsingFailure(exc)) - except ValueError as exc: - self._notify_skip_listeners(target, ParsingFailure(exc)) - finally: - entry.close() + with tarfile.open(target) as tar_file: + self._notify_read_listeners(target) + + for tar_entry in tar_file: + if tar_entry.isfile(): + entry = tar_file.extractfile(tar_entry) + + try: + for desc in stem.descriptor.parse_file(entry, validate = self._validate, document_handler = self._document_handler, **self._kwargs): + if self._is_stopped.is_set(): + return + + desc._set_path(os.path.abspath(target)) + desc._set_archive_path(tar_entry.name) + self._unreturned_descriptors.put(desc) + self._iter_notice.set() + except TypeError as exc: + self._notify_skip_listeners(target, ParsingFailure(exc)) + except ValueError as exc: + self._notify_skip_listeners(target, ParsingFailure(exc)) + finally: + entry.close() except IOError as exc: self._notify_skip_listeners(target, ReadFailed(exc)) - finally: - if tar_file: - tar_file.close() def _notify_read_listeners(self, path): for listener in self._read_listeners: diff --git a/stem/descriptor/remote.py b/stem/descriptor/remote.py index c88b338e..c7132a38 100644 --- a/stem/descriptor/remote.py +++ b/stem/descriptor/remote.py @@ -104,6 +104,7 @@ import socket import sys import threading import time +import urllib.request import stem import stem.client @@ -116,12 +117,6 @@ import stem.util.tor_tools from stem.util import log, str_tools -try: - # account for urllib's change between python 2.x and 3.x - import urllib.request as urllib -except ImportError: - import urllib2 as urllib - # TODO: remove in stem 2.x, replaced with stem.descriptor.Compression Compression = stem.util.enum.Enum( @@ -1070,8 +1065,8 @@ def _download_from_dirport(url, compression, timeout): """ try: - response = urllib.urlopen( - urllib.Request( + response = urllib.request.urlopen( + urllib.request.Request( url, headers = { 'Accept-Encoding': ', '.join(map(lambda c: c.encoding, compression)), diff --git a/stem/descriptor/router_status_entry.py b/stem/descriptor/router_status_entry.py index cf1c52f5..d248774a 100644 --- a/stem/descriptor/router_status_entry.py +++ b/stem/descriptor/router_status_entry.py @@ -374,9 +374,7 @@ def _base64_to_hex(identity, check_if_fingerprint = True): raise ValueError("Unable to decode identity string '%s'" % identity) fingerprint = binascii.hexlify(identity_decoded).upper() - - if stem.prereq.is_python_3(): - fingerprint = stem.util.str_tools._to_unicode(fingerprint) + fingerprint = stem.util.str_tools._to_unicode(fingerprint) if check_if_fingerprint: if not stem.util.tor_tools.is_valid_fingerprint(fingerprint): diff --git a/stem/descriptor/server_descriptor.py b/stem/descriptor/server_descriptor.py index 1d9adf32..84ba8b65 100644 --- a/stem/descriptor/server_descriptor.py +++ b/stem/descriptor/server_descriptor.py @@ -92,11 +92,6 @@ from stem.descriptor import ( _random_crypto_blob, ) -if stem.prereq._is_lru_cache_available(): - from functools import lru_cache -else: - from stem.util.lru_cache import lru_cache - # relay descriptors must have exactly one of the following REQUIRED_FIELDS = ( 'router', @@ -668,7 +663,7 @@ class ServerDescriptor(Descriptor): raise NotImplementedError('Unsupported Operation: this should be implemented by the ServerDescriptor subclass') - @lru_cache() + @functools.lru_cache() def get_annotations(self): """ Provides content that appeared prior to the descriptor. If this comes from @@ -910,7 +905,7 @@ class RelayDescriptor(ServerDescriptor): def create(cls, attr = None, exclude = (), validate = True, sign = False, signing_key = None, exit_policy = None): return cls(cls.content(attr, exclude, sign, signing_key, exit_policy), validate = validate, skip_crypto_validation = not sign) - @lru_cache() + @functools.lru_cache() def digest(self, hash_type = DigestHash.SHA1, encoding = DigestEncoding.HEX): """ Provides the digest of our descriptor's content. @@ -967,7 +962,7 @@ class RelayDescriptor(ServerDescriptor): return RouterStatusEntryV3.create(attr) - @lru_cache() + @functools.lru_cache() def _onion_key_crosscert_digest(self): """ Provides the digest of the onion-key-crosscert data. This consists of the @@ -1051,7 +1046,7 @@ class BridgeDescriptor(ServerDescriptor): return self.get_scrubbing_issues() == [] - @lru_cache() + @functools.lru_cache() def get_scrubbing_issues(self): """ Provides issues with our scrubbing. diff --git a/stem/directory.py b/stem/directory.py index b93b49a2..7a1ce7b8 100644 --- a/stem/directory.py +++ b/stem/directory.py @@ -38,9 +38,11 @@ as follows... .. versionadded:: 1.7.0 """ +import collections import os import re import sys +import urllib.request import stem import stem.util @@ -48,18 +50,6 @@ import stem.util.conf from stem.util import connection, str_tools, tor_tools -try: - # added in python 2.7 - from collections import OrderedDict -except ImportError: - from stem.util.ordereddict import OrderedDict - -try: - # account for urllib's change between python 2.x and 3.x - import urllib.request as urllib -except ImportError: - import urllib2 as urllib - GITWEB_AUTHORITY_URL = 'https://gitweb.torproject.org/tor.git/plain/src/app/config/auth_dirs.inc' GITWEB_FALLBACK_URL = 'https://gitweb.torproject.org/tor.git/plain/src/app/config/fallback_dirs.inc' FALLBACK_CACHE_PATH = os.path.join(os.path.dirname(__file__), 'cached_fallbacks.cfg') @@ -265,7 +255,7 @@ class Authority(Directory): @staticmethod def from_remote(timeout = 60): try: - lines = str_tools._to_unicode(urllib.urlopen(GITWEB_AUTHORITY_URL, timeout = timeout).read()).splitlines() + lines = str_tools._to_unicode(urllib.request.urlopen(GITWEB_AUTHORITY_URL, timeout = timeout).read()).splitlines() if not lines: raise IOError('no content') @@ -369,13 +359,13 @@ class Fallback(Directory): def __init__(self, address = None, or_port = None, dir_port = None, fingerprint = None, nickname = None, has_extrainfo = False, orport_v6 = None, header = None): super(Fallback, self).__init__(address, or_port, dir_port, fingerprint, nickname, orport_v6) self.has_extrainfo = has_extrainfo - self.header = OrderedDict(header) if header else OrderedDict() + self.header = collections.OrderedDict(header) if header else collections.OrderedDict() @staticmethod def from_cache(path = FALLBACK_CACHE_PATH): conf = stem.util.conf.Config() conf.load(path) - headers = OrderedDict([(k.split('.', 1)[1], conf.get(k)) for k in conf.keys() if k.startswith('header.')]) + headers = collections.OrderedDict([(k.split('.', 1)[1], conf.get(k)) for k in conf.keys() if k.startswith('header.')]) results = {} @@ -413,7 +403,7 @@ class Fallback(Directory): @staticmethod def from_remote(timeout = 60): try: - lines = str_tools._to_unicode(urllib.urlopen(GITWEB_FALLBACK_URL, timeout = timeout).read()).splitlines() + lines = str_tools._to_unicode(urllib.request.urlopen(GITWEB_FALLBACK_URL, timeout = timeout).read()).splitlines() if not lines: raise IOError('no content') diff --git a/stem/exit_policy.py b/stem/exit_policy.py index ddaf719c..0d1e7e42 100644 --- a/stem/exit_policy.py +++ b/stem/exit_policy.py @@ -67,6 +67,7 @@ exiting to a destination is permissible or not. For instance... from __future__ import absolute_import +import functools import re import socket import zlib @@ -77,11 +78,6 @@ import stem.util.connection import stem.util.enum import stem.util.str_tools -if stem.prereq._is_lru_cache_available(): - from functools import lru_cache -else: - from stem.util.lru_cache import lru_cache - AddressType = stem.util.enum.Enum(('WILDCARD', 'Wildcard'), ('IPv4', 'IPv4'), ('IPv6', 'IPv6')) # Addresses aliased by the 'private' policy. From the tor man page... @@ -128,7 +124,7 @@ def get_config_policy(rules, ip_address = None): elif ip_address and stem.util.connection.is_valid_ipv6_address(ip_address, allow_brackets = True) and not (ip_address[0] == '[' and ip_address[-1] == ']'): ip_address = '[%s]' % ip_address # ExitPolicy validation expects IPv6 addresses to be bracketed - if stem.util._is_str(rules): + if isinstance(rules, (bytes, str)): rules = rules.split(',') result = [] @@ -242,7 +238,7 @@ class ExitPolicy(object): # sanity check the types for rule in rules: - if not stem.util._is_str(rule) and not isinstance(rule, ExitPolicyRule): + if not isinstance(rule, (bytes, str)) and not isinstance(rule, ExitPolicyRule): raise TypeError('Exit policy rules can only contain strings or ExitPolicyRules, got a %s (%s)' % (type(rule), rules)) # Unparsed representation of the rules we were constructed with. Our @@ -253,7 +249,7 @@ class ExitPolicy(object): is_all_str = True for rule in rules: - if not stem.util._is_str(rule): + if not isinstance(rule, (bytes, str)): is_all_str = False if rules and is_all_str: @@ -271,7 +267,7 @@ class ExitPolicy(object): self._is_allowed_default = True - @lru_cache() + @functools.lru_cache() def can_exit_to(self, address = None, port = None, strict = False): """ Checks if this policy allows exiting to a given destination or not. If the @@ -295,7 +291,7 @@ class ExitPolicy(object): return self._is_allowed_default - @lru_cache() + @functools.lru_cache() def is_exiting_allowed(self): """ Provides **True** if the policy allows exiting whatsoever, **False** @@ -317,7 +313,7 @@ class ExitPolicy(object): return self._is_allowed_default - @lru_cache() + @functools.lru_cache() def summary(self): """ Provides a short description of our policy chain, similar to a @@ -470,7 +466,7 @@ class ExitPolicy(object): if isinstance(rule, bytes): rule = stem.util.str_tools._to_unicode(rule) - if stem.util._is_str(rule): + if isinstance(rule, (bytes, str)): if not rule.strip(): continue @@ -520,7 +516,7 @@ class ExitPolicy(object): for rule in self._get_rules(): yield rule - @lru_cache() + @functools.lru_cache() def __str__(self): return ', '.join([str(rule) for rule in self._get_rules()]) @@ -873,7 +869,7 @@ class ExitPolicyRule(object): return self._is_default_suffix - @lru_cache() + @functools.lru_cache() def __str__(self): """ Provides the string representation of our policy. This does not @@ -917,13 +913,13 @@ class ExitPolicyRule(object): return label - @lru_cache() + @functools.lru_cache() def _get_mask_bin(self): # provides an integer representation of our mask return int(stem.util.connection._address_to_binary(self.get_mask(False)), 2) - @lru_cache() + @functools.lru_cache() def _get_address_bin(self): # provides an integer representation of our address diff --git a/stem/interpreter/__init__.py b/stem/interpreter/__init__.py index 30af3f62..67984261 100644 --- a/stem/interpreter/__init__.py +++ b/stem/interpreter/__init__.py @@ -127,14 +127,7 @@ def main(): if args.run_cmd: if args.run_cmd.upper().startswith('SETEVENTS '): - # TODO: we can use a lambda here when dropping python 2.x support, but - # until then print's status as a keyword prevents it from being used in - # lambdas - - def handle_event(event_message): - print(format(str(event_message), *STANDARD_OUTPUT)) - - controller._handle_event = handle_event + controller._handle_event = lambda event_message: print(format(str(event_message), *STANDARD_OUTPUT)) if sys.stdout.isatty(): events = args.run_cmd.upper().split(' ', 1)[1] @@ -171,14 +164,14 @@ def main(): while True: try: prompt = '... ' if interpreter.is_multiline_context else PROMPT - user_input = input(prompt) if stem.prereq.is_python_3() else raw_input(prompt) + user_input = input(prompt) interpreter.run_command(user_input, print_response = True) except stem.SocketClosed: if showed_close_confirmation: print(format('Unable to run tor commands. The control connection has been closed.', *ERROR_OUTPUT)) else: prompt = format("Tor's control port has closed. Do you want to continue this interpreter? (y/n) ", *HEADER_BOLD_OUTPUT) - user_input = input(prompt) if stem.prereq.is_python_3() else raw_input(prompt) + user_input = input(prompt) print('') # blank line if user_input.lower() in ('y', 'yes'): diff --git a/stem/interpreter/autocomplete.py b/stem/interpreter/autocomplete.py index b6c5354c..05585b48 100644 --- a/stem/interpreter/autocomplete.py +++ b/stem/interpreter/autocomplete.py @@ -5,15 +5,12 @@ Tab completion for our interpreter prompt. """ +import functools + import stem.prereq from stem.interpreter import uses_settings -if stem.prereq._is_lru_cache_available(): - from functools import lru_cache -else: - from stem.util.lru_cache import lru_cache - @uses_settings def _get_commands(controller, config): @@ -84,7 +81,7 @@ class Autocompleter(object): def __init__(self, controller): self._commands = _get_commands(controller) - @lru_cache() + @functools.lru_cache() def matches(self, text): """ Provides autocompletion matches for the given text. diff --git a/stem/interpreter/commands.py b/stem/interpreter/commands.py index 0f8f333c..6e61fdda 100644 --- a/stem/interpreter/commands.py +++ b/stem/interpreter/commands.py @@ -7,6 +7,7 @@ Handles making requests and formatting the responses. import code import contextlib +import io import socket import sys @@ -21,11 +22,6 @@ import stem.util.tor_tools from stem.interpreter import STANDARD_OUTPUT, BOLD_OUTPUT, ERROR_OUTPUT, uses_settings, msg from stem.util.term import format -try: - from cStringIO import StringIO -except ImportError: - from io import StringIO - MAX_EVENTS = 100 @@ -359,7 +355,7 @@ class ControlInterpreter(code.InteractiveConsole): is_tor_command = cmd in config.get('help.usage', {}) and cmd.lower() != 'events' if self._run_python_commands and not is_tor_command: - console_output = StringIO() + console_output = io.StringIO() with redirect(console_output, console_output): self.is_multiline_context = code.InteractiveConsole.push(self, command) diff --git a/stem/interpreter/help.py b/stem/interpreter/help.py index d2e08d5c..5fde9246 100644 --- a/stem/interpreter/help.py +++ b/stem/interpreter/help.py @@ -5,6 +5,8 @@ Provides our /help responses. """ +import functools + import stem.prereq from stem.interpreter import ( @@ -17,11 +19,6 @@ from stem.interpreter import ( from stem.util.term import format -if stem.prereq._is_lru_cache_available(): - from functools import lru_cache -else: - from stem.util.lru_cache import lru_cache - def response(controller, arg): """ @@ -55,7 +52,7 @@ def _normalize(arg): return arg -@lru_cache() +@functools.lru_cache() @uses_settings def _response(controller, arg, config): if not arg: diff --git a/stem/manual.py b/stem/manual.py index 25d435d4..c1b4bd8f 100644 --- a/stem/manual.py +++ b/stem/manual.py @@ -48,10 +48,13 @@ us what our torrc options do... .. versionadded:: 1.5.0 """ +import collections +import functools import os import shutil import sys import tempfile +import urllib.request import stem import stem.prereq @@ -61,23 +64,6 @@ import stem.util.enum import stem.util.log import stem.util.system -try: - # added in python 2.7 - from collections import OrderedDict -except ImportError: - from stem.util.ordereddict import OrderedDict - -if stem.prereq._is_lru_cache_available(): - from functools import lru_cache -else: - from stem.util.lru_cache import lru_cache - -try: - # account for urllib's change between python 2.x and 3.x - import urllib.request as urllib -except ImportError: - import urllib2 as urllib - Category = stem.util.enum.Enum('GENERAL', 'CLIENT', 'RELAY', 'DIRECTORY', 'AUTHORITY', 'HIDDEN_SERVICE', 'DENIAL_OF_SERVICE', 'TESTING', 'UNKNOWN') GITWEB_MANUAL_URL = 'https://gitweb.torproject.org/tor.git/plain/doc/tor.1.txt' CACHE_PATH = os.path.join(os.path.dirname(__file__), 'cached_manual.sqlite') @@ -96,7 +82,7 @@ SCHEMA = ( 'CREATE TABLE torrc(key TEXT PRIMARY KEY, name TEXT, category TEXT, usage TEXT, summary TEXT, description TEXT, position INTEGER)', ) -CATEGORY_SECTIONS = OrderedDict(( +CATEGORY_SECTIONS = collections.OrderedDict(( ('GENERAL OPTIONS', Category.GENERAL), ('CLIENT OPTIONS', Category.CLIENT), ('SERVER OPTIONS', Category.RELAY), @@ -199,7 +185,7 @@ class ConfigOption(object): return not self == other -@lru_cache() +@functools.lru_cache() def _config(lowercase = True): """ Provides a dictionary for our settings.cfg. This has a couple categories... @@ -302,14 +288,13 @@ def download_man_page(path = None, file_handle = None, url = GITWEB_MANUAL_URL, elif not stem.util.system.is_available('a2x'): raise IOError('We require a2x from asciidoc to provide a man page') - dirpath = tempfile.mkdtemp() - asciidoc_path = os.path.join(dirpath, 'tor.1.txt') - manual_path = os.path.join(dirpath, 'tor.1') + with tempfile.TemporaryDirectory() as dirpath: + asciidoc_path = os.path.join(dirpath, 'tor.1.txt') + manual_path = os.path.join(dirpath, 'tor.1') - try: try: with open(asciidoc_path, 'wb') as asciidoc_file: - request = urllib.urlopen(url, timeout = timeout) + request = urllib.request.urlopen(url, timeout = timeout) shutil.copyfileobj(request, asciidoc_file) except: exc, stacktrace = sys.exc_info()[1:3] @@ -339,8 +324,6 @@ def download_man_page(path = None, file_handle = None, url = GITWEB_MANUAL_URL, with open(manual_path, 'rb') as manual_file: shutil.copyfileobj(manual_file, file_handle) file_handle.flush() - finally: - shutil.rmtree(dirpath) class Manual(object): @@ -374,10 +357,10 @@ class Manual(object): self.name = name self.synopsis = synopsis self.description = description - self.commandline_options = OrderedDict(commandline_options) - self.signals = OrderedDict(signals) - self.files = OrderedDict(files) - self.config_options = OrderedDict(config_options) + self.commandline_options = collections.OrderedDict(commandline_options) + self.signals = collections.OrderedDict(signals) + self.files = collections.OrderedDict(files) + self.config_options = collections.OrderedDict(config_options) self.man_commit = None self.stem_commit = None self.schema = None @@ -442,7 +425,7 @@ class Manual(object): signals = dict(conn.execute('SELECT name, description FROM signals').fetchall()) files = dict(conn.execute('SELECT name, description FROM files').fetchall()) - config_options = OrderedDict() + config_options = collections.OrderedDict() for entry in conn.execute('SELECT name, category, usage, summary, description FROM torrc ORDER BY position').fetchall(): option, category, usage, summary, option_description = entry @@ -460,7 +443,7 @@ class Manual(object): conf = stem.util.conf.Config() conf.load(path, commenting = False) - config_options = OrderedDict() + config_options = collections.OrderedDict() for key in conf.keys(): if key.startswith('config_options.'): @@ -479,9 +462,9 @@ class Manual(object): conf.get('name', ''), conf.get('synopsis', ''), conf.get('description', ''), - conf.get('commandline_options', OrderedDict()), - conf.get('signals', OrderedDict()), - conf.get('files', OrderedDict()), + conf.get('commandline_options', collections.OrderedDict()), + conf.get('signals', collections.OrderedDict()), + conf.get('files', collections.OrderedDict()), config_options, ) @@ -514,7 +497,7 @@ class Manual(object): except OSError as exc: raise IOError("Unable to run '%s': %s" % (man_cmd, exc)) - categories, config_options = _get_categories(man_output), OrderedDict() + categories, config_options = _get_categories(man_output), collections.OrderedDict() for category_header, category_enum in CATEGORY_SECTIONS.items(): _add_config_options(config_options, category_enum, categories.get(category_header, [])) @@ -677,7 +660,7 @@ def _get_categories(content): if content and content[-1].startswith('Tor'): content = content[:-1] - categories = OrderedDict() + categories = collections.OrderedDict() category, lines = None, [] for line in content: @@ -687,8 +670,7 @@ def _get_categories(content): # \u2014 - extra long dash # \xb7 - centered dot - char_for = chr if stem.prereq.is_python_3() else unichr - line = line.replace(char_for(0x2019), "'").replace(char_for(0x2014), '-').replace(char_for(0xb7), '*') + line = line.replace(chr(0x2019), "'").replace(chr(0x2014), '-').replace(chr(0xb7), '*') if line and not line.startswith(' '): if category: @@ -727,7 +709,7 @@ def _get_indented_descriptions(lines): ignoring those. """ - options, last_arg = OrderedDict(), None + options, last_arg = collections.OrderedDict(), None for line in lines: if line == ' Note': diff --git a/stem/prereq.py b/stem/prereq.py index 5e8e89dc..74584165 100644 --- a/stem/prereq.py +++ b/stem/prereq.py @@ -12,12 +12,10 @@ stem will still read descriptors - just without signature checks. :: check_requirements - checks for minimum requirements for running stem - is_python_3 - checks if python 3.0 or later is available is_sqlite_available - checks if the sqlite3 module is available is_crypto_available - checks if the cryptography module is available is_zstd_available - checks if the zstd module is available is_lzma_available - checks if the lzma module is available - is_mock_available - checks if the mock module is available """ import functools @@ -46,52 +44,8 @@ def check_requirements(): major_version, minor_version = sys.version_info[0:2] - if major_version < 2 or (major_version == 2 and minor_version < 6): - raise ImportError('stem requires python version 2.6 or greater') - - -def _is_python_26(): - """ - Checks if we're running python 2.6. This isn't for users as it'll be removed - in stem 2.0 (when python 2.6 support goes away). - - .. deprecated:: 1.8.0 - Stem 2.x will remove this method along with Python 2.x support. - - :returns: **True** if we're running python 2.6, **False** otherwise - """ - - major_version, minor_version = sys.version_info[0:2] - - return major_version == 2 and minor_version == 6 - - -def is_python_27(): - """ - Checks if we're running python 2.7 or above (including the 3.x series). - - .. deprecated:: 1.5.0 - Stem 2.x will remove this method along with Python 2.x support. - - :returns: **True** if we meet this requirement and **False** otherwise - """ - - major_version, minor_version = sys.version_info[0:2] - - return major_version > 2 or (major_version == 2 and minor_version >= 7) - - -def is_python_3(): - """ - Checks if we're in the 3.0 - 3.x range. - - .. deprecated:: 1.8.0 - Stem 2.x will remove this method along with Python 2.x support. - - :returns: **True** if we meet this requirement and **False** otherwise - """ - - return sys.version_info[0] == 3 + if major_version < 3 or (major_version == 3 and minor_version < 6): + raise ImportError('stem requires python version 3.6 or greater') def is_pypy(): @@ -207,63 +161,6 @@ def is_lzma_available(): return False -def is_mock_available(): - """ - Checks if the mock module is available. In python 3.3 and up it is a builtin - unittest module, but before this it needed to be `installed separately - <https://pypi.org/project/mock/>`_. Imports should be as follows.... - - :: - - try: - # added in python 3.3 - from unittest.mock import Mock - except ImportError: - from mock import Mock - - :returns: **True** if the mock module is available and **False** otherwise - """ - - try: - # checks for python 3.3 version - import unittest.mock - return True - except ImportError: - pass - - try: - import mock - - # check for mock's patch.dict() which was introduced in version 0.7.0 - - if not hasattr(mock.patch, 'dict'): - raise ImportError() - - # check for mock's new_callable argument for patch() which was introduced in version 0.8.0 - - if 'new_callable' not in inspect.getargspec(mock.patch).args: - raise ImportError() - - return True - except ImportError: - return False - - -def _is_lru_cache_available(): - """ - Functools added lru_cache to the standard library in Python 3.2. Prior to - this using a bundled implementation. We're also using this with Python 3.5 - due to a buggy implementation. (:trac:`26412`) - """ - - major_version, minor_version = sys.version_info[0:2] - - if major_version == 3 and minor_version == 5: - return False - else: - return hasattr(functools, 'lru_cache') - - def _is_sha3_available(): """ Check if hashlib has sha3 support. This requires Python 3.6+ *or* the `pysha3 diff --git a/stem/process.py b/stem/process.py index 4ed10dfb..3f1a0e19 100644 --- a/stem/process.py +++ b/stem/process.py @@ -143,12 +143,9 @@ def launch_tor(tor_cmd = 'tor', args = None, torrc_path = None, completion_perce last_problem = 'Timed out' while True: - # Tor's stdout will be read as ASCII bytes. This is fine for python 2, but - # in python 3 that means it'll mismatch with other operations (for instance - # the bootstrap_line.search() call later will fail). - # - # It seems like python 2.x is perfectly happy for this to be unicode, so - # normalizing to that. + # Tor's stdout will be read as ASCII bytes. That means it'll mismatch + # with other operations (for instance the bootstrap_line.search() call + # later will fail), so normalizing to unicode. init_line = tor_process.stdout.readline().decode('utf-8', 'replace').strip() diff --git a/stem/response/__init__.py b/stem/response/__init__.py index 1a5b2b45..2fbb9c48 100644 --- a/stem/response/__init__.py +++ b/stem/response/__init__.py @@ -229,7 +229,7 @@ class ControlMessage(object): :returns: **list** of (str, str, str) tuples for the components of this message """ - if stem.prereq.is_python_3() and not get_bytes: + if not get_bytes: return [(code, div, stem.util.str_tools._to_unicode(content)) for (code, div, content) in self._parsed_content] else: return list(self._parsed_content) @@ -246,7 +246,7 @@ class ControlMessage(object): :returns: **str** of the socket data used to generate this message """ - if stem.prereq.is_python_3() and not get_bytes: + if not get_bytes: return stem.util.str_tools._to_unicode(self._raw_content) else: return self._raw_content @@ -286,8 +286,7 @@ class ControlMessage(object): """ for _, _, content in self._parsed_content: - if stem.prereq.is_python_3(): - content = stem.util.str_tools._to_unicode(content) + content = stem.util.str_tools._to_unicode(content) yield ControlLine(content) @@ -304,9 +303,7 @@ class ControlMessage(object): """ content = self._parsed_content[index][2] - - if stem.prereq.is_python_3(): - content = stem.util.str_tools._to_unicode(content) + content = stem.util.str_tools._to_unicode(content) return ControlLine(content) @@ -534,7 +531,7 @@ def _parse_entry(line, quoted, escaped, get_bytes): next_entry = codecs.escape_decode(next_entry)[0] - if stem.prereq.is_python_3() and not get_bytes: + if not get_bytes: next_entry = stem.util.str_tools._to_unicode(next_entry) # normalize back to str if get_bytes: diff --git a/stem/response/events.py b/stem/response/events.py index e634a5d6..9b6d8393 100644 --- a/stem/response/events.py +++ b/stem/response/events.py @@ -23,10 +23,6 @@ QUOTED_KW_ARG = re.compile('^(.*) ([A-Za-z0-9_]+)="(.*)"$') CELL_TYPE = re.compile('^[a-z0-9_]+$') PARSE_NEWCONSENSUS_EVENTS = True -# TODO: We can remove the following when we drop python2.6 support. - -INT_TYPE = int if stem.prereq.is_python_3() else long - class Event(stem.response.ControlMessage): """ @@ -163,7 +159,7 @@ class Event(stem.response.ControlMessage): attr_values = getattr(self, attr) if attr_values: - if stem.util._is_str(attr_values): + if isinstance(attr_values, (bytes, str)): attr_values = [attr_values] for value in attr_values: @@ -284,8 +280,8 @@ class BandwidthEvent(Event): elif not self.read.isdigit() or not self.written.isdigit(): raise stem.ProtocolError("A BW event's bytes sent and received should be a positive numeric value, received: %s" % self) - self.read = INT_TYPE(self.read) - self.written = INT_TYPE(self.written) + self.read = int(self.read) + self.written = int(self.written) class BuildTimeoutSetEvent(Event): @@ -1095,8 +1091,8 @@ class StreamBwEvent(Event): elif not self.read.isdigit() or not self.written.isdigit(): raise stem.ProtocolError("A STREAM_BW event's bytes sent and received should be a positive numeric value, received: %s" % self) - self.read = INT_TYPE(self.read) - self.written = INT_TYPE(self.written) + self.read = int(self.read) + self.written = int(self.written) self.time = self._iso_timestamp(self.time) @@ -1174,8 +1170,8 @@ class ConnectionBandwidthEvent(Event): elif not tor_tools.is_valid_connection_id(self.id): raise stem.ProtocolError("Connection IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.id, self)) - self.read = INT_TYPE(self.read) - self.written = INT_TYPE(self.written) + self.read = int(self.read) + self.written = int(self.written) self._log_if_unrecognized('conn_type', stem.ConnectionType) @@ -1247,7 +1243,7 @@ class CircuitBandwidthEvent(Event): value = getattr(self, attr) if value: - setattr(self, attr, INT_TYPE(value)) + setattr(self, attr, int(value)) class CellStatsEvent(Event): diff --git a/stem/response/getinfo.py b/stem/response/getinfo.py index 0b9766ba..27442ffd 100644 --- a/stem/response/getinfo.py +++ b/stem/response/getinfo.py @@ -53,8 +53,7 @@ class GetInfoResponse(stem.response.ControlMessage): except ValueError: raise stem.ProtocolError('GETINFO replies should only contain parameter=value mappings:\n%s' % self) - if stem.prereq.is_python_3(): - key = stem.util.str_tools._to_unicode(key) + key = stem.util.str_tools._to_unicode(key) # if the value is a multiline value then it *must* be of the form # '<key>=\n<value>' diff --git a/stem/response/protocolinfo.py b/stem/response/protocolinfo.py index 1763e59f..46f6ab4f 100644 --- a/stem/response/protocolinfo.py +++ b/stem/response/protocolinfo.py @@ -108,9 +108,7 @@ class ProtocolInfoResponse(stem.response.ControlMessage): if line.is_next_mapping('COOKIEFILE', True, True): self.cookie_path = line.pop_mapping(True, True, get_bytes = True)[1].decode(sys.getfilesystemencoding()) - - if stem.prereq.is_python_3(): - self.cookie_path = stem.util.str_tools._to_unicode(self.cookie_path) # normalize back to str + self.cookie_path = stem.util.str_tools._to_unicode(self.cookie_path) # normalize back to str elif line_type == 'VERSION': # Line format: # VersionLine = "250-VERSION" SP "Tor=" TorVersion OptArguments CRLF diff --git a/stem/socket.py b/stem/socket.py index 26e8f22e..4ab534b8 100644 --- a/stem/socket.py +++ b/stem/socket.py @@ -204,21 +204,10 @@ class BaseSocket(object): except socket.error: pass - # Suppressing unexpected exceptions from close. For instance, if the - # socket's file has already been closed then with python 2.7 that raises - # with... - # error: [Errno 32] Broken pipe - - try: - self._socket.close() - except: - pass + self._socket.close() if self._socket_file: - try: - self._socket_file.close() - except: - pass + self._socket_file.close() self._socket = None self._socket_file = None @@ -680,14 +669,12 @@ def recv_message(control_file, arrived_at = None): log.info(ERROR_MSG % ('SocketClosed', 'socket file has been closed')) raise stem.SocketClosed('socket file has been closed') - except (socket.error, ValueError) as exc: - # When disconnected we get... - # - # Python 2: - # socket.error: [Errno 107] Transport endpoint is not connected + except (OSError, ValueError) as exc: + # when disconnected this errors with... # - # Python 3: - # ValueError: I/O operation on closed file. + # * ValueError: I/O operation on closed file + # * OSError: [Errno 107] Transport endpoint is not connected + # * OSError: [Errno 9] Bad file descriptor log.info(ERROR_MSG % ('SocketClosed', 'received exception "%s"' % exc)) raise stem.SocketClosed(exc) @@ -710,9 +697,8 @@ def recv_message(control_file, arrived_at = None): status_code, divider, content = line[:3], line[3:4], line[4:-2] # strip CRLF off content - if stem.prereq.is_python_3(): - status_code = stem.util.str_tools._to_unicode(status_code) - divider = stem.util.str_tools._to_unicode(divider) + status_code = stem.util.str_tools._to_unicode(status_code) + divider = stem.util.str_tools._to_unicode(divider) # Most controller responses are single lines, in which case we don't need # so much overhead. diff --git a/stem/util/__init__.py b/stem/util/__init__.py index 5be29777..2bcc8a07 100644 --- a/stem/util/__init__.py +++ b/stem/util/__init__.py @@ -49,19 +49,10 @@ def _hash_value(val): if not HASH_TYPES: my_hash = 0 else: - # TODO: I hate doing this but until Python 2.x support is dropped we - # can't readily be strict about bytes vs unicode for attributes. This - # is because test assertions often use strings, and normalizing this - # would require wrapping most with to_unicode() calls. - # - # This hack will go away when we drop Python 2.x support. + # Hashing common builtins (ints, bools, etc) provide consistant values but + # many others vary their value on interpreter invokation. - if _is_str(val): - my_hash = hash('str') - else: - # Hashing common builtins (ints, bools, etc) provide consistant values but many others vary their value on interpreter invokation. - - my_hash = hash(str(type(val))) + my_hash = hash(str(type(val))) if isinstance(val, (tuple, list)): for v in val: @@ -75,40 +66,6 @@ def _hash_value(val): return my_hash -def _is_str(val): - """ - Check if a value is a string. This will be removed when we no longer provide - backward compatibility for the Python 2.x series. - - :param object val: value to be checked - - :returns: **True** if the value is some form of string (unicode or bytes), - and **False** otherwise - """ - - if stem.prereq.is_python_3(): - return isinstance(val, (bytes, str)) - else: - return isinstance(val, (bytes, unicode)) - - -def _is_int(val): - """ - Check if a value is an integer. This will be removed when we no longer - provide backward compatibility for the Python 2.x series. - - :param object val: value to be checked - - :returns: **True** if the value is some form of integer (int or long), - and **False** otherwise - """ - - if stem.prereq.is_python_3(): - return isinstance(val, int) - else: - return isinstance(val, (int, long)) - - def datetime_to_unix(timestamp): """ Converts a utc datetime object to a unix timestamp. @@ -120,11 +77,7 @@ def datetime_to_unix(timestamp): :returns: **float** for the unix timestamp of the given datetime object """ - if stem.prereq._is_python_26(): - delta = (timestamp - datetime.datetime(1970, 1, 1)) - return delta.days * 86400 + delta.seconds - else: - return (timestamp - datetime.datetime(1970, 1, 1)).total_seconds() + return (timestamp - datetime.datetime(1970, 1, 1)).total_seconds() def _pubkey_bytes(key): @@ -132,7 +85,7 @@ def _pubkey_bytes(key): Normalizes X25509 and ED25519 keys into their public key bytes. """ - if _is_str(key): + if isinstance(key, (bytes, str)): return key if not stem.prereq.is_crypto_available(): diff --git a/stem/util/conf.py b/stem/util/conf.py index b4580ed9..1dbcc243 100644 --- a/stem/util/conf.py +++ b/stem/util/conf.py @@ -157,6 +157,7 @@ Here's an expanation of what happened... +- get_value - provides the value for a given key as a string """ +import collections import inspect import os import threading @@ -165,12 +166,6 @@ import stem.prereq from stem.util import log -try: - # added in python 2.7 - from collections import OrderedDict -except ImportError: - from stem.util.ordereddict import OrderedDict - CONFS = {} # mapping of identifier to singleton instances of configs @@ -453,9 +448,9 @@ class Config(object): """ def __init__(self): - self._path = None # location we last loaded from or saved to - self._contents = OrderedDict() # configuration key/value pairs - self._listeners = [] # functors to be notified of config changes + self._path = None # location we last loaded from or saved to + self._contents = collections.OrderedDict() # configuration key/value pairs + self._listeners = [] # functors to be notified of config changes # used for accessing _contents self._contents_lock = threading.RLock() @@ -640,14 +635,12 @@ class Config(object): """ with self._contents_lock: - unicode_type = str if stem.prereq.is_python_3() else unicode - if value is None: if overwrite and key in self._contents: del self._contents[key] else: pass # no value so this is a no-op - elif isinstance(value, (bytes, unicode_type)): + elif isinstance(value, (bytes, str)): if not overwrite and key in self._contents: self._contents[key].append(value) else: @@ -735,7 +728,7 @@ class Config(object): elif isinstance(default, tuple): val = tuple(val) elif isinstance(default, dict): - val_map = OrderedDict() + val_map = collections.OrderedDict() for entry in val: if '=>' in entry: entry_key, entry_val = entry.split('=>', 1) diff --git a/stem/util/connection.py b/stem/util/connection.py index bd1fb5a2..f88b3f85 100644 --- a/stem/util/connection.py +++ b/stem/util/connection.py @@ -62,6 +62,7 @@ import re import socket import sys import time +import urllib.request import stem import stem.util @@ -70,12 +71,6 @@ import stem.util.system from stem.util import conf, enum, log, str_tools -try: - # account for urllib's change between python 2.x and 3.x - import urllib.request as urllib -except ImportError: - import urllib2 as urllib - # Connection resolution is risky to log about since it's highly likely to # contain sensitive information. That said, it's also difficult to get right in # a platform independent fashion. To opt into the logging requried to @@ -197,7 +192,7 @@ def download(url, timeout = None, retries = None): start_time = time.time() try: - return urllib.urlopen(url, timeout = timeout).read() + return urllib.request.urlopen(url, timeout = timeout).read() except socket.timeout as exc: raise stem.DownloadTimeout(url, exc, sys.exc_info()[2], timeout) except: @@ -458,7 +453,7 @@ def is_valid_ipv4_address(address): if isinstance(address, bytes): address = str_tools._to_unicode(address) - elif not stem.util._is_str(address): + elif not isinstance(address, (bytes, str)): return False # checks if theres four period separated values @@ -488,7 +483,7 @@ def is_valid_ipv6_address(address, allow_brackets = False): if isinstance(address, bytes): address = str_tools._to_unicode(address) - elif not stem.util._is_str(address): + elif not isinstance(address, (bytes, str)): return False if allow_brackets: diff --git a/stem/util/enum.py b/stem/util/enum.py index 76a52a55..abaf2490 100644 --- a/stem/util/enum.py +++ b/stem/util/enum.py @@ -76,7 +76,7 @@ class Enum(object): keys, values = [], [] for entry in args: - if stem.util._is_str(entry): + if isinstance(entry, (bytes, str)): key, val = entry, _to_camel_case(entry) elif isinstance(entry, tuple) and len(entry) == 2: key, val = entry diff --git a/stem/util/log.py b/stem/util/log.py index e8d61c1d..4ef977b1 100644 --- a/stem/util/log.py +++ b/stem/util/log.py @@ -153,8 +153,7 @@ def escape(message): :returns: str that is escaped """ - if stem.prereq.is_python_3(): - message = stem.util.str_tools._to_unicode(message) + message = stem.util.str_tools._to_unicode(message) for pattern, replacement in (('\n', '\\n'), ('\r', '\\r'), ('\t', '\\t')): message = message.replace(pattern, replacement) @@ -232,13 +231,7 @@ class LogBuffer(logging.Handler): """ def __init__(self, runlevel, yield_records = False): - # TODO: At least in python 2.6 logging.Handler has a bug in that it doesn't - # extend object, causing our super() call to fail. When we drop python 2.6 - # support we should switch back to using super() instead. - # - # super(LogBuffer, self).__init__(level = logging_level(runlevel)) - - logging.Handler.__init__(self, level = logging_level(runlevel)) + super(LogBuffer, self).__init__(level = logging_level(runlevel)) self.formatter = FORMATTER self._buffer = [] diff --git a/stem/util/lru_cache.py b/stem/util/lru_cache.py deleted file mode 100644 index 011d4456..00000000 --- a/stem/util/lru_cache.py +++ /dev/null @@ -1,182 +0,0 @@ -# Drop in replace for python 3.2's collections.lru_cache, from... -# http://code.activestate.com/recipes/578078-py26-and-py30-backport-of-python-33s-lru-cache/ -# -# ... which is under the MIT license. Stem users should *not* rely upon this -# module. It will be removed when we drop support for python 3.2 and below. - -""" -Memoization decorator that caches a function's return value. If later called -with the same arguments then the cached value is returned rather than -reevaluated. - -This is a a python 2.x port of `functools.lru_cache -<http://docs.python.org/3/library/functools.html#functools.lru_cache>`_. If -using python 3.2 or later you should use that instead. -""" - -from collections import namedtuple -from functools import update_wrapper -from threading import RLock - -_CacheInfo = namedtuple('CacheInfo', ['hits', 'misses', 'maxsize', 'currsize']) - - -class _HashedSeq(list): - __slots__ = 'hashvalue' - - def __init__(self, tup, hash=hash): - self[:] = tup - self.hashvalue = hash(tup) - - def __hash__(self): - return self.hashvalue - - -def _make_key(args, kwds, typed, - kwd_mark = (object(),), - fasttypes = set([int, str, frozenset, type(None)]), - sorted=sorted, tuple=tuple, type=type, len=len): - 'Make a cache key from optionally typed positional and keyword arguments' - key = args - if kwds: - sorted_items = sorted(kwds.items()) - key += kwd_mark - for item in sorted_items: - key += item - if typed: - key += tuple(type(v) for v in args) - if kwds: - key += tuple(type(v) for k, v in sorted_items) - elif len(key) == 1 and type(key[0]) in fasttypes: - return key[0] - return _HashedSeq(key) - - -def lru_cache(maxsize=100, typed=False): - """Least-recently-used cache decorator. - - If *maxsize* is set to None, the LRU features are disabled and the cache - can grow without bound. - - If *typed* is True, arguments of different types will be cached separately. - For example, f(3.0) and f(3) will be treated as distinct calls with - distinct results. - - Arguments to the cached function must be hashable. - - View the cache statistics named tuple (hits, misses, maxsize, currsize) with - f.cache_info(). Clear the cache and statistics with f.cache_clear(). - Access the underlying function with f.__wrapped__. - - See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used - - """ - - # Users should only access the lru_cache through its public API: - # cache_info, cache_clear, and f.__wrapped__ - # The internals of the lru_cache are encapsulated for thread safety and - # to allow the implementation to change (including a possible C version). - - def decorating_function(user_function): - - cache = dict() - stats = [0, 0] # make statistics updateable non-locally - HITS, MISSES = 0, 1 # names for the stats fields - make_key = _make_key - cache_get = cache.get # bound method to lookup key or return None - _len = len # localize the global len() function - lock = RLock() # because linkedlist updates aren't threadsafe - root = [] # root of the circular doubly linked list - root[:] = [root, root, None, None] # initialize by pointing to self - nonlocal_root = [root] # make updateable non-locally - PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields - - if maxsize == 0: - - def wrapper(*args, **kwds): - # no caching, just do a statistics update after a successful call - result = user_function(*args, **kwds) - stats[MISSES] += 1 - return result - - elif maxsize is None: - - def wrapper(*args, **kwds): - # simple caching without ordering or size limit - key = make_key(args, kwds, typed) - result = cache_get(key, root) # root used here as a unique not-found sentinel - if result is not root: - stats[HITS] += 1 - return result - result = user_function(*args, **kwds) - cache[key] = result - stats[MISSES] += 1 - return result - - else: - - def wrapper(*args, **kwds): - # size limited caching that tracks accesses by recency - key = make_key(args, kwds, typed) if kwds or typed else args - with lock: - link = cache_get(key) - if link is not None: - # record recent use of the key by moving it to the front of the list - root, = nonlocal_root - link_prev, link_next, key, result = link - link_prev[NEXT] = link_next - link_next[PREV] = link_prev - last = root[PREV] - last[NEXT] = root[PREV] = link - link[PREV] = last - link[NEXT] = root - stats[HITS] += 1 - return result - result = user_function(*args, **kwds) - with lock: - root, = nonlocal_root - if key in cache: - # getting here means that this same key was added to the - # cache while the lock was released. since the link - # update is already done, we need only return the - # computed result and update the count of misses. - pass - elif _len(cache) >= maxsize: - # use the old root to store the new key and result - oldroot = root - oldroot[KEY] = key - oldroot[RESULT] = result - # empty the oldest link and make it the new root - root = nonlocal_root[0] = oldroot[NEXT] - oldkey = root[KEY] - root[KEY] = root[RESULT] = None - # now update the cache dictionary for the new links - del cache[oldkey] - cache[key] = oldroot - else: - # put result in a new link at the front of the list - last = root[PREV] - link = [last, root, key, result] - last[NEXT] = root[PREV] = cache[key] = link - stats[MISSES] += 1 - return result - - def cache_info(): - """Report cache statistics""" - with lock: - return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache)) - - def cache_clear(): - """Clear the cache and cache statistics""" - with lock: - cache.clear() - root = nonlocal_root[0] - root[:] = [root, root, None, None] - stats[:] = [0, 0] - - wrapper.__wrapped__ = user_function - wrapper.cache_info = cache_info - wrapper.cache_clear = cache_clear - return update_wrapper(wrapper, user_function) - - return decorating_function diff --git a/stem/util/ordereddict.py b/stem/util/ordereddict.py deleted file mode 100644 index ec228f31..00000000 --- a/stem/util/ordereddict.py +++ /dev/null @@ -1,133 +0,0 @@ -# Drop in replacement for python 2.7's OrderedDict, from... -# https://pypi.org/project/ordereddict/ -# -# Stem users should *not* rely upon this module. It will be removed when we -# drop support for python 2.6 and below. - -# Copyright (c) 2009 Raymond Hettinger -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -# OTHER DEALINGS IN THE SOFTWARE. - -from UserDict import DictMixin - - -class OrderedDict(dict, DictMixin): - def __init__(self, *args, **kwds): - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - try: - self.__end - except AttributeError: - self.clear() - self.update(*args, **kwds) - - def clear(self): - self.__end = end = [] - end += [None, end, end] # sentinel node for doubly linked list - self.__map = {} # key --> [key, prev, next] - dict.clear(self) - - def __setitem__(self, key, value): - if key not in self: - end = self.__end - curr = end[1] - curr[2] = end[1] = self.__map[key] = [key, curr, end] - dict.__setitem__(self, key, value) - - def __delitem__(self, key): - dict.__delitem__(self, key) - key, prev, next = self.__map.pop(key) - prev[2] = next - next[1] = prev - - def __iter__(self): - end = self.__end - curr = end[2] - while curr is not end: - yield curr[0] - curr = curr[2] - - def __reversed__(self): - end = self.__end - curr = end[1] - while curr is not end: - yield curr[0] - curr = curr[1] - - def popitem(self, last=True): - if not self: - raise KeyError('dictionary is empty') - if last: - key = reversed(self).next() - else: - key = iter(self).next() - value = self.pop(key) - return key, value - - def __reduce__(self): - items = [[k, self[k]] for k in self] - tmp = self.__map, self.__end - del self.__map, self.__end - inst_dict = vars(self).copy() - self.__map, self.__end = tmp - if inst_dict: - return (self.__class__, (items,), inst_dict) - return self.__class__, (items,) - - def keys(self): - return list(self) - - setdefault = DictMixin.setdefault - update = DictMixin.update - pop = DictMixin.pop - values = DictMixin.values - items = DictMixin.items - iterkeys = DictMixin.iterkeys - itervalues = DictMixin.itervalues - iteritems = DictMixin.iteritems - - def __repr__(self): - if not self: - return '%s()' % (self.__class__.__name__,) - return '%s(%r)' % (self.__class__.__name__, self.items()) - - def copy(self): - return self.__class__(self) - - @classmethod - def fromkeys(cls, iterable, value=None): - d = cls() - for key in iterable: - d[key] = value - return d - - def __eq__(self, other): - if isinstance(other, OrderedDict): - if len(self) != len(other): - return False - for p, q in zip(self.items(), other.items()): - if p != q: - return False - return True - return dict.__eq__(self, other) - - def __ne__(self, other): - return not self == other diff --git a/stem/util/proc.py b/stem/util/proc.py index f0e0104f..ecb7f3f7 100644 --- a/stem/util/proc.py +++ b/stem/util/proc.py @@ -48,6 +48,7 @@ future, use them at your own risk.** """ import base64 +import functools import os import platform import socket @@ -68,11 +69,6 @@ try: except ImportError: IS_PWD_AVAILABLE = False -if stem.prereq._is_lru_cache_available(): - from functools import lru_cache -else: - from stem.util.lru_cache import lru_cache - # os.sysconf is only defined on unix try: CLOCK_TICKS = os.sysconf(os.sysconf_names['SC_CLK_TCK']) @@ -88,7 +84,7 @@ Stat = stem.util.enum.Enum( ) -@lru_cache() +@functools.lru_cache() def is_available(): """ Checks if proc information is available on this platform. @@ -109,7 +105,7 @@ def is_available(): return True -@lru_cache() +@functools.lru_cache() def system_start_time(): """ Provides the unix time (seconds since epoch) when the system started. @@ -132,7 +128,7 @@ def system_start_time(): raise exc -@lru_cache() +@functools.lru_cache() def physical_memory(): """ Provides the total physical memory on the system in bytes. diff --git a/stem/util/str_tools.py b/stem/util/str_tools.py index 58effbee..6b852f3d 100644 --- a/stem/util/str_tools.py +++ b/stem/util/str_tools.py @@ -61,30 +61,17 @@ TIME_UNITS = ( _timestamp_re = re.compile(r'(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})') -if stem.prereq.is_python_3(): - def _to_bytes_impl(msg): - if isinstance(msg, str): - return codecs.latin_1_encode(msg, 'replace')[0] - else: - return msg - - def _to_unicode_impl(msg): - if msg is not None and not isinstance(msg, str): - return msg.decode('utf-8', 'replace') - else: - return msg -else: - def _to_bytes_impl(msg): - if msg is not None and isinstance(msg, unicode): - return codecs.latin_1_encode(msg, 'replace')[0] - else: - return msg +def _to_bytes_impl(msg): + if isinstance(msg, str): + return codecs.latin_1_encode(msg, 'replace')[0] + else: + return msg - def _to_unicode_impl(msg): - if msg is not None and not isinstance(msg, unicode): - return msg.decode('utf-8', 'replace') - else: - return msg +def _to_unicode_impl(msg): + if msg is not None and not isinstance(msg, str): + return msg.decode('utf-8', 'replace') + else: + return msg def _to_bytes(msg): @@ -137,7 +124,7 @@ def _to_int(msg): :returns: **int** representation of the string """ - if stem.prereq.is_python_3() and isinstance(msg, bytes): + if isinstance(msg, bytes): # iterating over bytes in python3 provides ints rather than characters return sum([pow(256, (len(msg) - i - 1)) * c for (i, c) in enumerate(msg)]) else: @@ -508,7 +495,7 @@ def _parse_timestamp(entry): :raises: **ValueError** if the timestamp is malformed """ - if not stem.util._is_str(entry): + if not isinstance(entry, (bytes, str)): raise ValueError('parse_timestamp() input must be a str, got a %s' % type(entry)) try: @@ -534,7 +521,7 @@ def _parse_iso_timestamp(entry): :raises: **ValueError** if the timestamp is malformed """ - if not stem.util._is_str(entry): + if not isinstance(entry, (bytes, str)): raise ValueError('parse_iso_timestamp() input must be a str, got a %s' % type(entry)) # based after suggestions from... diff --git a/stem/util/system.py b/stem/util/system.py index 0d2262f5..e514c3a4 100644 --- a/stem/util/system.py +++ b/stem/util/system.py @@ -454,7 +454,7 @@ def is_running(command): if command_listing: command_listing = [c.strip() for c in command_listing] - if stem.util._is_str(command): + if isinstance(command, (bytes, str)): command = [command] for cmd in command: diff --git a/stem/util/test_tools.py b/stem/util/test_tools.py index c7dce7ce..d65b3ea4 100644 --- a/stem/util/test_tools.py +++ b/stem/util/test_tools.py @@ -56,15 +56,6 @@ ASYNC_TESTS = {} AsyncStatus = stem.util.enum.UppercaseEnum('PENDING', 'RUNNING', 'FINISHED') AsyncResult = collections.namedtuple('AsyncResult', 'type msg') -# TODO: Providing a copy of SkipTest that works with python 2.6. This will be -# dropped when we remove python 2.6 support. - -if stem.prereq._is_python_26(): - class SkipTest(Exception): - 'Notes that the test was skipped.' -else: - SkipTest = unittest.case.SkipTest - def assert_equal(expected, actual, msg = None): """ @@ -111,7 +102,7 @@ def skip(msg): :raises: **unittest.case.SkipTest** for this reason """ - raise SkipTest(msg) + raise unittest.case.SkipTest(msg) def asynchronous(func): @@ -159,9 +150,6 @@ class AsyncTest(object): self._status = AsyncStatus.PENDING def run(self, *runner_args, **kwargs): - if stem.prereq._is_python_26(): - return # not supported under python 2.6 - def _wrapper(conn, runner, args): os.nice(12) @@ -170,7 +158,7 @@ class AsyncTest(object): conn.send(AsyncResult('success', None)) except AssertionError as exc: conn.send(AsyncResult('failure', str(exc))) - except SkipTest as exc: + except unittest.case.SkipTest as exc: conn.send(AsyncResult('skipped', str(exc))) except: conn.send(AsyncResult('error', traceback.format_exc())) @@ -209,9 +197,6 @@ class AsyncTest(object): self.result(None) def result(self, test): - if stem.prereq._is_python_26(): - return # not supported under python 2.6 - with self._process_lock: if self._status == AsyncStatus.PENDING: self.run() @@ -259,21 +244,6 @@ class TimedTestRunner(unittest.TextTestRunner): TEST_RUNTIMES[self.id()] = time.time() - start_time return result - # TODO: remove and drop unnecessary 'returns' when dropping python 2.6 - # support - - def skipTest(self, message): - if not stem.prereq._is_python_26(): - return super(original_type, self).skipTest(message) - - # TODO: remove when dropping python 2.6 support - - def assertItemsEqual(self, expected, actual): - if stem.prereq._is_python_26(): - self.assertEqual(set(expected), set(actual)) - else: - return super(original_type, self).assertItemsEqual(expected, actual) - def assertRaisesWith(self, exc_type, exc_msg, func, *args, **kwargs): """ Asserts the given invokation raises the expected excepiton. This is @@ -287,16 +257,6 @@ class TimedTestRunner(unittest.TextTestRunner): return self.assertRaisesRegexp(exc_type, '^%s$' % re.escape(exc_msg), func, *args, **kwargs) - def assertRaisesRegexp(self, exc_type, exc_msg, func, *args, **kwargs): - if stem.prereq._is_python_26(): - try: - func(*args, **kwargs) - self.fail('Expected a %s to be raised but nothing was' % exc_type) - except exc_type as exc: - self.assertTrue(re.search(exc_msg, str(exc), re.MULTILINE)) - else: - return super(original_type, self).assertRaisesRegexp(exc_type, exc_msg, func, *args, **kwargs) - def id(self): return '%s.%s.%s' % (original_type.__module__, original_type.__name__, self._testMethodName) @@ -511,21 +471,6 @@ def stylistic_issues(paths, check_newlines = False, check_exception_keyword = Fa if '"""' in content: is_block_comment = not is_block_comment - if check_exception_keyword and content.startswith('except') and content.endswith(', exc:'): - # Python 2.6 - 2.7 supports two forms for exceptions... - # - # except ValueError, exc: - # except ValueError as exc: - # - # The former is the old method and no longer supported in python 3 - # going forward. - - # TODO: This check only works if the exception variable is called - # 'exc'. We should generalize this via a regex so other names work - # too. - - issues.setdefault(filename, []).append(Issue(index + 1, "except clause should use 'as', not comma", line)) - if prefer_single_quotes and not is_block_comment: if '"' in content and "'" not in content and '"""' not in content and not content.endswith('\\'): # Checking if the line already has any single quotes since that diff --git a/stem/version.py b/stem/version.py index 6bf0befe..71f16e2c 100644 --- a/stem/version.py +++ b/stem/version.py @@ -84,6 +84,7 @@ easily parsed and compared, for instance... ===================================== =========== """ +import functools import os import re @@ -92,11 +93,6 @@ import stem.util import stem.util.enum import stem.util.system -if stem.prereq._is_lru_cache_available(): - from functools import lru_cache -else: - from stem.util.lru_cache import lru_cache - # cache for the get_system_tor_version function VERSION_CACHE = {} @@ -150,7 +146,7 @@ def get_system_tor_version(tor_cmd = 'tor'): return VERSION_CACHE[tor_cmd] -@lru_cache() +@functools.lru_cache() def _get_version(version_str): return Version(version_str) diff --git a/test/integ/connection/authentication.py b/test/integ/connection/authentication.py index 1ab6aae0..cd562e7b 100644 --- a/test/integ/connection/authentication.py +++ b/test/integ/connection/authentication.py @@ -280,7 +280,6 @@ class TestAuthenticate(unittest.TestCase): if test.runner.Torrc.PASSWORD not in runner.get_options() or test.runner.Torrc.COOKIE in runner.get_options(): self.skipTest('(requires only password auth)') - return for i in range(10): with runner.get_tor_controller(False) as controller: diff --git a/test/integ/connection/connect.py b/test/integ/connection/connect.py index f4df233f..84b4a8fc 100644 --- a/test/integ/connection/connect.py +++ b/test/integ/connection/connect.py @@ -2,27 +2,19 @@ Integration tests for the connect_* convenience functions. """ +import io import unittest import stem.connection import test.require import test.runner -try: - from StringIO import StringIO -except ImportError: - from io import StringIO - -try: - # added in python 3.3 - from unittest.mock import patch -except ImportError: - from mock import patch +from unittest.mock import patch class TestConnect(unittest.TestCase): @test.require.controller - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) def test_connect(self, stdout_mock): """ Basic sanity checks for the connect function. @@ -41,7 +33,7 @@ class TestConnect(unittest.TestCase): self.assertEqual('', stdout_mock.getvalue()) @test.require.controller - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) def test_connect_port(self, stdout_mock): """ Basic sanity checks for the connect_port function. @@ -63,7 +55,7 @@ class TestConnect(unittest.TestCase): self.assertEqual(control_socket, None) @test.require.controller - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) def test_connect_socket_file(self, stdout_mock): """ Basic sanity checks for the connect_socket_file function. @@ -85,7 +77,7 @@ class TestConnect(unittest.TestCase): self.assertEqual(control_socket, None) @test.require.controller - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) def test_connect_to_socks_port(self, stdout_mock): """ Common user gotcha is connecting to the SocksPort or ORPort rather than the diff --git a/test/integ/control/base_controller.py b/test/integ/control/base_controller.py index 294eaee3..323b57c7 100644 --- a/test/integ/control/base_controller.py +++ b/test/integ/control/base_controller.py @@ -47,7 +47,6 @@ class TestBaseController(unittest.TestCase): if stem.util.system.is_mac(): self.skipTest('(ticket #6235)') - return with test.runner.get_runner().get_tor_socket() as control_socket: controller = stem.control.BaseController(control_socket) @@ -97,7 +96,6 @@ class TestBaseController(unittest.TestCase): if stem.util.system.is_mac(): self.skipTest('(ticket #6235)') - return with test.runner.get_runner().get_tor_socket() as control_socket: controller = stem.control.BaseController(control_socket) diff --git a/test/integ/control/controller.py b/test/integ/control/controller.py index a32f66f0..257d9fbc 100644 --- a/test/integ/control/controller.py +++ b/test/integ/control/controller.py @@ -754,71 +754,70 @@ class TestController(unittest.TestCase): """ runner = test.runner.get_runner() - tmpdir = tempfile.mkdtemp() - with runner.get_tor_controller() as controller: - try: - # successfully set a single option - connlimit = int(controller.get_conf('ConnLimit')) - controller.set_conf('connlimit', str(connlimit - 1)) - self.assertEqual(connlimit - 1, int(controller.get_conf('ConnLimit'))) - - # successfully set a single list option - exit_policy = ['accept *:7777', 'reject *:*'] - controller.set_conf('ExitPolicy', exit_policy) - self.assertEqual(exit_policy, controller.get_conf('ExitPolicy', multiple = True)) + with tempfile.TemporaryDirectory() as tmpdir: - # fail to set a single option + with runner.get_tor_controller() as controller: try: - controller.set_conf('invalidkeyboo', 'abcde') - self.fail() - except stem.InvalidArguments as exc: - self.assertEqual(['invalidkeyboo'], exc.arguments) + # successfully set a single option + connlimit = int(controller.get_conf('ConnLimit')) + controller.set_conf('connlimit', str(connlimit - 1)) + self.assertEqual(connlimit - 1, int(controller.get_conf('ConnLimit'))) - # resets configuration parameters - controller.reset_conf('ConnLimit', 'ExitPolicy') - self.assertEqual(connlimit, int(controller.get_conf('ConnLimit'))) - self.assertEqual(None, controller.get_conf('ExitPolicy')) + # successfully set a single list option + exit_policy = ['accept *:7777', 'reject *:*'] + controller.set_conf('ExitPolicy', exit_policy) + self.assertEqual(exit_policy, controller.get_conf('ExitPolicy', multiple = True)) - # successfully sets multiple config options - controller.set_options({ - 'connlimit': str(connlimit - 2), - 'contactinfo': 'stem@testing', - }) + # fail to set a single option + try: + controller.set_conf('invalidkeyboo', 'abcde') + self.fail() + except stem.InvalidArguments as exc: + self.assertEqual(['invalidkeyboo'], exc.arguments) - self.assertEqual(connlimit - 2, int(controller.get_conf('ConnLimit'))) - self.assertEqual('stem@testing', controller.get_conf('contactinfo')) + # resets configuration parameters + controller.reset_conf('ConnLimit', 'ExitPolicy') + self.assertEqual(connlimit, int(controller.get_conf('ConnLimit'))) + self.assertEqual(None, controller.get_conf('ExitPolicy')) - # fail to set multiple config options - try: + # successfully sets multiple config options controller.set_options({ + 'connlimit': str(connlimit - 2), 'contactinfo': 'stem@testing', - 'bombay': 'vadapav', }) - self.fail() - except stem.InvalidArguments as exc: - self.assertEqual(['bombay'], exc.arguments) - # context-sensitive keys (the only retched things for which order matters) - controller.set_options(( - ('HiddenServiceDir', tmpdir), - ('HiddenServicePort', '17234 127.0.0.1:17235'), - )) - - self.assertEqual(tmpdir, controller.get_conf('HiddenServiceDir')) - self.assertEqual('17234 127.0.0.1:17235', controller.get_conf('HiddenServicePort')) - finally: - # reverts configuration changes + self.assertEqual(connlimit - 2, int(controller.get_conf('ConnLimit'))) + self.assertEqual('stem@testing', controller.get_conf('contactinfo')) - controller.set_options(( - ('ExitPolicy', 'reject *:*'), - ('ConnLimit', None), - ('ContactInfo', None), - ('HiddenServiceDir', None), - ('HiddenServicePort', None), - ), reset = True) + # fail to set multiple config options + try: + controller.set_options({ + 'contactinfo': 'stem@testing', + 'bombay': 'vadapav', + }) + self.fail() + except stem.InvalidArguments as exc: + self.assertEqual(['bombay'], exc.arguments) + + # context-sensitive keys (the only retched things for which order matters) + controller.set_options(( + ('HiddenServiceDir', tmpdir), + ('HiddenServicePort', '17234 127.0.0.1:17235'), + )) + + self.assertEqual(tmpdir, controller.get_conf('HiddenServiceDir')) + self.assertEqual('17234 127.0.0.1:17235', controller.get_conf('HiddenServicePort')) + finally: + # reverts configuration changes - shutil.rmtree(tmpdir) + controller.set_options(( + ('ExitPolicy', 'reject *:*'), + ('ConnLimit', None), + ('ContactInfo', None), + ('HiddenServiceDir', None), + ('HiddenServicePort', None), + ), reset = True) @test.require.controller def test_set_conf_for_usebridges(self): @@ -1296,7 +1295,6 @@ class TestController(unittest.TestCase): if not os.path.exists(runner.get_test_dir('cached-microdescs')): self.skipTest('(no cached microdescriptors)') - return with runner.get_tor_controller() as controller: count = 0 @@ -1318,7 +1316,6 @@ class TestController(unittest.TestCase): if test.tor_version() >= Requirement.MICRODESCRIPTOR_IS_DEFAULT: self.skipTest('(requires server descriptors)') - return with runner.get_tor_controller() as controller: # we should balk at invalid content @@ -1349,7 +1346,6 @@ class TestController(unittest.TestCase): if test.tor_version() >= Requirement.MICRODESCRIPTOR_IS_DEFAULT: self.skipTest('(requires server descriptors)') - return with runner.get_tor_controller() as controller: count = 0 @@ -1534,6 +1530,5 @@ class TestController(unittest.TestCase): if TEST_ROUTER_STATUS_ENTRY is None: # this is only likely to occure if we can't get descriptors self.skipTest('(no named relays)') - return return TEST_ROUTER_STATUS_ENTRY diff --git a/test/integ/descriptor/collector.py b/test/integ/descriptor/collector.py index 3af25c29..53f45ae5 100644 --- a/test/integ/descriptor/collector.py +++ b/test/integ/descriptor/collector.py @@ -93,7 +93,6 @@ class TestCollector(unittest.TestCase): def _test_index(self, compression): if compression and not compression.available: self.skipTest('(%s unavailable)' % compression) - return collector = stem.descriptor.collector.CollecTor() index = collector.index(compression = compression) diff --git a/test/integ/descriptor/extrainfo_descriptor.py b/test/integ/descriptor/extrainfo_descriptor.py index c0a4d30c..4bfb8f3f 100644 --- a/test/integ/descriptor/extrainfo_descriptor.py +++ b/test/integ/descriptor/extrainfo_descriptor.py @@ -28,7 +28,7 @@ class TestExtraInfoDescriptor(unittest.TestCase): descriptor_path = os.path.join(test_dir, 'cached-extrainfo') if not os.path.exists(descriptor_path): - raise stem.util.test_tools.SkipTest('(no cached descriptors)') + raise unittest.case.SkipTest('(no cached descriptors)') with open(descriptor_path, 'rb') as descriptor_file: for desc in stem.descriptor.parse_file(descriptor_file, 'extra-info 1.0', validate = True): diff --git a/test/integ/descriptor/microdescriptor.py b/test/integ/descriptor/microdescriptor.py index 3f2b20cb..4e67e15b 100644 --- a/test/integ/descriptor/microdescriptor.py +++ b/test/integ/descriptor/microdescriptor.py @@ -28,7 +28,7 @@ class TestMicrodescriptor(unittest.TestCase): descriptor_path = os.path.join(test_dir, 'cached-microdescs') if not os.path.exists(descriptor_path): - raise stem.util.test_tools.SkipTest('(no cached descriptors)') + raise unittest.case.SkipTest('(no cached descriptors)') with open(descriptor_path, 'rb') as descriptor_file: for desc in stem.descriptor.parse_file(descriptor_file, 'microdescriptor 1.0', validate = True): diff --git a/test/integ/descriptor/networkstatus.py b/test/integ/descriptor/networkstatus.py index b9684e6f..efe646f9 100644 --- a/test/integ/descriptor/networkstatus.py +++ b/test/integ/descriptor/networkstatus.py @@ -43,12 +43,12 @@ class TestNetworkStatus(unittest.TestCase): consensus_path = os.path.join(test_dir, 'cached-consensus') if not os.path.exists(consensus_path): - raise stem.util.test_tools.SkipTest('(no cached-consensus)') + raise unittest.case.SkipTest('(no cached-consensus)') elif stem.util.system.is_windows(): # Unable to check memory usage on windows, so can't prevent hanging the # system if things go bad. - raise stem.util.test_tools.SkipTest('(unavailable on windows)') + raise unittest.case.SkipTest('(unavailable on windows)') count, reported_flags = 0, [] @@ -79,9 +79,9 @@ class TestNetworkStatus(unittest.TestCase): consensus_path = os.path.join(test_dir, 'cached-microdesc-consensus') if not os.path.exists(consensus_path): - raise stem.util.test_tools.SkipTest('(no cached-microdesc-consensus)') + raise unittest.case.SkipTest('(no cached-microdesc-consensus)') elif stem.util.system.is_windows(): - raise stem.util.test_tools.SkipTest('(unavailable on windows)') + raise unittest.case.SkipTest('(unavailable on windows)') count, reported_flags = 0, [] diff --git a/test/integ/descriptor/server_descriptor.py b/test/integ/descriptor/server_descriptor.py index a62ee8f6..8aa4388c 100644 --- a/test/integ/descriptor/server_descriptor.py +++ b/test/integ/descriptor/server_descriptor.py @@ -28,7 +28,7 @@ class TestServerDescriptor(unittest.TestCase): descriptor_path = os.path.join(test_dir, 'cached-descriptors') if not os.path.exists(descriptor_path): - raise stem.util.test_tools.SkipTest('(no cached descriptors)') + raise unittest.case.SkipTest('(no cached descriptors)') with open(descriptor_path, 'rb') as descriptor_file: for desc in stem.descriptor.parse_file(descriptor_file, 'server-descriptor 1.0', validate = True): diff --git a/test/integ/directory/fallback.py b/test/integ/directory/fallback.py index e12b2659..83c87a8d 100644 --- a/test/integ/directory/fallback.py +++ b/test/integ/directory/fallback.py @@ -33,7 +33,6 @@ class TestFallback(unittest.TestCase): # added so many fallbacks now that this takes a looong time. :( self.skipTest('(skipped by default)') - return unsuccessful = {} downloader = stem.descriptor.remote.DescriptorDownloader() diff --git a/test/integ/installation.py b/test/integ/installation.py index 2ac655aa..2f41ef58 100644 --- a/test/integ/installation.py +++ b/test/integ/installation.py @@ -110,9 +110,9 @@ class TestInstallation(unittest.TestCase): git_dir = os.path.join(test.STEM_BASE, '.git') if not stem.util.system.is_available('git'): - raise stem.util.test_tools.SkipTest('(git unavailable)') + raise unittest.case.SkipTest('(git unavailable)') elif not os.path.exists(git_dir): - raise stem.util.test_tools.SkipTest('(not a git checkout)') + raise unittest.case.SkipTest('(not a git checkout)') if os.path.exists(DIST_PATH): raise AssertionError("%s already exists, maybe you manually ran 'python setup.py sdist'?" % DIST_PATH) @@ -127,8 +127,8 @@ class TestInstallation(unittest.TestCase): # tarball has a prefix 'stem-[verion]' directory so stipping that out - dist_tar = tarfile.open(os.path.join(DIST_PATH, 'stem-dry-run-%s.tar.gz' % stem.__version__)) - tar_contents = ['/'.join(info.name.split('/')[1:]) for info in dist_tar.getmembers() if info.isfile()] + with tarfile.open(os.path.join(DIST_PATH, 'stem-dry-run-%s.tar.gz' % stem.__version__)) as dist_tar: + tar_contents = ['/'.join(info.name.split('/')[1:]) for info in dist_tar.getmembers() if info.isfile()] issues = [] diff --git a/test/integ/interpreter.py b/test/integ/interpreter.py index 65d03fd3..32c52c28 100644 --- a/test/integ/interpreter.py +++ b/test/integ/interpreter.py @@ -38,10 +38,8 @@ class TestInterpreter(unittest.TestCase): if test.runner.Torrc.PASSWORD in test.runner.get_runner().get_options(): self.skipTest('password auth unsupported') - return elif not READLINE_AVAILABLE: self.skipTest('readline unavailable') - return expected = ['250-config-file=%s' % test.runner.get_runner().get_torrc_path(), '250 OK'] self.assertEqual(expected, _run_prompt('--run', 'GETINFO config-file')) @@ -50,10 +48,8 @@ class TestInterpreter(unittest.TestCase): def test_running_file(self): if test.runner.Torrc.PASSWORD in test.runner.get_runner().get_options(): self.skipTest('password auth unsupported') - return elif not READLINE_AVAILABLE: self.skipTest('readline unavailable') - return expected = [ '250-config-file=%s' % test.runner.get_runner().get_torrc_path(), diff --git a/test/integ/manual.py b/test/integ/manual.py index ae0b2b16..3e721ac6 100644 --- a/test/integ/manual.py +++ b/test/integ/manual.py @@ -59,9 +59,6 @@ EXPECTED_EXIT_POLICY_DESCRIPTION_END = 'it applies to both IPv4 and IPv6 address class TestManual(unittest.TestCase): - # TODO: remove when dropping support for python 2.6 - skip_reason = 'setUpClass() unsupported in python 2.6' - @classmethod def setUpClass(self): self.man_path = None @@ -91,15 +88,14 @@ class TestManual(unittest.TestCase): if self.man_path and os.path.exists(self.man_path): os.remove(self.man_path) + # TODO: replace with a 'require' annotation + def requires_downloaded_manual(self): if self.skip_reason: self.skipTest(self.skip_reason) - return True elif self.download_error: self.fail(self.download_error) - return False - def test_escapes_non_ascii(self): """ Check that our manual parser escapes all non-ascii characters. If this @@ -108,8 +104,7 @@ class TestManual(unittest.TestCase): stem/manual.py's _get_categories(). """ - if self.requires_downloaded_manual(): - return + self.requires_downloaded_manual() def check(content): try: @@ -131,8 +126,7 @@ class TestManual(unittest.TestCase): it has indented lines within it. Ensure we parse this correctly. """ - if self.requires_downloaded_manual(): - return + self.requires_downloaded_manual() manual = stem.manual.Manual.from_man(self.man_path) self.assertTrue(manual.config_options['ExitPolicy'].description.startswith(EXPECTED_EXIT_POLICY_DESCRIPTION_START)) @@ -143,8 +137,7 @@ class TestManual(unittest.TestCase): Check if the cached manual information bundled with Stem is up to date or not. """ - if self.requires_downloaded_manual(): - return + self.requires_downloaded_manual() cached_manual = stem.manual.Manual.from_cache() latest_manual = stem.manual.Manual.from_man(self.man_path) @@ -158,8 +151,7 @@ class TestManual(unittest.TestCase): then go ahead and simply update these assertions. """ - if self.requires_downloaded_manual(): - return + self.requires_downloaded_manual() def assert_equal(category, expected, actual): if expected != actual: @@ -208,8 +200,7 @@ class TestManual(unittest.TestCase): class to match. """ - if self.requires_downloaded_manual(): - return + self.requires_downloaded_manual() categories = stem.manual._get_categories(self.man_content) @@ -230,8 +221,7 @@ class TestManual(unittest.TestCase): Check that all the configuration options tor supports are in the man page. """ - if self.requires_downloaded_manual(): - return + self.requires_downloaded_manual() with test.runner.get_runner().get_tor_controller() as controller: config_options_in_tor = set([line.split()[0] for line in controller.get_info('config/names').splitlines() if line.split()[1] != 'Virtual']) diff --git a/test/integ/process.py b/test/integ/process.py index 26346ece..e40c501a 100644 --- a/test/integ/process.py +++ b/test/integ/process.py @@ -9,7 +9,6 @@ import hashlib import os import random import re -import shutil import subprocess import tempfile import threading @@ -28,13 +27,9 @@ import test import test.require from contextlib import contextmanager -from stem.util.test_tools import asynchronous, assert_equal, assert_in, skip +from unittest.mock import patch, Mock -try: - # added in python 3.3 - from unittest.mock import patch, Mock -except ImportError: - from mock import patch, Mock +from stem.util.test_tools import asynchronous, assert_equal, assert_in, skip BASIC_RELAY_TORRC = """\ SocksPort 9089 @@ -57,18 +52,8 @@ def random_port(): @contextmanager -def tmp_directory(): - tmp_dir = tempfile.mkdtemp() - - try: - yield tmp_dir - finally: - shutil.rmtree(tmp_dir) - - -@contextmanager def torrc(): - with tmp_directory() as data_directory: + with tempfile.TemporaryDirectory() as data_directory: torrc_path = os.path.join(data_directory, 'torrc') with open(torrc_path, 'w') as torrc_file: @@ -106,7 +91,7 @@ def run_tor(tor_cmd, *args, **kwargs): elif not exit_status and expect_failure: raise AssertionError("Didn't expect tor to be able to start when we run: %s\n%s" % (' '.join(args), stdout)) - return stem.util.str_tools._to_unicode(stdout) if stem.prereq.is_python_3() else stdout + return stem.util.str_tools._to_unicode(stdout) class TestProcess(unittest.TestCase): @@ -177,12 +162,8 @@ class TestProcess(unittest.TestCase): # I'm not gonna even pretend to understand the following. Ported directly # from tor's test_cmdline_args.py. - if stem.prereq.is_python_3(): - output_hex = binascii.a2b_hex(stem.util.str_tools._to_bytes(output).strip()[3:]) - salt, how, hashed = output_hex[:8], output_hex[8], output_hex[9:] - else: - output_hex = binascii.a2b_hex(output.strip()[3:]) - salt, how, hashed = output_hex[:8], ord(output_hex[8]), output_hex[9:] + output_hex = binascii.a2b_hex(stem.util.str_tools._to_bytes(output).strip()[3:]) + salt, how, hashed = output_hex[:8], output_hex[8], output_hex[9:] count = (16 + (how & 15)) << ((how >> 4) + 6) stuff = salt + b'my_password' @@ -236,7 +217,7 @@ class TestProcess(unittest.TestCase): output = run_tor(tor_cmd, '--list-fingerprint', with_torrc = True, expect_failure = True) assert_in("Clients don't have long-term identity keys. Exiting.", output, 'Should fail to start due to lacking an ORPort') - with tmp_directory() as data_directory: + with tempfile.TemporaryDirectory() as data_directory: torrc_path = os.path.join(data_directory, 'torrc') with open(torrc_path, 'w') as torrc_file: @@ -332,7 +313,7 @@ class TestProcess(unittest.TestCase): if test.tor_version() < stem.version.Requirement.TORRC_VIA_STDIN: skip('(requires %s)' % stem.version.Requirement.TORRC_VIA_STDIN) - with tmp_directory() as data_directory: + with tempfile.TemporaryDirectory() as data_directory: torrc = BASIC_RELAY_TORRC % data_directory output = run_tor(tor_cmd, '-f', '-', '--dump-config', 'short', stdin = torrc) assert_equal(sorted(torrc.splitlines()), sorted(output.splitlines())) @@ -390,7 +371,7 @@ class TestProcess(unittest.TestCase): it isn't. """ - with tmp_directory() as data_directory: + with tempfile.TemporaryDirectory() as data_directory: # Tries running tor in another thread with the given timeout argument. This # issues an invalid torrc so we terminate right away if we get to the point # of actually invoking tor. @@ -443,7 +424,7 @@ class TestProcess(unittest.TestCase): Exercises launch_tor_with_config when we write a torrc to disk. """ - with tmp_directory() as data_directory: + with tempfile.TemporaryDirectory() as data_directory: control_port = random_port() control_socket, tor_process = None, None @@ -487,7 +468,7 @@ class TestProcess(unittest.TestCase): if test.tor_version() < stem.version.Requirement.TORRC_VIA_STDIN: skip('(requires %s)' % stem.version.Requirement.TORRC_VIA_STDIN) - with tmp_directory() as data_directory: + with tempfile.TemporaryDirectory() as data_directory: control_port = random_port() control_socket, tor_process = None, None @@ -529,7 +510,7 @@ class TestProcess(unittest.TestCase): # [warn] Failed to parse/validate config: Failed to bind one of the listener ports. # [err] Reading config failed--see warnings above. - with tmp_directory() as data_directory: + with tempfile.TemporaryDirectory() as data_directory: both_ports = random_port() try: @@ -551,7 +532,7 @@ class TestProcess(unittest.TestCase): Runs launch_tor where it times out before completing. """ - with tmp_directory() as data_directory: + with tempfile.TemporaryDirectory() as data_directory: start_time = time.time() try: @@ -583,7 +564,7 @@ class TestProcess(unittest.TestCase): elif test.tor_version() < stem.version.Requirement.TAKEOWNERSHIP: skip('(requires %s)' % stem.version.Requirement.TAKEOWNERSHIP) - with tmp_directory() as data_directory: + with tempfile.TemporaryDirectory() as data_directory: sleep_process = subprocess.Popen(['sleep', '60']) tor_process = stem.process.launch_tor_with_config( @@ -627,7 +608,7 @@ class TestProcess(unittest.TestCase): if test.tor_version() < stem.version.Requirement.TAKEOWNERSHIP: skip('(requires %s)' % stem.version.Requirement.TAKEOWNERSHIP) - with tmp_directory() as data_directory: + with tempfile.TemporaryDirectory() as data_directory: control_port = random_port() tor_process = stem.process.launch_tor_with_config( diff --git a/test/integ/response/protocolinfo.py b/test/integ/response/protocolinfo.py index 5d8c74f6..917b87c3 100644 --- a/test/integ/response/protocolinfo.py +++ b/test/integ/response/protocolinfo.py @@ -14,11 +14,7 @@ import test.integ.util.system import test.require import test.runner -try: - # added in python 3.3 - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch +from unittest.mock import Mock, patch class TestProtocolInfo(unittest.TestCase): diff --git a/test/integ/util/connection.py b/test/integ/util/connection.py index ed55ad89..861b8ba9 100644 --- a/test/integ/util/connection.py +++ b/test/integ/util/connection.py @@ -4,6 +4,7 @@ that we're running. """ import unittest +import urllib.request import stem import stem.util.connection @@ -13,12 +14,6 @@ import test.runner from stem.util.connection import Resolver -try: - # account for urllib's change between python 2.x and 3.x - import urllib.request as urllib -except ImportError: - import urllib2 as urllib - class TestConnection(unittest.TestCase): @test.require.ptrace @@ -27,10 +22,8 @@ class TestConnection(unittest.TestCase): if test.runner.Torrc.PORT not in runner.get_options(): self.skipTest('(no control port)') - return elif resolver not in stem.util.connection.system_resolvers(): self.skipTest('(resolver unavailable on this platform)') - return with runner.get_tor_socket(): connections = stem.util.connection.get_connections(resolver, process_pid = runner.get_pid()) @@ -60,7 +53,7 @@ class TestConnection(unittest.TestCase): self.assertEqual('Failed to download from https://no.such.testing.url (URLError): Name or service not known', str(exc)) self.assertEqual('https://no.such.testing.url', exc.url) self.assertEqual('Name or service not known', exc.error.reason.strerror) - self.assertEqual(urllib.URLError, type(exc.error)) + self.assertEqual(urllib.request.URLError, type(exc.error)) def test_connections_by_proc(self): self.check_resolver(Resolver.PROC) diff --git a/test/integ/util/proc.py b/test/integ/util/proc.py index da4d6efc..315082d5 100644 --- a/test/integ/util/proc.py +++ b/test/integ/util/proc.py @@ -76,7 +76,6 @@ class TestProc(unittest.TestCase): return elif not os.access('/proc/net/tcp', os.R_OK) or not os.access('/proc/net/udp', os.R_OK): self.skipTest('(proc lacks read permissions)') - return # making a controller connection so that we have something to query for with runner.get_tor_socket(): diff --git a/test/integ/util/system.py b/test/integ/util/system.py index 89019685..2a7ac07a 100644 --- a/test/integ/util/system.py +++ b/test/integ/util/system.py @@ -14,13 +14,9 @@ import stem.util.system import test.require import test.runner -from stem.util.system import State, DaemonTask +from unittest.mock import Mock, patch -try: - # added in python 3.3 - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch +from stem.util.system import State, DaemonTask def filter_system_call(prefixes): @@ -287,15 +283,12 @@ class TestSystem(unittest.TestCase): if stem.util.system.is_windows(): self.skipTest('(unavailable on windows)') - return elif stem.util.system.is_mac() or stem.util.system.is_gentoo(): self.skipTest('(resolvers unavailable)') - return elif not stem.util.system.is_available('netstat') or \ stem.util.system.is_available('sockstat') or \ stem.util.system.is_available('lsof'): self.skipTest('(connection resolvers unavailable)') - return runner = test.runner.get_runner() tor_pid, tor_port = runner.get_pid(), test.runner.CONTROL_PORT @@ -313,7 +306,6 @@ class TestSystem(unittest.TestCase): if stem.util.system.is_gentoo(): self.skipTest('(unavailable on gentoo)') - return netstat_prefix = stem.util.system.GET_PID_BY_PORT_NETSTAT @@ -353,7 +345,6 @@ class TestSystem(unittest.TestCase): if stem.util.system.is_mac() or stem.util.system.is_gentoo(): self.skipTest('(resolvers unavailable)') - return lsof_prefix = stem.util.system.GET_PID_BY_PORT_LSOF @@ -370,13 +361,14 @@ class TestSystem(unittest.TestCase): Checks the stem.util.system.pid_by_open_file function. """ + # check a directory that doesn't exist + + self.assertEqual(None, stem.util.system.pid_by_open_file('/no/such/path')) + # check a directory that exists, but isn't claimed by any application - tmpdir = tempfile.mkdtemp() - self.assertEqual(None, stem.util.system.pid_by_open_file(tmpdir)) - # check a directory that doesn't exist - os.rmdir(tmpdir) - self.assertEqual(None, stem.util.system.pid_by_open_file(tmpdir)) + with tempfile.TemporaryDirectory() as tmpdir: + self.assertEqual(None, stem.util.system.pid_by_open_file(tmpdir)) @require_path def test_pids_by_user(self): @@ -397,7 +389,6 @@ class TestSystem(unittest.TestCase): if stem.util.system.is_windows(): self.skipTest('(unavailable on windows)') - return runner = test.runner.get_runner() runner_pid, tor_cwd = runner.get_pid(), runner.get_tor_cwd() @@ -537,7 +528,6 @@ class TestSystem(unittest.TestCase): if getpass.getuser() == 'root': self.skipTest('(running as root)') - return self.assertEqual(os.getcwd(), stem.util.system.expand_path('.')) self.assertEqual(os.getcwd(), stem.util.system.expand_path('./')) @@ -568,7 +558,6 @@ class TestSystem(unittest.TestCase): if stem.prereq.is_pypy(): self.skipTest('(unimplemented for pypy)') - return initial_name = stem.util.system.get_process_name() self.assertTrue('run_tests.py' in initial_name) diff --git a/test/network.py b/test/network.py index 5072c5b6..5fea625e 100644 --- a/test/network.py +++ b/test/network.py @@ -127,9 +127,7 @@ class Socks(_socket_socket): :param int expected_size: number of bytes to return - :returns: - * **str** in Python 2 (bytes is str) - * **bytes** in Python 3 + :returns: **bytes** for the requested content :raises: * :class:`socket.error` for socket errors @@ -152,32 +150,21 @@ class Socks(_socket_socket): :param list integers: list of ints to convert - :returns: - * **str** in Python 2 (bytes is str) - * **bytes** in Python 3 + :returns: **bytes** for the requested content """ - if bytes is str: - bytes_ = ''.join([chr(x) for x in integers]) # Python 2 - else: - bytes_ = bytes(integers) # Python 3 - return bytes_ + return bytes(integers) def _bytes_to_ints(self, bytes_): """ - Returns a tuple of integers converted from a string (Python 2) or - bytes (Python 3). + Returns a tuple of integers converted from bytes. - :param str,bytes bytes_: byte string to convert + :param bytes bytes_: byte string to convert :returns: **list** of ints """ - try: - integers = [ord(x) for x in bytes_] # Python 2 - except TypeError: - integers = [x for x in bytes_] # Python 3 - return tuple(integers) + return tuple([x for x in bytes_]) def _pack_string(self, string_): """ @@ -185,16 +172,10 @@ class Socks(_socket_socket): :param str string_: string to convert - :returns: - * **str** in Python 2 (bytes is str) - * **bytes** in Python 3 + :returns: **bytes** for the requested content """ - try: - return struct.pack('>%ss' % len(string_), string_) - except struct.error: - # Python 3: encode str to bytes - return struct.pack('>%ss' % len(string_), string_.encode()) + return struct.pack('>%ss' % len(string_), string_.encode()) def connect(self, address): """ diff --git a/test/task.py b/test/task.py index 1fbcf9a4..19026165 100644 --- a/test/task.py +++ b/test/task.py @@ -14,7 +14,6 @@ |- PYTHON_VERSION - checks our python version |- PLATFORM_VERSION - checks our operating system version |- CRYPTO_VERSION - checks our version of cryptography - |- MOCK_VERSION - checks our version of mock |- PYFLAKES_VERSION - checks our version of pyflakes |- PYCODESTYLE_VERSION - checks our version of pycodestyle |- CLEAN_PYC - removes any *.pyc without a corresponding *.py @@ -25,6 +24,7 @@ +- PYCODESTYLE_TASK - style checks """ +import importlib import os import platform import re @@ -46,14 +46,6 @@ from test.output import STATUS, ERROR, NO_NL, println TASK_DESCRIPTION_WIDTH = 40 -try: - # TODO: remove check when dropping python 2.6 support - - import importlib - HAS_IMPORTLIB = True -except ImportError: - HAS_IMPORTLIB = False - CONFIG = stem.util.conf.config_dict('test', { 'integ.test_directory': './test/data', 'test.unit_tests': '', @@ -163,9 +155,6 @@ def _import_tests(): register if they're asynchronous. """ - if not HAS_IMPORTLIB: - return - for module in (CONFIG['test.unit_tests'].splitlines() + CONFIG['test.integ_tests'].splitlines()): try: importlib.import_module(module.rsplit('.', 1)[0]) @@ -273,7 +262,7 @@ class Task(object): self.is_successful = True output_msg = 'running' if self._is_background_task else 'done' - if self.result and self.print_result and stem.util._is_str(self.result): + if self.result and self.print_result and isinstance(self.result, (bytes, str)): output_msg = self.result elif self.print_runtime: output_msg += ' (%0.1fs)' % (time.time() - start_time) @@ -305,7 +294,7 @@ class ModuleVersion(Task): def version_check(): if prereq_check is None or prereq_check(): for module in modules: - if HAS_IMPORTLIB and stem.util.test_tools._module_exists(module): + if stem.util.test_tools._module_exists(module): return importlib.import_module(module).__version__ return 'missing' @@ -333,7 +322,6 @@ TOR_VERSION = Task('tor version', _check_tor_version) PYTHON_VERSION = Task('python version', _check_python_version) PLATFORM_VERSION = Task('operating system', _check_platform_version) CRYPTO_VERSION = ModuleVersion('cryptography version', 'cryptography', stem.prereq.is_crypto_available) -MOCK_VERSION = ModuleVersion('mock version', ['unittest.mock', 'mock'], stem.prereq.is_mock_available) PYFLAKES_VERSION = ModuleVersion('pyflakes version', 'pyflakes') PYCODESTYLE_VERSION = ModuleVersion('pycodestyle version', ['pycodestyle', 'pep8']) CLEAN_PYC = Task('checking for orphaned .pyc files', _clean_orphaned_pyc, (SRC_PATHS,), print_runtime = True) diff --git a/test/unit/client/size.py b/test/unit/client/size.py index dcd5fea3..d3bb4a4a 100644 --- a/test/unit/client/size.py +++ b/test/unit/client/size.py @@ -11,8 +11,6 @@ from stem.client.datatype import Size class TestSize(unittest.TestCase): def test_attributes(self): self.assertEqual('CHAR', Size.CHAR.name) - self.assertEqual('!B', Size.CHAR.format) - self.assertEqual(1, Size.CHAR.size) self.assertEqual(2, Size.SHORT.size) self.assertEqual(4, Size.LONG.size) @@ -27,8 +25,8 @@ class TestSize(unittest.TestCase): self.assertRaisesWith(ValueError, 'Size.pack encodes an integer, but was a str', Size.CHAR.pack, 'hi') self.assertRaisesWith(ValueError, 'Packed values must be positive (attempted to pack -1 as a CHAR)', Size.CHAR.pack, -1) - bad_size = Size('BAD_SIZE', 1, '!H') - self.assertRaisesRegexp(ValueError, re.escape("'\\x00\\x12' is the wrong size for a BAD_SIZE field"), bad_size.pack, 18) + too_small = Size('TOO_SMALL', 1) + self.assertRaises(OverflowError, too_small.pack, 1800) def test_unpack(self): self.assertEqual(18, Size.CHAR.unpack(b'\x12')) diff --git a/test/unit/connection/authentication.py b/test/unit/connection/authentication.py index 117b39d9..f6241e0e 100644 --- a/test/unit/connection/authentication.py +++ b/test/unit/connection/authentication.py @@ -14,15 +14,11 @@ import unittest import stem.connection import test +from unittest.mock import Mock, patch + from stem.response import ControlMessage from stem.util import log -try: - # added in python 3.3 - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch - class TestAuthenticate(unittest.TestCase): @patch('stem.connection.get_protocolinfo') diff --git a/test/unit/connection/connect.py b/test/unit/connection/connect.py index 37ded2f5..175a1ebd 100644 --- a/test/unit/connection/connect.py +++ b/test/unit/connection/connect.py @@ -2,25 +2,18 @@ Unit tests for the stem.connection.connect function. """ +import io import unittest -try: - from StringIO import StringIO -except ImportError: - from io import StringIO - -try: - from mock import Mock, patch -except ImportError: - from unittest.mock import Mock, patch - import stem import stem.connection import stem.socket +from unittest.mock import Mock, patch + class TestConnect(unittest.TestCase): - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) @patch('stem.util.system.is_running') @patch('os.path.exists', Mock(return_value = True)) @patch('stem.socket.ControlSocketFile', Mock(side_effect = stem.SocketError('failed'))) @@ -33,7 +26,7 @@ class TestConnect(unittest.TestCase): is_running_mock.return_value = True self._assert_connect_fails_with({}, stdout_mock, "Unable to connect to tor. Maybe it's running without a ControlPort?") - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) @patch('os.path.exists') @patch('stem.util.system.is_running', Mock(return_value = True)) @patch('stem.socket.ControlSocketFile', Mock(side_effect = stem.SocketError('failed'))) @@ -121,7 +114,7 @@ class TestConnect(unittest.TestCase): authenticate_mock.assert_any_call(control_socket, None, None) authenticate_mock.assert_any_call(control_socket, 'my_password', None) - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) @patch('stem.connection.authenticate') def test_auth_failure(self, authenticate_mock, stdout_mock): control_socket = stem.socket.ControlPort(connect = False) diff --git a/test/unit/control/controller.py b/test/unit/control/controller.py index 94c1b65f..9628c913 100644 --- a/test/unit/control/controller.py +++ b/test/unit/control/controller.py @@ -14,17 +14,13 @@ import stem.socket import stem.util.system import stem.version +from unittest.mock import Mock, patch + from stem import ControllerError, DescriptorUnavailable, InvalidArguments, InvalidRequest, ProtocolError, UnsatisfiableRequest from stem.control import MALFORMED_EVENTS, _parse_circ_path, Listener, Controller, EventType from stem.response import ControlMessage from stem.exit_policy import ExitPolicy -try: - # added in python 3.3 - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch - NS_DESC = 'r %s %s u5lTXJKGsLKufRLnSyVqT7TdGYw 2012-12-30 22:02:49 77.223.43.54 9001 0\ns Fast Named Running Stable Valid\nw Bandwidth=75' TEST_TIMESTAMP = 12345 diff --git a/test/unit/descriptor/bandwidth_file.py b/test/unit/descriptor/bandwidth_file.py index 3040c38b..9bee5f95 100644 --- a/test/unit/descriptor/bandwidth_file.py +++ b/test/unit/descriptor/bandwidth_file.py @@ -2,26 +2,17 @@ Unit tests for stem.descriptor.bandwidth_file. """ +import collections import datetime import unittest import stem.descriptor +from unittest.mock import Mock, patch + from stem.descriptor.bandwidth_file import BandwidthFile from test.unit.descriptor import get_resource -try: - # added in python 2.7 - from collections import OrderedDict -except ImportError: - from stem.util.ordereddict import OrderedDict - -try: - # added in python 3.3 - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch - EXPECTED_MEASUREMENT_1 = { 'scanner': '/scanner.1/scan-data/bws-0.0:0.8-done-2019-01-13-22:55:22', 'measured_at': '1547441722', @@ -272,7 +263,7 @@ class TestBandwidthFile(unittest.TestCase): Exercise the example in our content method's pydoc. """ - content = BandwidthFile.content(OrderedDict([ + content = BandwidthFile.content(collections.OrderedDict([ ('timestamp', '12345'), ('version', '1.2.0'), ('content', []), @@ -286,7 +277,7 @@ class TestBandwidthFile(unittest.TestCase): Include an unrecognized header field. """ - desc = BandwidthFile.create(OrderedDict([('version', '1.1.0'), ('new_header', 'neat stuff')])) + desc = BandwidthFile.create(collections.OrderedDict([('version', '1.1.0'), ('new_header', 'neat stuff')])) self.assertEqual(EXPECTED_NEW_HEADER_CONTENT, str(desc)) self.assertEqual('1.1.0', desc.version) self.assertEqual({'version': '1.1.0', 'new_header': 'neat stuff'}, desc.header) @@ -309,7 +300,7 @@ class TestBandwidthFile(unittest.TestCase): self.assertRaisesWith(ValueError, "The 'version' header must be in the second position", BandwidthFile.from_str, WRONG_VERSION_POSITION, validate = True) - content = BandwidthFile.content(OrderedDict([ + content = BandwidthFile.content(collections.OrderedDict([ ('timestamp', '1410723598'), ('file_created', '2019-01-14T05:35:06'), ('version', '1.1.0'), diff --git a/test/unit/descriptor/collector.py b/test/unit/descriptor/collector.py index b4ff7636..99e19d7c 100644 --- a/test/unit/descriptor/collector.py +++ b/test/unit/descriptor/collector.py @@ -8,20 +8,13 @@ import unittest import stem.prereq +from unittest.mock import Mock, patch + from stem.descriptor import Compression, DocumentHandler from stem.descriptor.collector import CollecTor, File from test.unit.descriptor import get_resource from test.unit.descriptor.data.collector.index import EXAMPLE_INDEX -try: - # added in python 3.3 - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch - -URL_OPEN = 'urllib.request.urlopen' if stem.prereq.is_python_3() else 'urllib2.urlopen' - - with open(get_resource('collector/index.json'), 'rb') as index_file: EXAMPLE_INDEX_JSON = index_file.read() @@ -60,7 +53,7 @@ class TestCollector(unittest.TestCase): # tests for the CollecTor class - @patch(URL_OPEN) + @patch('urllib.request.urlopen') def test_index_plaintext(self, urlopen_mock): urlopen_mock.return_value = io.BytesIO(EXAMPLE_INDEX_JSON) @@ -68,11 +61,10 @@ class TestCollector(unittest.TestCase): self.assertEqual(EXAMPLE_INDEX, collector.index(Compression.PLAINTEXT)) urlopen_mock.assert_called_with('https://collector.torproject.org/index/index.json', timeout = None) - @patch(URL_OPEN) + @patch('urllib.request.urlopen') def test_index_gzip(self, urlopen_mock): if not Compression.GZIP.available: self.skipTest('(gzip compression unavailable)') - return import zlib urlopen_mock.return_value = io.BytesIO(zlib.compress(EXAMPLE_INDEX_JSON)) @@ -81,11 +73,10 @@ class TestCollector(unittest.TestCase): self.assertEqual(EXAMPLE_INDEX, collector.index(Compression.GZIP)) urlopen_mock.assert_called_with('https://collector.torproject.org/index/index.json.gz', timeout = None) - @patch(URL_OPEN) + @patch('urllib.request.urlopen') def test_index_bz2(self, urlopen_mock): if not Compression.BZ2.available: self.skipTest('(bz2 compression unavailable)') - return import bz2 urlopen_mock.return_value = io.BytesIO(bz2.compress(EXAMPLE_INDEX_JSON)) @@ -94,11 +85,10 @@ class TestCollector(unittest.TestCase): self.assertEqual(EXAMPLE_INDEX, collector.index(Compression.BZ2)) urlopen_mock.assert_called_with('https://collector.torproject.org/index/index.json.bz2', timeout = None) - @patch(URL_OPEN) + @patch('urllib.request.urlopen') def test_index_lzma(self, urlopen_mock): if not Compression.LZMA.available: self.skipTest('(lzma compression unavailable)') - return import lzma urlopen_mock.return_value = io.BytesIO(lzma.compress(EXAMPLE_INDEX_JSON)) @@ -107,7 +97,7 @@ class TestCollector(unittest.TestCase): self.assertEqual(EXAMPLE_INDEX, collector.index(Compression.LZMA)) urlopen_mock.assert_called_with('https://collector.torproject.org/index/index.json.xz', timeout = None) - @patch(URL_OPEN) + @patch('urllib.request.urlopen') def test_index_retries(self, urlopen_mock): urlopen_mock.side_effect = IOError('boom') @@ -121,21 +111,17 @@ class TestCollector(unittest.TestCase): self.assertRaisesRegexp(IOError, 'boom', collector.index) self.assertEqual(5, urlopen_mock.call_count) - @patch(URL_OPEN, Mock(return_value = io.BytesIO(b'not json'))) + @patch('urllib.request.urlopen', Mock(return_value = io.BytesIO(b'not json'))) def test_index_malformed_json(self): collector = CollecTor() - - if stem.prereq.is_python_3(): - self.assertRaisesRegexp(ValueError, 'Expecting value: line 1 column 1', collector.index, Compression.PLAINTEXT) - else: - self.assertRaisesRegexp(ValueError, 'No JSON object could be decoded', collector.index, Compression.PLAINTEXT) + self.assertRaisesRegexp(ValueError, 'Expecting value: line 1 column 1', collector.index, Compression.PLAINTEXT) def test_index_malformed_compression(self): for compression in (Compression.GZIP, Compression.BZ2, Compression.LZMA): if not compression.available: continue - with patch(URL_OPEN, Mock(return_value = io.BytesIO(b'not compressed'))): + with patch('urllib.request.urlopen', Mock(return_value = io.BytesIO(b'not compressed'))): collector = CollecTor() self.assertRaisesRegexp(IOError, 'Failed to decompress as %s' % compression, collector.index, compression) diff --git a/test/unit/descriptor/compression.py b/test/unit/descriptor/compression.py index 3945bc9c..a51bba62 100644 --- a/test/unit/descriptor/compression.py +++ b/test/unit/descriptor/compression.py @@ -32,7 +32,6 @@ class TestCompression(unittest.TestCase): if not compression.available: self.skipTest('(%s unavailable)' % compression) - return with open(get_resource(filename), 'rb') as compressed_file: content = compression.decompress(compressed_file.read()) diff --git a/test/unit/descriptor/export.py b/test/unit/descriptor/export.py index a33c1d42..d27ed241 100644 --- a/test/unit/descriptor/export.py +++ b/test/unit/descriptor/export.py @@ -2,13 +2,9 @@ Unit tests for stem.descriptor.export. """ +import io import unittest -try: - from StringIO import StringIO -except ImportError: - from io import StringIO - import stem.prereq from stem.descriptor.server_descriptor import RelayDescriptor, BridgeDescriptor @@ -21,10 +17,6 @@ class TestExport(unittest.TestCase): Exports a single minimal tor server descriptor. """ - if stem.prereq._is_python_26(): - self.skipTest('(header added in python 2.7)') - return - desc = RelayDescriptor.create({ 'router': 'caerSidi 71.35.133.197 9001 0 0', 'published': '2012-03-01 17:15:27', @@ -63,7 +55,7 @@ class TestExport(unittest.TestCase): desc = RelayDescriptor.create() desc_csv = export_csv(desc) - csv_buffer = StringIO() + csv_buffer = io.StringIO() export_csv_file(csv_buffer, desc) self.assertEqual(desc_csv, csv_buffer.getvalue()) @@ -73,10 +65,6 @@ class TestExport(unittest.TestCase): Checks that the default attributes for our csv output doesn't include private fields. """ - if stem.prereq._is_python_26(): - self.skipTest('(header added in python 2.7)') - return - desc = RelayDescriptor.create() desc_csv = export_csv(desc) diff --git a/test/unit/descriptor/hidden_service_v3.py b/test/unit/descriptor/hidden_service_v3.py index 33507314..4004a94d 100644 --- a/test/unit/descriptor/hidden_service_v3.py +++ b/test/unit/descriptor/hidden_service_v3.py @@ -3,6 +3,7 @@ Unit tests for stem.descriptor.hidden_service for version 3. """ import base64 +import collections import functools import unittest @@ -13,6 +14,8 @@ import stem.prereq import test.require +from unittest.mock import patch, Mock + from stem.descriptor.hidden_service import ( IntroductionPointV3, HiddenServiceDescriptorV3, @@ -27,18 +30,6 @@ from test.unit.descriptor import ( base_expect_invalid_attr_for_text, ) -try: - # added in python 2.7 - from collections import OrderedDict -except ImportError: - from stem.util.ordereddict import OrderedDict - -try: - # added in python 3.3 - from unittest.mock import patch, Mock -except ImportError: - from mock import patch, Mock - require_sha3 = test.require.needs(stem.prereq._is_sha3_available, 'requires sha3') require_x25519 = test.require.needs(lambda: stem.descriptor.hidden_service.X25519_AVAILABLE, 'requires openssl x5509') @@ -328,7 +319,7 @@ class TestHiddenServiceDescriptorV3(unittest.TestCase): # include optional parameters - desc = InnerLayer.create(OrderedDict(( + desc = InnerLayer.create(collections.OrderedDict(( ('intro-auth-required', 'ed25519'), ('single-onion-service', ''), ))) diff --git a/test/unit/descriptor/networkstatus/document_v3.py b/test/unit/descriptor/networkstatus/document_v3.py index ce7cfdb7..93f98d77 100644 --- a/test/unit/descriptor/networkstatus/document_v3.py +++ b/test/unit/descriptor/networkstatus/document_v3.py @@ -2,6 +2,7 @@ Unit tests for the NetworkStatusDocumentV3 of stem.descriptor.networkstatus. """ +import collections import datetime import io import unittest @@ -31,12 +32,6 @@ from stem.descriptor.router_status_entry import ( from test.unit.descriptor import get_resource -try: - # added in python 2.7 - from collections import OrderedDict -except ImportError: - from stem.util.ordereddict import OrderedDict - BANDWIDTH_WEIGHT_ENTRIES = ( 'Wbd', 'Wbe', 'Wbg', 'Wbm', 'Wdb', @@ -869,7 +864,7 @@ DnN5aFtYKiTc19qIC7Nmo+afPdDEf0MlJvEOP5EWl3w= Parses the parameters attributes. """ - document = NetworkStatusDocumentV3.create(OrderedDict([ + document = NetworkStatusDocumentV3.create(collections.OrderedDict([ ('vote-status', 'vote'), ('recommended-client-protocols', 'HSDir=1 HSIntro=3'), ('recommended-relay-protocols', 'Cons=1 Desc=1'), @@ -1231,7 +1226,7 @@ DnN5aFtYKiTc19qIC7Nmo+afPdDEf0MlJvEOP5EWl3w= COMMITMENT_1 = '1 sha3-256 4CAEC248004A0DC6CE86EBD5F608C9B05500C70C AAAAAFd4/kAaklgYr4ijHZjXXy/B354jQfL31BFhhE46nuOHSPITyw== AAAAAFd4/kCpZeis3yJyr//rz8hXCeeAhHa4k3lAcAiMJd1vEMTPuw==' COMMITMENT_2 = '1 sha3-256 598536A9DD4E6C0F18B4AD4B88C7875A0A29BA31 AAAAAFd4/kC7S920awC5/HF5RfX4fKZtYqjm6qMh9G91AcjZm13DQQ==' - authority = DirectoryAuthority.create(OrderedDict([ + authority = DirectoryAuthority.create(collections.OrderedDict([ ('shared-rand-participate', ''), ('shared-rand-commit', '%s\nshared-rand-commit %s' % (COMMITMENT_1, COMMITMENT_2)), ('shared-rand-previous-value', '8 hAQLxyt0U3gu7QR2owixRCbIltcyPrz3B0YBfUshOkE='), diff --git a/test/unit/descriptor/reader.py b/test/unit/descriptor/reader.py index 76d1d8f3..f49183e5 100644 --- a/test/unit/descriptor/reader.py +++ b/test/unit/descriptor/reader.py @@ -19,11 +19,7 @@ import stem.util.system import test.unit.descriptor -try: - # added in python 3.3 - from unittest.mock import patch -except ImportError: - from mock import patch +from unittest.mock import patch BASIC_LISTING = """ /tmp 123 @@ -44,23 +40,13 @@ def _get_raw_tar_descriptors(): test_path = os.path.join(DESCRIPTOR_TEST_DATA, 'descriptor_archive.tar') raw_descriptors = [] - # TODO: revert to using the 'with' keyword for this when dropping python - # 2.6 support - - tar_file = None - - try: - tar_file = tarfile.open(test_path) - + with tarfile.open(test_path) as tar_file: for tar_entry in tar_file: if tar_entry.isfile(): entry = tar_file.extractfile(tar_entry) entry.readline() # strip header raw_descriptors.append(entry.read().decode('utf-8', 'replace')) entry.close() - finally: - if tar_file: - tar_file.close() TAR_DESCRIPTORS = raw_descriptors @@ -192,7 +178,6 @@ class TestDescriptorReader(unittest.TestCase): if getpass.getuser() == 'root': self.skipTest('(running as root)') - return # Skip the test on windows, since you can only set the file's # read-only flag with os.chmod(). For more information see... @@ -558,10 +543,8 @@ class TestDescriptorReader(unittest.TestCase): if getpass.getuser() == 'root': self.skipTest('(running as root)') - return elif stem.util.system.is_windows(): self.skipTest('(chmod not functional)') - return test_path = os.path.join(self.temp_directory, 'secret_file') diff --git a/test/unit/descriptor/remote.py b/test/unit/descriptor/remote.py index ab0ad3d7..d9893535 100644 --- a/test/unit/descriptor/remote.py +++ b/test/unit/descriptor/remote.py @@ -2,6 +2,7 @@ Unit tests for stem.descriptor.remote. """ +import http.client import io import socket import time @@ -13,26 +14,11 @@ import stem.descriptor.remote import stem.prereq import stem.util.str_tools +from unittest.mock import patch, Mock, MagicMock + from stem.descriptor.remote import Compression from test.unit.descriptor import read_resource -try: - from http.client import HTTPMessage # python3 -except ImportError: - from httplib import HTTPMessage # python2 - -try: - # added in python 3.3 - from unittest.mock import patch, Mock, MagicMock -except ImportError: - from mock import patch, Mock, MagicMock - -# The urlopen() method is in a different location depending on if we're using -# python 2.x or 3.x. The 2to3 converter accounts for this in imports, but not -# mock annotations. - -URL_OPEN = 'urllib.request.urlopen' if stem.prereq.is_python_3() else 'urllib2.urlopen' - TEST_RESOURCE = '/tor/server/fp/9695DFC35FFEB861329B9F1AB04C46397020CE31' # Output from requesting moria1's descriptor from itself... @@ -104,16 +90,13 @@ def _dirport_mock(data, encoding = 'identity'): dirport_mock = Mock() dirport_mock().read.return_value = data - if stem.prereq.is_python_3(): - headers = HTTPMessage() + headers = http.client.HTTPMessage() - for line in HEADER.splitlines(): - key, value = line.split(': ', 1) - headers.add_header(key, encoding if key == 'Content-Encoding' else value) + for line in HEADER.splitlines(): + key, value = line.split(': ', 1) + headers.add_header(key, encoding if key == 'Content-Encoding' else value) - dirport_mock().headers = headers - else: - dirport_mock().headers = HTTPMessage(io.BytesIO(HEADER % encoding)) + dirport_mock().headers = headers return dirport_mock @@ -165,7 +148,7 @@ class TestDescriptorDownloader(unittest.TestCase): self.assertRaisesRegexp(stem.ProtocolError, "^Response should begin with HTTP success, but was 'HTTP/1.0 500 Kaboom'", request.run) - @patch(URL_OPEN, _dirport_mock(TEST_DESCRIPTOR)) + @patch('urllib.request.urlopen', _dirport_mock(TEST_DESCRIPTOR)) def test_using_dirport(self): """ Download a descriptor through the DirPort. @@ -203,7 +186,7 @@ class TestDescriptorDownloader(unittest.TestCase): query = stem.descriptor.remote.Query(TEST_RESOURCE, compression = Compression.LZMA, start = False) self.assertEqual([stem.descriptor.Compression.PLAINTEXT], query.compression) - @patch(URL_OPEN, _dirport_mock(read_resource('compressed_identity'), encoding = 'identity')) + @patch('urllib.request.urlopen', _dirport_mock(read_resource('compressed_identity'), encoding = 'identity')) def test_compression_plaintext(self): """ Download a plaintext descriptor. @@ -218,7 +201,7 @@ class TestDescriptorDownloader(unittest.TestCase): self.assertEqual(1, len(descriptors)) self.assertEqual('moria1', descriptors[0].nickname) - @patch(URL_OPEN, _dirport_mock(read_resource('compressed_gzip'), encoding = 'gzip')) + @patch('urllib.request.urlopen', _dirport_mock(read_resource('compressed_gzip'), encoding = 'gzip')) def test_compression_gzip(self): """ Download a gip compressed descriptor. @@ -233,7 +216,7 @@ class TestDescriptorDownloader(unittest.TestCase): self.assertEqual(1, len(descriptors)) self.assertEqual('moria1', descriptors[0].nickname) - @patch(URL_OPEN, _dirport_mock(read_resource('compressed_zstd'), encoding = 'x-zstd')) + @patch('urllib.request.urlopen', _dirport_mock(read_resource('compressed_zstd'), encoding = 'x-zstd')) def test_compression_zstd(self): """ Download a zstd compressed descriptor. @@ -241,7 +224,6 @@ class TestDescriptorDownloader(unittest.TestCase): if not stem.prereq.is_zstd_available(): self.skipTest('(requires zstd module)') - return descriptors = list(stem.descriptor.remote.get_server_descriptors( '9695DFC35FFEB861329B9F1AB04C46397020CE31', @@ -252,7 +234,7 @@ class TestDescriptorDownloader(unittest.TestCase): self.assertEqual(1, len(descriptors)) self.assertEqual('moria1', descriptors[0].nickname) - @patch(URL_OPEN, _dirport_mock(read_resource('compressed_lzma'), encoding = 'x-tor-lzma')) + @patch('urllib.request.urlopen', _dirport_mock(read_resource('compressed_lzma'), encoding = 'x-tor-lzma')) def test_compression_lzma(self): """ Download a lzma compressed descriptor. @@ -260,7 +242,6 @@ class TestDescriptorDownloader(unittest.TestCase): if not stem.prereq.is_lzma_available(): self.skipTest('(requires lzma module)') - return descriptors = list(stem.descriptor.remote.get_server_descriptors( '9695DFC35FFEB861329B9F1AB04C46397020CE31', @@ -271,7 +252,7 @@ class TestDescriptorDownloader(unittest.TestCase): self.assertEqual(1, len(descriptors)) self.assertEqual('moria1', descriptors[0].nickname) - @patch(URL_OPEN) + @patch('urllib.request.urlopen') def test_each_getter(self, dirport_mock): """ Surface level exercising of each getter method for downloading descriptors. @@ -288,7 +269,7 @@ class TestDescriptorDownloader(unittest.TestCase): downloader.get_bandwidth_file() downloader.get_detached_signatures() - @patch(URL_OPEN, _dirport_mock(TEST_DESCRIPTOR)) + @patch('urllib.request.urlopen', _dirport_mock(TEST_DESCRIPTOR)) def test_reply_headers(self): query = stem.descriptor.remote.get_server_descriptors('9695DFC35FFEB861329B9F1AB04C46397020CE31', start = False) self.assertEqual(None, query.reply_headers) # initially we don't have a reply @@ -311,7 +292,7 @@ class TestDescriptorDownloader(unittest.TestCase): self.assertEqual(1, len(descriptors)) self.assertEqual('moria1', descriptors[0].nickname) - @patch(URL_OPEN, _dirport_mock(TEST_DESCRIPTOR)) + @patch('urllib.request.urlopen', _dirport_mock(TEST_DESCRIPTOR)) def test_query_download(self): """ Check Query functionality when we successfully download a descriptor. @@ -336,7 +317,7 @@ class TestDescriptorDownloader(unittest.TestCase): self.assertEqual('9695DFC35FFEB861329B9F1AB04C46397020CE31', desc.fingerprint) self.assertEqual(TEST_DESCRIPTOR, desc.get_bytes()) - @patch(URL_OPEN, _dirport_mock(b'some malformed stuff')) + @patch('urllib.request.urlopen', _dirport_mock(b'some malformed stuff')) def test_query_with_malformed_content(self): """ Query with malformed descriptor content. @@ -363,7 +344,7 @@ class TestDescriptorDownloader(unittest.TestCase): self.assertRaises(ValueError, query.run) - @patch(URL_OPEN) + @patch('urllib.request.urlopen') def test_query_with_timeout(self, dirport_mock): def urlopen_call(*args, **kwargs): time.sleep(0.06) @@ -398,7 +379,7 @@ class TestDescriptorDownloader(unittest.TestCase): expected_error = 'Endpoints must be an stem.ORPort, stem.DirPort, or two value tuple. ' + error_suffix self.assertRaisesWith(ValueError, expected_error, stem.descriptor.remote.Query, TEST_RESOURCE, 'server-descriptor 1.0', endpoints = endpoints) - @patch(URL_OPEN, _dirport_mock(TEST_DESCRIPTOR)) + @patch('urllib.request.urlopen', _dirport_mock(TEST_DESCRIPTOR)) def test_can_iterate_multiple_times(self): query = stem.descriptor.remote.Query( TEST_RESOURCE, diff --git a/test/unit/descriptor/router_status_entry.py b/test/unit/descriptor/router_status_entry.py index c428d6b2..7ee942e0 100644 --- a/test/unit/descriptor/router_status_entry.py +++ b/test/unit/descriptor/router_status_entry.py @@ -2,6 +2,7 @@ Unit tests for stem.descriptor.router_status_entry. """ +import collections import datetime import functools import unittest @@ -27,12 +28,6 @@ from stem.descriptor.router_status_entry import ( _base64_to_hex, ) -try: - # Added in 2.7 - from collections import OrderedDict -except ImportError: - from stem.util.ordereddict import OrderedDict - ENTRY_WITHOUT_ED25519 = """\ r seele AAoQ1DAR6kkoo19hBAX5K0QztNw m0ynPuwzSextzsiXYJYA0Hce+Cs 2015-08-23 00:26:35 73.15.150.172 9001 0 s Running Stable Valid @@ -258,7 +253,7 @@ class TestRouterStatusEntry(unittest.TestCase): Parse a router status entry with an IPv6 address. """ - expected_protocols = OrderedDict(( + expected_protocols = collections.OrderedDict(( ('Cons', [1]), ('Desc', [1]), ('DirCache', [1]), diff --git a/test/unit/descriptor/server_descriptor.py b/test/unit/descriptor/server_descriptor.py index d87d51e9..aeb58df1 100644 --- a/test/unit/descriptor/server_descriptor.py +++ b/test/unit/descriptor/server_descriptor.py @@ -2,6 +2,7 @@ Unit tests for stem.descriptor.server_descriptor. """ +import collections import datetime import functools import hashlib @@ -20,6 +21,8 @@ import stem.version import stem.util.str_tools import test.require +from unittest.mock import Mock, patch + from stem.client.datatype import CertType from stem.descriptor import DigestHash, DigestEncoding from stem.descriptor.certificate import ExtensionType @@ -31,18 +34,6 @@ from test.unit.descriptor import ( base_expect_invalid_attr_for_text, ) -try: - # Added in 2.7 - from collections import OrderedDict -except ImportError: - from stem.util.ordereddict import OrderedDict - -try: - # added in python 3.3 - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch - TARFILE_FINGERPRINTS = set([ 'B6D83EC2D9E18B0A7A33428F8CFA9C536769E209', 'E0BD57A11F00041A9789577C53A1B784473669E4', @@ -74,16 +65,12 @@ class TestServerDescriptor(unittest.TestCase): Fetch server descriptors via parse_file() for a tarfile object. """ - # TODO: When dropping python 2.6 support we can go back to using the 'with' - # keyword here. + with tarfile.open(get_resource('descriptor_archive.tar')) as tar_file: + descriptors = list(stem.descriptor.parse_file(tar_file)) + self.assertEqual(3, len(descriptors)) - tar_file = tarfile.open(get_resource('descriptor_archive.tar')) - descriptors = list(stem.descriptor.parse_file(tar_file)) - self.assertEqual(3, len(descriptors)) - - fingerprints = set([desc.fingerprint for desc in descriptors]) - self.assertEqual(TARFILE_FINGERPRINTS, fingerprints) - tar_file.close() + fingerprints = set([desc.fingerprint for desc in descriptors]) + self.assertEqual(TARFILE_FINGERPRINTS, fingerprints) def test_metrics_descriptor(self): """ @@ -288,7 +275,7 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4= exc_msg = 'Server descriptor lacks a fingerprint. This is an optional field, but required to make a router status entry.' self.assertRaisesWith(ValueError, exc_msg, desc_without_fingerprint.make_router_status_entry) - desc = RelayDescriptor.create(OrderedDict(( + desc = RelayDescriptor.create(collections.OrderedDict(( ('router', 'caerSidi 71.35.133.197 9001 0 0'), ('published', '2012-02-29 04:03:19'), ('fingerprint', '4F0C 867D F0EF 6816 0568 C826 838F 482C EA7C FE44'), diff --git a/test/unit/directory/authority.py b/test/unit/directory/authority.py index 1c2a86b4..1574bea3 100644 --- a/test/unit/directory/authority.py +++ b/test/unit/directory/authority.py @@ -9,13 +9,7 @@ import stem import stem.directory import stem.prereq -try: - # added in python 3.3 - from unittest.mock import patch, Mock -except ImportError: - from mock import patch, Mock - -URL_OPEN = 'urllib.request.urlopen' if stem.prereq.is_python_3() else 'urllib2.urlopen' +from unittest.mock import patch, Mock AUTHORITY_GITWEB_CONTENT = b"""\ "moria1 orport=9101 " @@ -53,7 +47,7 @@ class TestAuthority(unittest.TestCase): self.assertTrue(len(authorities) > 4) self.assertEqual('128.31.0.39', authorities['moria1'].address) - @patch(URL_OPEN, Mock(return_value = io.BytesIO(AUTHORITY_GITWEB_CONTENT))) + @patch('urllib.request.urlopen', Mock(return_value = io.BytesIO(AUTHORITY_GITWEB_CONTENT))) def test_from_remote(self): expected = { 'moria1': stem.directory.Authority( @@ -77,6 +71,6 @@ class TestAuthority(unittest.TestCase): self.assertEqual(expected, stem.directory.Authority.from_remote()) - @patch(URL_OPEN, Mock(return_value = io.BytesIO(b''))) + @patch('urllib.request.urlopen', Mock(return_value = io.BytesIO(b''))) def test_from_remote_empty(self): self.assertRaisesRegexp(stem.DownloadFailed, 'no content', stem.directory.Authority.from_remote) diff --git a/test/unit/directory/fallback.py b/test/unit/directory/fallback.py index 06b4510d..a7c54efb 100644 --- a/test/unit/directory/fallback.py +++ b/test/unit/directory/fallback.py @@ -2,6 +2,7 @@ Unit tests for stem.directory.Fallback. """ +import collections import io import re import tempfile @@ -11,19 +12,7 @@ import stem import stem.directory import stem.util.conf -try: - # added in python 2.7 - from collections import OrderedDict -except ImportError: - from stem.util.ordereddict import OrderedDict - -try: - # added in python 3.3 - from unittest.mock import patch, Mock -except ImportError: - from mock import patch, Mock - -URL_OPEN = 'urllib.request.urlopen' if stem.prereq.is_python_3() else 'urllib2.urlopen' +from unittest.mock import patch, Mock FALLBACK_GITWEB_CONTENT = b"""\ /* type=fallback */ @@ -58,7 +47,7 @@ URL: https:onionoo.torproject.orguptime?first_seen_days=30-&flag=V2Dir&type=rela /* ===== */ """ -HEADER = OrderedDict(( +HEADER = collections.OrderedDict(( ('type', 'fallback'), ('version', '2.0.0'), ('timestamp', '20170526090242'), @@ -75,7 +64,7 @@ class TestFallback(unittest.TestCase): 'nickname': 'rueckgrat', 'has_extrainfo': True, 'orport_v6': ('2a01:4f8:162:51e2::2', 9001), - 'header': OrderedDict(( + 'header': collections.OrderedDict(( ('type', 'fallback'), ('version', '2.0.0'), ('timestamp', '20170526090242'), @@ -95,7 +84,7 @@ class TestFallback(unittest.TestCase): self.assertTrue(len(fallbacks) > 10) self.assertEqual('185.13.39.197', fallbacks['001524DD403D729F08F7E5D77813EF12756CFA8D'].address) - @patch(URL_OPEN, Mock(return_value = io.BytesIO(FALLBACK_GITWEB_CONTENT))) + @patch('urllib.request.urlopen', Mock(return_value = io.BytesIO(FALLBACK_GITWEB_CONTENT))) def test_from_remote(self): expected = { '0756B7CD4DFC8182BE23143FAC0642F515182CEB': stem.directory.Fallback( @@ -122,15 +111,15 @@ class TestFallback(unittest.TestCase): self.assertEqual(expected, stem.directory.Fallback.from_remote()) - @patch(URL_OPEN, Mock(return_value = io.BytesIO(b''))) + @patch('urllib.request.urlopen', Mock(return_value = io.BytesIO(b''))) def test_from_remote_empty(self): self.assertRaisesRegexp(stem.DownloadFailed, 'no content', stem.directory.Fallback.from_remote) - @patch(URL_OPEN, Mock(return_value = io.BytesIO(b'\n'.join(FALLBACK_GITWEB_CONTENT.splitlines()[1:])))) + @patch('urllib.request.urlopen', Mock(return_value = io.BytesIO(b'\n'.join(FALLBACK_GITWEB_CONTENT.splitlines()[1:])))) def test_from_remote_no_header(self): self.assertRaisesRegexp(IOError, 'does not have a type field indicating it is fallback directory metadata', stem.directory.Fallback.from_remote) - @patch(URL_OPEN, Mock(return_value = io.BytesIO(FALLBACK_GITWEB_CONTENT.replace(b'version=2.0.0', b'version')))) + @patch('urllib.request.urlopen', Mock(return_value = io.BytesIO(FALLBACK_GITWEB_CONTENT.replace(b'version=2.0.0', b'version')))) def test_from_remote_malformed_header(self): self.assertRaisesRegexp(IOError, 'Malformed fallback directory header line: /\\* version \\*/', stem.directory.Fallback.from_remote) @@ -145,7 +134,7 @@ class TestFallback(unittest.TestCase): } for entry, expected in test_values.items(): - with patch(URL_OPEN, Mock(return_value = io.BytesIO(entry))): + with patch('urllib.request.urlopen', Mock(return_value = io.BytesIO(entry))): self.assertRaisesRegexp(IOError, re.escape(expected), stem.directory.Fallback.from_remote) def test_persistence(self): diff --git a/test/unit/doctest.py b/test/unit/doctest.py index 28eef0cf..84712dc2 100644 --- a/test/unit/doctest.py +++ b/test/unit/doctest.py @@ -15,13 +15,9 @@ import stem.util.system import stem.version import test -from stem.response import ControlMessage +from unittest.mock import Mock, patch -try: - # added in python 3.3 - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch +from stem.response import ControlMessage EXPECTED_CIRCUIT_STATUS = """\ 20 EXTENDED $718BCEA286B531757ACAFF93AE04910EA73DE617=KsmoinOK,$649F2D0ACF418F7CFC6539AB2257EB2D5297BAFA=Eskimo BUILD_FLAGS=NEED_CAPACITY PURPOSE=GENERAL TIME_CREATED=2012-12-06T13:51:11.433755 diff --git a/test/unit/exit_policy/policy.py b/test/unit/exit_policy/policy.py index f6cf9957..0cb755ae 100644 --- a/test/unit/exit_policy/policy.py +++ b/test/unit/exit_policy/policy.py @@ -5,11 +5,7 @@ Unit tests for the stem.exit_policy.ExitPolicy class. import pickle import unittest -try: - # added in python 3.3 - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch +from unittest.mock import Mock, patch from stem.exit_policy import ( DEFAULT_POLICY_RULES, diff --git a/test/unit/installation.py b/test/unit/installation.py index fd8709ba..4b2a795c 100644 --- a/test/unit/installation.py +++ b/test/unit/installation.py @@ -7,9 +7,6 @@ import test class TestInstallation(unittest.TestCase): - # TODO: remove when dropping support for python 2.6 - skip_reason = 'setUpClass() unsupported in python 2.6' - @classmethod def setUpClass(self): setup_path = os.path.join(test.STEM_BASE, 'setup.py') @@ -25,7 +22,6 @@ class TestInstallation(unittest.TestCase): def test_installs_all_modules(self): if self.skip_reason: self.skipTest(self.skip_reason) - return True # Modules cited my our setup.py looks like... # @@ -49,7 +45,6 @@ class TestInstallation(unittest.TestCase): def test_installs_all_data_files(self): if self.skip_reason: self.skipTest(self.skip_reason) - return True # Checking that we have all non-source files. Data looks like... # diff --git a/test/unit/interpreter/__init__.py b/test/unit/interpreter/__init__.py index 7c107687..a48a19bc 100644 --- a/test/unit/interpreter/__init__.py +++ b/test/unit/interpreter/__init__.py @@ -9,11 +9,7 @@ __all__ = [ 'help', ] -try: - # added in python 3.3 - from unittest.mock import Mock -except ImportError: - from mock import Mock +from unittest.mock import Mock GETINFO_NAMES = """ info/names -- List of GETINFO options, types, and documentation. diff --git a/test/unit/interpreter/autocomplete.py b/test/unit/interpreter/autocomplete.py index 40bcab48..8971ddc7 100644 --- a/test/unit/interpreter/autocomplete.py +++ b/test/unit/interpreter/autocomplete.py @@ -1,15 +1,10 @@ import unittest -from stem.interpreter.autocomplete import _get_commands, Autocompleter +from unittest.mock import Mock +from stem.interpreter.autocomplete import _get_commands, Autocompleter from test.unit.interpreter import CONTROLLER -try: - # added in python 3.3 - from unittest.mock import Mock -except ImportError: - from mock import Mock - class TestAutocompletion(unittest.TestCase): def test_autocomplete_results_from_config(self): diff --git a/test/unit/interpreter/commands.py b/test/unit/interpreter/commands.py index 59ceadfe..412178e6 100644 --- a/test/unit/interpreter/commands.py +++ b/test/unit/interpreter/commands.py @@ -5,16 +5,12 @@ import stem import stem.response import stem.version +from unittest.mock import Mock, patch + from stem.interpreter.commands import ControlInterpreter, _get_fingerprint from stem.response import ControlMessage from test.unit.interpreter import CONTROLLER -try: - # added in python 3.3 - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch - EXPECTED_EVENTS_RESPONSE = """\ \x1b[34mBW 15 25\x1b[0m \x1b[34mBW 758 570\x1b[0m diff --git a/test/unit/manual.py b/test/unit/manual.py index 72f847e5..e1891ca4 100644 --- a/test/unit/manual.py +++ b/test/unit/manual.py @@ -2,36 +2,21 @@ Unit testing for the stem.manual module. """ +import collections import io import os import sqlite3 import tempfile import unittest +import urllib.request import stem.prereq import stem.manual import stem.util.system import test.require -try: - # account for urllib's change between python 2.x and 3.x - import urllib.request as urllib -except ImportError: - import urllib2 as urllib - -try: - # added in python 3.3 - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch - -try: - # added in python 2.7 - from collections import OrderedDict -except ImportError: - from stem.util.ordereddict import OrderedDict - -URL_OPEN = 'urllib.request.urlopen' if stem.prereq.is_python_3() else 'urllib2.urlopen' +from unittest.mock import Mock, patch + EXAMPLE_MAN_PATH = os.path.join(os.path.dirname(__file__), 'tor_man_example') UNKNOWN_OPTIONS_MAN_PATH = os.path.join(os.path.dirname(__file__), 'tor_man_with_unknown') @@ -59,7 +44,7 @@ EXPECTED_FILES = { '$HOME/.torrc': 'Fallback location for torrc, if @CONFDIR@/torrc is not found.', } -EXPECTED_CONFIG_OPTIONS = OrderedDict() +EXPECTED_CONFIG_OPTIONS = collections.OrderedDict() EXPECTED_CONFIG_OPTIONS['BandwidthRate'] = stem.manual.ConfigOption( name = 'BandwidthRate', @@ -91,6 +76,10 @@ EXPECTED_CONFIG_OPTIONS['Bridge'] = stem.manual.ConfigOption( CACHED_MANUAL = None +TEMP_DIR_MOCK = Mock() +TEMP_DIR_MOCK.__enter__ = Mock(return_value = '/no/such/path') +TEMP_DIR_MOCK.__exit__ = Mock(return_value = False) + def _cached_manual(): global CACHED_MANUAL @@ -154,7 +143,6 @@ class TestManual(unittest.TestCase): if not stem.manual.HAS_ENCODING_ARG: self.skipTest('(man lacks --encoding arg on OSX, BSD, and Slackware #18660)') - return manual = stem.manual.Manual.from_man(EXAMPLE_MAN_PATH) @@ -175,7 +163,6 @@ class TestManual(unittest.TestCase): if not stem.manual.HAS_ENCODING_ARG: self.skipTest('(man lacks --encoding arg on OSX and BSD and Slackware, #18660)') - return manual = stem.manual.Manual.from_man(UNKNOWN_OPTIONS_MAN_PATH) @@ -203,7 +190,6 @@ class TestManual(unittest.TestCase): if not stem.manual.HAS_ENCODING_ARG: self.skipTest('(man lacks --encoding arg on OSX, BSD and Slackware, #18660)') - return manual = stem.manual.Manual.from_man(EXAMPLE_MAN_PATH) @@ -220,7 +206,6 @@ class TestManual(unittest.TestCase): if not stem.manual.HAS_ENCODING_ARG: self.skipTest('(man lacks --encoding arg on OSX, BSD, and Slackware #18660)') - return manual = stem.manual.Manual.from_man(EXAMPLE_MAN_PATH) @@ -249,40 +234,36 @@ class TestManual(unittest.TestCase): exc_msg = 'We require a2x from asciidoc to provide a man page' self.assertRaisesWith(IOError, exc_msg, stem.manual.download_man_page, '/tmp/no_such_file') - @patch('tempfile.mkdtemp', Mock(return_value = '/no/such/path')) - @patch('shutil.rmtree', Mock()) + @patch('tempfile.TemporaryDirectory', Mock(return_value = TEMP_DIR_MOCK)) @patch('stem.manual.open', Mock(side_effect = IOError('unable to write to file')), create = True) @patch('stem.util.system.is_available', Mock(return_value = True)) def test_download_man_page_when_unable_to_write(self): exc_msg = "Unable to download tor's manual from https://gitweb.torproject.org/tor.git/plain/doc/tor.1.txt to /no/such/path/tor.1.txt: unable to write to file" self.assertRaisesWith(IOError, exc_msg, stem.manual.download_man_page, '/tmp/no_such_file') - @patch('tempfile.mkdtemp', Mock(return_value = '/no/such/path')) - @patch('shutil.rmtree', Mock()) + @patch('tempfile.TemporaryDirectory', Mock(return_value = TEMP_DIR_MOCK)) @patch('stem.manual.open', Mock(return_value = io.BytesIO()), create = True) @patch('stem.util.system.is_available', Mock(return_value = True)) - @patch(URL_OPEN, Mock(side_effect = urllib.URLError('<urlopen error [Errno -2] Name or service not known>'))) + @patch('urllib.request.urlopen', Mock(side_effect = urllib.request.URLError('<urlopen error [Errno -2] Name or service not known>'))) def test_download_man_page_when_download_fails(self): exc_msg = "Unable to download tor's manual from https://www.atagar.com/foo/bar to /no/such/path/tor.1.txt: <urlopen error <urlopen error [Errno -2] Name or service not known>>" self.assertRaisesWith(IOError, exc_msg, stem.manual.download_man_page, '/tmp/no_such_file', url = 'https://www.atagar.com/foo/bar') - @patch('tempfile.mkdtemp', Mock(return_value = '/no/such/path')) - @patch('shutil.rmtree', Mock()) + @patch('tempfile.TemporaryDirectory', Mock(return_value = TEMP_DIR_MOCK)) @patch('stem.manual.open', Mock(return_value = io.BytesIO()), create = True) @patch('stem.util.system.call', Mock(side_effect = stem.util.system.CallError('call failed', 'a2x -f manpage /no/such/path/tor.1.txt', 1, None, None, 'call failed'))) @patch('stem.util.system.is_available', Mock(return_value = True)) - @patch(URL_OPEN, Mock(return_value = io.BytesIO(b'test content'))) + @patch('urllib.request.urlopen', Mock(return_value = io.BytesIO(b'test content'))) def test_download_man_page_when_a2x_fails(self): exc_msg = "Unable to run 'a2x -f manpage /no/such/path/tor.1.txt': call failed" self.assertRaisesWith(IOError, exc_msg, stem.manual.download_man_page, '/tmp/no_such_file', url = 'https://www.atagar.com/foo/bar') - @patch('tempfile.mkdtemp', Mock(return_value = '/no/such/path')) - @patch('shutil.rmtree', Mock()) + @patch('tempfile.TemporaryDirectory', Mock(return_value = TEMP_DIR_MOCK)) @patch('stem.manual.open', create = True) @patch('stem.util.system.call') @patch('stem.util.system.is_available', Mock(return_value = True)) @patch('os.path.exists', Mock(return_value = True)) - @patch(URL_OPEN, Mock(return_value = io.BytesIO(b'test content'))) + @patch('urllib.request.urlopen', Mock(return_value = io.BytesIO(b'test content'))) def test_download_man_page_when_successful(self, call_mock, open_mock): open_mock.side_effect = lambda path, *args: { '/no/such/path/tor.1.txt': io.BytesIO(), @@ -315,4 +296,4 @@ class TestManual(unittest.TestCase): self.assertEqual({}, manual.commandline_options) self.assertEqual({}, manual.signals) self.assertEqual({}, manual.files) - self.assertEqual(OrderedDict(), manual.config_options) + self.assertEqual(collections.OrderedDict(), manual.config_options) diff --git a/test/unit/response/events.py b/test/unit/response/events.py index 82506e6d..27862054 100644 --- a/test/unit/response/events.py +++ b/test/unit/response/events.py @@ -10,16 +10,12 @@ import stem.response import stem.response.events import stem.util.log +from unittest.mock import Mock + from stem import * # enums and exceptions from stem.response import ControlMessage from stem.descriptor.router_status_entry import RouterStatusEntryV3 -try: - # added in python 3.3 - from unittest.mock import Mock -except ImportError: - from mock import Mock - # ADDRMAP event ADDRMAP = '650 ADDRMAP www.atagar.com 75.119.206.243 "2012-11-19 00:50:13" \ diff --git a/test/unit/response/protocolinfo.py b/test/unit/response/protocolinfo.py index ab6dd0eb..dd8d2160 100644 --- a/test/unit/response/protocolinfo.py +++ b/test/unit/response/protocolinfo.py @@ -11,15 +11,11 @@ import stem.util.proc import stem.util.system import stem.version +from unittest.mock import Mock, patch + from stem.response import ControlMessage from stem.response.protocolinfo import AuthMethod -try: - # added in python 3.3 - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch - NO_AUTH = """250-PROTOCOLINFO 1 250-AUTH METHODS=NULL 250-VERSION Tor="0.2.1.30" diff --git a/test/unit/tutorial.py b/test/unit/tutorial.py index e866f3ca..91a70a32 100644 --- a/test/unit/tutorial.py +++ b/test/unit/tutorial.py @@ -7,6 +7,8 @@ import unittest import stem.descriptor.remote +from unittest.mock import Mock, patch + from stem.control import Controller from stem.descriptor.router_status_entry import RouterStatusEntryV2, RouterStatusEntryV3 from stem.descriptor.networkstatus import NetworkStatusDocumentV3 @@ -14,17 +16,6 @@ from stem.descriptor.server_descriptor import RelayDescriptor from stem.exit_policy import ExitPolicy from test.unit import exec_documentation_example -try: - from StringIO import StringIO -except ImportError: - from io import StringIO - -try: - # added in python 3.3 - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch - OVER_THE_RIVER_OUTPUT = """\ * Connecting to tor @@ -47,7 +38,7 @@ class TestTutorial(unittest.TestCase): stem.descriptor.remote.SINGLETON_DOWNLOADER = None - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) @patch('stem.control.Controller.from_port', spec = Controller) def test_the_little_relay_that_could(self, from_port_mock, stdout_mock): controller = from_port_mock().__enter__() @@ -59,7 +50,7 @@ class TestTutorial(unittest.TestCase): exec_documentation_example('hello_world.py') self.assertEqual('My Tor relay has read 33406 bytes and written 29649.\n', stdout_mock.getvalue()) - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) @patch('shutil.rmtree') @patch('stem.control.Controller.from_port', spec = Controller) def test_over_the_river(self, from_port_mock, rmtree_mock, stdout_mock): @@ -125,7 +116,7 @@ class TestTutorial(unittest.TestCase): self.assertEqual(OVER_THE_RIVER_OUTPUT, stdout_mock.getvalue()) - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) @patch('stem.descriptor.remote.DescriptorDownloader') def test_mirror_mirror_on_the_wall_1(self, downloader_mock, stdout_mock): downloader_mock().get_consensus.return_value = [RouterStatusEntryV2.create({ @@ -135,7 +126,7 @@ class TestTutorial(unittest.TestCase): exec_documentation_example('current_descriptors.py') self.assertEqual('found relay caerSidi (A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB)\n', stdout_mock.getvalue()) - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) @patch('stem.control.Controller.from_port', spec = Controller) def test_mirror_mirror_on_the_wall_2(self, from_port_mock, stdout_mock): controller = from_port_mock().__enter__() @@ -146,7 +137,7 @@ class TestTutorial(unittest.TestCase): exec_documentation_example('descriptor_from_tor_control_socket.py') self.assertEqual('found relay caerSidi (A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB)\n', stdout_mock.getvalue()) - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) @patch('%s.open' % __name__, create = True) def test_mirror_mirror_on_the_wall_3(self, open_mock, stdout_mock): def tutorial_example(): @@ -164,7 +155,7 @@ class TestTutorial(unittest.TestCase): tutorial_example() self.assertEqual('found relay caerSidi (A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB)\n', stdout_mock.getvalue()) - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) @patch('stem.descriptor.collector.get_server_descriptors') def test_mirror_mirror_on_the_wall_4(self, get_desc_mock, stdout_mock): get_desc_mock.return_value = iter([RelayDescriptor.create({ @@ -175,7 +166,7 @@ class TestTutorial(unittest.TestCase): exec_documentation_example('collector_reading.py') self.assertEqual('1 relays published an exiting policy today...\n\n caerSidi (2C3C46625698B6D67DF32BC1918AD3EE1F9906B1)\n', stdout_mock.getvalue()) - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) @patch('stem.descriptor.remote.DescriptorDownloader') @patch('stem.prereq.is_crypto_available', Mock(return_value = False)) def test_mirror_mirror_on_the_wall_5(self, downloader_mock, stdout_mock): diff --git a/test/unit/tutorial_examples.py b/test/unit/tutorial_examples.py index c4b26ad0..d96ff71b 100644 --- a/test/unit/tutorial_examples.py +++ b/test/unit/tutorial_examples.py @@ -2,19 +2,17 @@ Tests for the examples given in stem's tutorial. """ +import io import itertools import os import unittest -try: - from StringIO import StringIO -except ImportError: - from io import StringIO - import stem.response import stem.descriptor.remote import stem.prereq +from unittest.mock import Mock, patch + from stem.control import Controller from stem.descriptor.networkstatus import NetworkStatusDocumentV3 from stem.descriptor.router_status_entry import RouterStatusEntryV3 @@ -24,12 +22,6 @@ from stem.response import ControlMessage from test.unit import exec_documentation_example -try: - # added in python 3.3 - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch - OPEN_FUNCTION = open # make a reference so mocking open() won't mess with us CIRC_CONTENT = '650 CIRC %d %s \ @@ -129,13 +121,7 @@ def _get_router_status(address = None, port = None, nickname = None, fingerprint class TestTutorialExamples(unittest.TestCase): - def assert_equal_unordered(self, expected, actual): - if stem.prereq.is_python_3(): - self.assertCountEqual(expected.splitlines(), actual.splitlines()) - else: - self.assertItemsEqual(expected.splitlines(), actual.splitlines()) - - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) @patch('stem.control.Controller.from_port', spec = Controller) def test_list_circuits(self, from_port_mock, stdout_mock): path_1 = ('B1FA7D51B8B6F0CB585D944F450E7C06EDE7E44C', 'ByTORAndTheSnowDog') @@ -164,9 +150,9 @@ class TestTutorialExamples(unittest.TestCase): }[fingerprint] exec_documentation_example('list_circuits.py') - self.assert_equal_unordered(LIST_CIRCUITS_OUTPUT, stdout_mock.getvalue()) + self.assertCountEqual(LIST_CIRCUITS_OUTPUT.splitlines(), stdout_mock.getvalue().splitlines()) - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) @patch('stem.control.Controller.from_port', spec = Controller) def test_exit_used(self, from_port_mock, stdout_mock): def tutorial_example(mock_event): @@ -215,9 +201,9 @@ class TestTutorialExamples(unittest.TestCase): controller.get_info.return_value = 'unknown' tutorial_example(event) - self.assert_equal_unordered(EXIT_USED_OUTPUT, stdout_mock.getvalue()) + self.assertCountEqual(EXIT_USED_OUTPUT.splitlines(), stdout_mock.getvalue().splitlines()) - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) @patch('stem.descriptor.remote.DescriptorDownloader') def test_outdated_relays(self, downloader_mock, stdout_mock): downloader_mock().get_server_descriptors.return_value = [ @@ -229,19 +215,12 @@ class TestTutorialExamples(unittest.TestCase): exec_documentation_example('outdated_relays.py') - self.assert_equal_unordered(OUTDATED_RELAYS_OUTPUT, stdout_mock.getvalue()) + self.assertCountEqual(OUTDATED_RELAYS_OUTPUT.splitlines(), stdout_mock.getvalue().splitlines()) - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) @patch('stem.descriptor.remote.Query') @patch('stem.directory.Authority.from_cache') def test_compare_flags(self, authorities_mock, query_mock, stdout_mock): - if stem.prereq._is_python_26(): - # example imports OrderedDict from collections which doesn't work under - # python 2.6 - - self.skipTest("(example doesn't support python 2.6)") - return - authorities_mock().items.return_value = [('moria1', DIRECTORY_AUTHORITIES['moria1']), ('maatuska', DIRECTORY_AUTHORITIES['maatuska'])] fingerprint = [ @@ -277,9 +256,9 @@ class TestTutorialExamples(unittest.TestCase): exec_documentation_example('compare_flags.py') - self.assert_equal_unordered(COMPARE_FLAGS_OUTPUT, stdout_mock.getvalue()) + self.assertCountEqual(COMPARE_FLAGS_OUTPUT.splitlines(), stdout_mock.getvalue().splitlines()) - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) @patch('stem.directory.Authority.from_cache') @patch('stem.descriptor.remote.DescriptorDownloader.query') def test_votes_by_bandwidth_authorities(self, query_mock, authorities_mock, stdout_mock): @@ -310,9 +289,9 @@ class TestTutorialExamples(unittest.TestCase): query_mock.side_effect = [query1, query2, query3] exec_documentation_example('votes_by_bandwidth_authorities.py') - self.assert_equal_unordered(VOTES_BY_BANDWIDTH_AUTHORITIES_OUTPUT, stdout_mock.getvalue()) + self.assertCountEqual(VOTES_BY_BANDWIDTH_AUTHORITIES_OUTPUT.splitlines(), stdout_mock.getvalue().splitlines()) - @patch('sys.stdout', new_callable = StringIO) + @patch('sys.stdout', new_callable = io.StringIO) @patch('stem.descriptor.parse_file') @patch('stem.descriptor.remote.Query') def test_persisting_a_consensus(self, query_mock, parse_file_mock, stdout_mock): diff --git a/test/unit/util/connection.py b/test/unit/util/connection.py index 8721ff6d..d848c40b 100644 --- a/test/unit/util/connection.py +++ b/test/unit/util/connection.py @@ -5,25 +5,15 @@ Unit tests for the stem.util.connection functions. import io import platform import unittest +import urllib.request import stem import stem.util.connection -from stem.util.connection import Resolver, Connection - -try: - # account for urllib's change between python 2.x and 3.x - import urllib.request as urllib -except ImportError: - import urllib2 as urllib +from unittest.mock import Mock, patch -try: - # added in python 3.3 - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch +from stem.util.connection import Resolver, Connection -URL_OPEN = 'urllib.request.urlopen' if stem.prereq.is_python_3() else 'urllib2.urlopen' URL = 'https://example.unit.test.url' NETSTAT_OUTPUT = """\ @@ -177,16 +167,16 @@ _tor tor 15843 20* internet stream tcp 0x0 192.168.1.100:36174 --> class TestConnection(unittest.TestCase): - @patch(URL_OPEN) + @patch('urllib.request.urlopen') def test_download(self, urlopen_mock): urlopen_mock.return_value = io.BytesIO(b'hello') self.assertEqual(b'hello', stem.util.connection.download(URL)) urlopen_mock.assert_called_with(URL, timeout = None) - @patch(URL_OPEN) + @patch('urllib.request.urlopen') def test_download_failure(self, urlopen_mock): - urlopen_mock.side_effect = urllib.URLError('boom') + urlopen_mock.side_effect = urllib.request.URLError('boom') try: stem.util.connection.download(URL) @@ -195,12 +185,12 @@ class TestConnection(unittest.TestCase): self.assertEqual('Failed to download from https://example.unit.test.url (URLError): boom', str(exc)) self.assertEqual(URL, exc.url) self.assertEqual('boom', exc.error.reason) - self.assertEqual(urllib.URLError, type(exc.error)) - self.assertTrue('return urllib.urlopen(url, timeout = timeout).read()' in exc.stacktrace_str) + self.assertEqual(urllib.request.URLError, type(exc.error)) + self.assertTrue('return urllib.request.urlopen(url, timeout = timeout).read()' in exc.stacktrace_str) - @patch(URL_OPEN) + @patch('urllib.request.urlopen') def test_download_retries(self, urlopen_mock): - urlopen_mock.side_effect = urllib.URLError('boom') + urlopen_mock.side_effect = urllib.request.URLError('boom') self.assertRaisesRegexp(IOError, 'boom', stem.util.connection.download, URL) self.assertEqual(1, urlopen_mock.call_count) diff --git a/test/unit/util/proc.py b/test/unit/util/proc.py index 6ee29136..2316f669 100644 --- a/test/unit/util/proc.py +++ b/test/unit/util/proc.py @@ -7,14 +7,11 @@ import unittest import test +from unittest.mock import Mock, patch + from stem.util import proc from stem.util.connection import Connection -try: - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch - TITLE_LINE = b'sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout' TCP6_CONTENT = b"""\ diff --git a/test/unit/util/system.py b/test/unit/util/system.py index b4fb81ea..32be337c 100644 --- a/test/unit/util/system.py +++ b/test/unit/util/system.py @@ -14,13 +14,9 @@ import unittest import stem.prereq -from stem.util import system +from unittest.mock import Mock, patch -try: - # added in python 3.3 - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch +from stem.util import system # Base responses for the pid_by_name tests. The 'success' and # 'multiple_results' entries are filled in by tests. diff --git a/test/unit/version.py b/test/unit/version.py index 3c21855c..abdb65c0 100644 --- a/test/unit/version.py +++ b/test/unit/version.py @@ -7,13 +7,9 @@ import unittest import stem.util.system import stem.version -from stem.version import Version +from unittest.mock import Mock, patch -try: - # added in python 3.3 - from unittest.mock import Mock, patch -except ImportError: - from mock import Mock, patch +from stem.version import Version VERSION_CMD_OUTPUT = """Mar 22 23:09:37.088 [notice] Tor v0.2.2.35 \ (git-73ff13ab3cc9570d). This is experimental software. Do not rely on it for \ |
