Repository: enthought/encore (language: python)

Issue:
use unittest.mock instead of mock
Could you please consider to utilize unittest.mock, as a fallback at least?


Discussion / hints:
We no longer support Python 2, so it would definitely make sense to replace all uses of `mock` with `unittest.mock`; we've already done that in most other ETS packages.

OTOH, resources are limited, so you may need to wait a while. If you want to speed things up, would you be interested in contributing a PR?

> If you want to speed things up, [...]

Scratch that. It's a one-line change. PR shortly.


Thank you!
Unfortunately, while this particular fix _is_ a one-line change, the rest of the codebase still hasn't really made it to Python 3. I've tried to make some progress on that in #114.

Fix patch:
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 9166306..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-language: python
-python:
-  - 2.7
-  - 3.5
-install:
-  - pip install -U nose
-  - pip install coverage coveralls requests futures sphinx six
-  - python setup.py develop
-before_script:
-  - mkdir testrunner
-  - cp .coveragerc testrunner/
-  - cd testrunner
-script:
-  - coverage run -m nose.core encore --exe
-notifications:
-  email:
-    - travis-ci@enthought.com
-after_success:
-  coveralls
diff --git a/docs/source/concurrent/index.rst b/docs/source/concurrent/index.rst
index 7b00f8c..6749a70 100644
--- a/docs/source/concurrent/index.rst
+++ b/docs/source/concurrent/index.rst
@@ -6,8 +6,8 @@ The :py:mod:`encore.concurrent` module provides utilities and libraries to
 assist with threaded and other parallel code.
 
 The :py:mod:`encore.concurrent.futures` subpackage provides an enhanced
-version of the ``concurrent.futures`` package for Python 2.7 with some
-useful experimental additions.
+version of the ``concurrent.futures`` package from the standard library
+with some useful experimental additions.
 
 The :py:mod:`encore.concurrent.threadtools` module provides some utilities
 that encapsulate useful patterns in threaded code.
diff --git a/docs/source/storage/usage.rst b/docs/source/storage/usage.rst
index 41673cf..6597b3d 100644
--- a/docs/source/storage/usage.rst
+++ b/docs/source/storage/usage.rst
@@ -41,7 +41,7 @@ methods::
 In this case datastream is a file-like object that streams bytes::
 
     data = datastream.read()
-    print data
+    print(data)
 
 More likely you will have used some sort of serialization format like XML, JSON
 or YAML to store your data in the document, so instead you can do::
@@ -70,9 +70,9 @@ The metadata stores auxilliary information about the data that is stored in the
 key.  It is a dictionary of reasonably serializable values (frequently it will
 serialize to JSON or similar format)::
 
-    print 'Document title:', metadata['title']
-    print 'Document author:', metadata['author']
-    print 'Document encoding:', metadata['encoding']
+    print('Document title:', metadata['title'])
+    print('Document author:', metadata['author'])
+    print('Document encoding:', metadata['encoding'])
 
     # checksum
     import hashlib
@@ -110,7 +110,7 @@ key-value store API gives a simple query mechanism that permits this sort of
 matching::
 
     for key, metadata in store.query(author='alibaba', organization='40 Thieves'):
-        print key, ':', metadata['title']
+        print(key, ':', metadata['title'])
 
 This will print the key and title of all documents which have an ``author`` key
 with value ``'alibaba'`` and an ``organization`` key with value ``'40 Thieves'``.
@@ -122,19 +122,19 @@ If all the user is concerned with is which keys match, there is an alternative
 method :py:meth:`~.AbstractReadOnlyStore.query_keys`::
 
     for key in store.query_keys(author='alibaba', organization='40 Thieves'):
-        print key
+        print(key)
 
 To iterate over all the keys in a store, you can simply call :py:meth:`~.AbstractReadOnlyStore.query_keys`
 with no arguments::
 
     for key in store.query_keys():
-        print key
+        print(key)
 
 Finally, as a useful utility, you can use glob-style matching on the keys using
 the :py:meth:`~.AbstractReadOnlyStore.glob` method::
 
     for key in store.glob('*.jpg'):
-        print key
+        print(key)
 
 Writing
 -------
diff --git a/encore/concurrent/futures/enhanced_thread_pool_executor.py b/encore/concurrent/futures/enhanced_thread_pool_executor.py
index 86ffb32..8234f42 100644
--- a/encore/concurrent/futures/enhanced_thread_pool_executor.py
+++ b/encore/concurrent/futures/enhanced_thread_pool_executor.py
@@ -32,13 +32,12 @@
 
 """
 
-from __future__ import with_statement
 import atexit
 import itertools
+import queue
 import threading
 import weakref
 import time
-from six.moves import queue
 
 from concurrent.futures import _base
 
@@ -238,9 +237,9 @@ def map(self, fn, *iterables, **kwargs):
         Raises:
             TimeoutError: If the entire result iterator could not be generated
                 before the given timeout.
-                
+
             Exception: If fn(\*args) raises for any values.
-            
+
         """
         timeout = kwargs.get('timeout')
         if timeout is not None:
diff --git a/encore/events/event_manager.py b/encore/events/event_manager.py
index 6078b1d..710c382 100644
--- a/encore/events/event_manager.py
+++ b/encore/events/event_manager.py
@@ -20,8 +20,6 @@
 from types import MethodType
 import traceback
 
-import six
-
 # Logging.
 logger = logging.getLogger(__name__)
 
@@ -34,7 +32,7 @@
 ###############################################################################
 class LoggingTracer(object):
     """ A tracer object for event manager to log events.
-    
+
     Usage
     -----
     event_manager.set_trace(LoggingTracer())
@@ -71,18 +69,8 @@ class MethodNotifier(object):
     def __init__(self, meth, notify=None, args=()):
         self.func = meth.__func__
 
-        if six.PY2:
-            self.cls = meth.im_class
-        else:
-            self.cls = meth.__self__.__class__
-
-        obj = meth.__self__
-        if obj is None:
-            # Unbound Method.
-            self.obj = None
-        else:
-            # Bound method.
-            self.obj = weakref.ref(meth.__self__, self.notify)
+        self.cls = meth.__self__.__class__
+        self.obj = weakref.ref(meth.__self__, self.notify)
         if notify:
             self._notify = notify
             self._args = args
@@ -96,16 +84,12 @@ def __call__(self):
         """ Return the original listener method, or None if it no longer exists.
         """
         obj = self.obj
-        if obj is None:
-            # Unbound method.
-            return six.create_unbound_method(self.func, self.cls)
-        else:
-            objc = obj()
-            if objc is None:
-                # Bound method whose object has been garbage collected.
-                return
+        objc = obj()
+        if objc is None:
+            # Bound method whose object has been garbage collected.
+            return
 
-            return six.create_bound_method(self.func, objc)
+        return MethodType(self.func, objc)
 
 ###############################################################################
 # `EventInfo` Private Class.
@@ -148,7 +132,7 @@ def connect(self, func, filter=None, priority=0, count=0):
         filter : dict
             Filters to match for before calling the listener. The listener is
             called only when the event matches all of the filter .
-            
+
             Filter specification:
                 - key: string which is extended name of an attribute of the
                     event instance. For example string 'source.name' will be
@@ -166,7 +150,7 @@ def connect(self, func, filter=None, priority=0, count=0):
 
         Note
         ----
-       
+
         Reconnecting an already connected listener will disconnect the
         old listener. This may have rammifications in changing the filters
         and the priority.
@@ -342,7 +326,7 @@ def connect(self, cls, func, filter=None, priority=0):
         filter : dict
             Filters to match for before calling the listener. The listener is
             called only when the event matches all of the filter .
-            
+
             Filter specification:
                 - key: string which is name of an attribute of the event instance.
                 - value: the value of the specified attribute.
@@ -353,7 +337,7 @@ def connect(self, cls, func, filter=None, priority=0):
 
         Note
         ----
-       
+
         Reconnecting an already connected listener will disconnect the
         old listener. This may have rammifications in changing the filters
         and the priority.
@@ -386,7 +370,7 @@ def disconnect(self, cls, func):
         ------
         KeyError :
             if `func` is not already connected.
-            
+
         """
         if self._trace_func is not None:
             if self._trace_func('disconnect', self.disconnect,
@@ -409,7 +393,7 @@ def emit(self, event, block=True):
 
         Note
         ----
-        
+
         Listeners of superclasses of the event are also called.
         Eg. a :py:class:`BaseEvent` listener will also be notified about any
         derived class events.
@@ -445,7 +429,7 @@ def emit(self, event, block=True):
                     'event: {2}:\n{3}'.format(e, listener, event,
                                               traceback.format_exc()))
             if event._handled:
-                # Only enable when debugging -- very slow even if it doesn't get emitted because 
+                # Only enable when debugging -- very slow even if it doesn't get emitted because
                 # it must still do the string formatting.
                 #logger.debug('Event: {0} handled by listener: {1}'.format(
                 #                                        event, listener))
@@ -459,15 +443,15 @@ def get_event(self, cls=None):
         Parameters
         ----------
         cls : class
-            The class of the event we want the ``EventInfo`` for.        
+            The class of the event we want the ``EventInfo`` for.
             If ``cls`` is ``None``, then all known event types are returned.
-        
+
         Returns
         -------
         event_info :
             An ``EventInfo`` instance for the class, or a dictionary mapping
             classes to ``EventInfo`` instances.
-        
+
         """
         if cls is None:
             return self.event_map
@@ -476,25 +460,25 @@ def get_event(self, cls=None):
 
     def get_listeners(self, event, cls=None):
         """ Return listeners which will be called on specified event.
-        
+
         Parameters
         ----------
-        
+
         event : event instance or class
             The event we want to get the listeners for.
             If ``event`` is an instance of BaseEvent(), the listeners which will
             be called for the event are returned (satisfying any filters on the
             listeners).
-            
+
             If ``event`` is BaseEvent subclass, all listeners for specified event
             class are returned (but no filtering is performed).
-        
+
         cls : BaseEvent subclass
             ``cls`` argument is generally not needed, it is for internal use.
             If ``cls`` is specified as a subclass of ``BaseEvent``, then only
             listeners for the specified event class and superclasses are
             returned.
-        
+
         """
         evt_map = self.event_map
         if cls is None:
@@ -551,7 +535,7 @@ def is_enabled(self, cls):
 
     def get_event_hierarchy(self, cls):
         """ The the sequence of event classes which are notified for given cls.
-        
+
         """
         return cls.__mro__[:self.bmro_clip]
 
diff --git a/encore/events/heartbeat.py b/encore/events/heartbeat.py
index f1ac3cd..2f6626a 100644
--- a/encore/events/heartbeat.py
+++ b/encore/events/heartbeat.py
@@ -6,17 +6,12 @@
 # LICENSE.txt
 #
 
-import sys
 import time
 import threading
 
 from .abstract_event_manager import BaseEvent
 from .package_globals import get_event_manager
 
-if sys.platform == 'win32':
-    accurate_time = time.clock
-else:
-    accurate_time = time.time
 
 class HeartbeatEvent(BaseEvent):
     """ Event which is emitted periodically
@@ -25,34 +20,34 @@ class HeartbeatEvent(BaseEvent):
 
 class Heartbeat(object):
     """ Service which emits an event periodically
-    
+
     Note that unless the event manager uses threaded dispatch, event listeners
     which take longer than the interval to perform will result in a slower
     heartbeat.  The heartbeat runs on its own thread, and as a result, any
     listeners will also run on that thread.
-    
+
     The heartbeat is only intended to be approximately accurate, and should not
     be used for applications which require precise timing.
-    
+
     """
-    
+
     def __init__(self, interval=1/50., event_manager=None):
         self.state = 'waiting'
         self.interval = interval
         self.frame_count = 0
         self.event_manager = (event_manager if event_manager is not None
             else get_event_manager())
-    
+
     def run(self):
         self.state = 'running'
         while self.state in ['running', 'paused']:
             if self.state == 'running':
-                t = accurate_time()
+                t = time.monotonic()
                 self.event_manager.emit(HeartbeatEvent(source=self, time=t,
                     frame=self.frame_count, interval=self.interval))
                 self.frame_count += 1
                 # try to ensure regular heartbeat, but always sleep for at least 1ms
-                wait = max(t+self.interval-accurate_time(), 0.001)
+                wait = max(t+self.interval-time.monotonic(), 0.001)
             else:
                 wait = self.interval
             time.sleep(wait)
@@ -62,5 +57,3 @@ def serve(self):
         thread = threading.Thread(target=self.run)
         thread.daemon = True
         thread.start()
-    
-
diff --git a/encore/storage/abstract_store.py b/encore/storage/abstract_store.py
index d5d8317..2839e3b 100644
--- a/encore/storage/abstract_store.py
+++ b/encore/storage/abstract_store.py
@@ -22,9 +22,8 @@
 """
 
 from abc import ABCMeta, abstractmethod, abstractproperty
-from six.moves import zip as izip
 import fnmatch
-from six import BytesIO
+from io import BytesIO
 import warnings
 
 from encore.events.api import get_event_manager
@@ -843,7 +842,7 @@ def multiset(self, keys, values, buffer_size=1048576):
 
         """
         with self.transaction('Setting '+', '.join('"%s"' % key for key in keys)):
-            for key, value in izip(keys, values):
+            for key, value in zip(keys, values):
                 self.set(key, value, buffer_size)
 
 
@@ -886,7 +885,7 @@ def multiset_data(self, keys, datas, buffer_size=1048576):
 
         """
         with self.transaction('Setting data for '+', '.join('"%s"' % key for key in keys)):
-            for key, data in izip(keys, datas):
+            for key, data in zip(keys, datas):
                 self.set_data(key, data, buffer_size)
 
 
@@ -916,7 +915,7 @@ def multiset_metadata(self, keys, metadatas):
 
         """
         with self.transaction('Setting metadata for '+', '.join('"%s"' % key for key in keys)):
-            for key, metadata in izip(keys, metadatas):
+            for key, metadata in zip(keys, metadatas):
                 self.set_metadata(key, metadata)
 
 
@@ -946,7 +945,7 @@ def multiupdate_metadata(self, keys, metadatas):
 
         """
         with self.transaction('Updating metadata for '+', '.join('"%s"' % key for key in keys)):
-            for key, metadata in izip(keys, metadatas):
+            for key, metadata in zip(keys, metadatas):
                 self.update_metadata(key, metadata)
 
 
diff --git a/encore/storage/dict_memory_store.py b/encore/storage/dict_memory_store.py
index e87bdd0..0b6194a 100644
--- a/encore/storage/dict_memory_store.py
+++ b/encore/storage/dict_memory_store.py
@@ -17,7 +17,7 @@
 
 """
 
-from six import BytesIO
+from io import BytesIO
 import time
 
 from .abstract_store import AbstractStore
@@ -31,28 +31,28 @@
 
 class DictMemoryStore(AbstractStore):
     """ Dictionary-based in-memory Store
-    
+
     This is a simple implementation of the key-value store API that lives entirely
     in memory.  This uses a dictionary of StringValue objects to store all
     relevant information about an object - data and metadata are stored in
     private attributes.
 
     The streams returned by data methods are cStringIO.StringIO objects.
-    
+
     Parameters
     ----------
     event_manager :
         An object which implements the :py:class:`~.abstract_event_manager.BaseEventManager` API.
 
     """
-    
+
     def __init__(self):
         super(DictMemoryStore, self).__init__()
         self._store = {}
-    
+
     def connect(self, credentials=None):
         """ Connect to the key-value store
-        
+
         Parameters
         ----------
         credentials : None
@@ -61,11 +61,11 @@ def connect(self, credentials=None):
 
         """
         self._connected = True
-    
-    
+
+
     def disconnect(self):
         """ Disconnect from the key-value store
-        
+
         This store does not authenticate, and has no external resources, so this
         does nothing
 
@@ -75,7 +75,7 @@ def disconnect(self):
 
     def is_connected(self):
         """ Whether or not the store is currently connected
-        
+
         Returns
         -------
         connected : bool
@@ -84,10 +84,10 @@ def is_connected(self):
         """
         return self._connected
 
-    
+
     def info(self):
         """ Get information about the key-value store
-        
+
         Returns
         -------
         metadata : dict
@@ -97,17 +97,17 @@ def info(self):
         return {
             'readonly': False
         }
-    
-    
+
+
     def get(self, key):
         """ Retrieve a stream of data and metdata from a given key in the key-value store.
-        
+
         Parameters
         ----------
         key : string
             The key for the resource in the key-value store.  They key is a unique
             identifier for the resource within the key-value store.
-        
+
         Returns
         -------
         data : file-like
@@ -115,7 +115,7 @@ def get(self, key):
             key-value store
         metadata : dictionary
             A dictionary of metadata for the key.
-        
+
         Raises
         ------
         KeyError :
@@ -123,14 +123,14 @@ def get(self, key):
 
         """
         return StringValue(*self._store[key])
-    
-    
+
+
     def set(self, key, value, buffer_size=1048576):
         """ Store a stream of data into a given key in the key-value store.
-        
+
         This may be left unimplemented by subclasses that represent a read-only
         key-value store.
-        
+
         Parameters
         ----------
         key : string
@@ -144,7 +144,7 @@ def set(self, key, value, buffer_size=1048576):
             An optional indicator of the number of bytes to read at a time.
             Implementations are free to ignore this hint or use a different
             default if they need to.  The default is 1048576 bytes (1 MiB).
-        
+
         Events
         ------
         StoreProgressStartEvent :
@@ -159,7 +159,7 @@ def set(self, key, value, buffer_size=1048576):
         StoreSetEvent :
             On successful completion of a transaction, a StoreSetEvent should be
             emitted with the key & metadata
-     
+
         """
         if isinstance(value, tuple):
             data_stream, metadata = value
@@ -173,7 +173,7 @@ def set(self, key, value, buffer_size=1048576):
         progress = StoreProgressManager(source=self, steps=steps,
                 message="Setting data into '%s'" % (key,), key=key,
                 metadata=metadata)
-                
+
         with progress:
             chunks = buffer_iterator(data_stream, buffer_size, progress)
             data = b''.join(chunks)
@@ -184,7 +184,7 @@ def set(self, key, value, buffer_size=1048576):
             else:
                 created = modified = time.time()
             self._store[key] = (data, metadata, created, modified)
-        
+
         if update:
             self.event_manager.emit(StoreUpdateEvent(self, key=key,
                 metadata=metadata))
@@ -195,53 +195,53 @@ def set(self, key, value, buffer_size=1048576):
 
     def delete(self, key):
         """ Delete a key from the repsository.
-        
+
         Parameters
         ----------
         key : string
             The key for the resource in the key-value store.  They key is a unique
             identifier for the resource within the key-value store.
-        
+
         Events
         ------
         StoreDeleteEvent :
             On successful completion of a transaction, a StoreDeleteEvent should
             be emitted with the key.
-        
+
         """
         metadata = self._store[key][1]
         del self._store[key]
         self.event_manager.emit(StoreDeleteEvent(self, key=key,
             metadata=metadata))
-    
-    
+
+
     def exists(self, key):
         """ Test whether or not a key exists in the key-value store
-        
+
         Parameters
         ----------
         key : string
             The key for the resource in the key-value store.  They key is a unique
             identifier for the resource within the key-value store.
-        
+
         Returns
         -------
         exists : bool
             Whether or not the key exists in the key-value store.
-        
+
         """
         return key in self._store
 
 
     def get_data(self, key):
         """ Retrieve a stream from a given key in the key-value store.
-        
+
         Parameters
         ----------
         key : string
             The key for the resource in the key-value store.  They key is a unique
             identifier for the resource within the key-value store.
-        
+
         Returns
         -------
         data : file-like
@@ -254,7 +254,7 @@ def get_data(self, key):
 
     def get_metadata(self, key, select=None):
         """ Retrieve the metadata for a given key in the key-value store.
-        
+
         Parameters
         ----------
         key : string
@@ -263,13 +263,13 @@ def get_metadata(self, key, select=None):
         select : iterable of strings or None
             Which metadata keys to populate in the result.  If unspecified, then
             return the entire metadata dictionary.
-        
+
         Returns
         -------
         metadata : dict
             A dictionary of metadata associated with the key.  The dictionary
             has keys as specified by the metadata_keys argument.
-        
+
         Raises
         ------
         KeyError :
@@ -283,14 +283,14 @@ def get_metadata(self, key, select=None):
             return dict((metadata_key, metadata[metadata_key])
                 for metadata_key in select if metadata_key in metadata)
         return metadata.copy()
-    
-    
+
+
     def set_data(self, key, data, buffer_size=1048576):
         """ Replace the data for a given key in the key-value store.
-        
+
         If the key does not already exist, it tacitly creates an empty metadata
         object.
-        
+
         Parameters
         ----------
         key : string
@@ -323,14 +323,14 @@ def set_data(self, key, data, buffer_size=1048576):
         metadata = self._store.get(key, (None, {}))[1]
         self.set(key, (data, metadata))
 
-        
+
     def set_metadata(self, key, metadata):
         """ Set new metadata for a given key in the key-value store.
-        
+
         This replaces the existing metadata set for the key with a new set of
         metadata.  If the key does not already exist, it tacitly creates an
         empty data object.
-        
+
         Parameters
         ----------
         key : string
@@ -353,10 +353,10 @@ def set_metadata(self, key, metadata):
 
     def update_metadata(self, key, metadata):
         """ Update the metadata for a given key in the key-value store.
-        
+
         This performs a dictionary update on the existing metadata with the
         provided metadata keys and values
-        
+
         Parameters
         ----------
         key : string
@@ -374,13 +374,13 @@ def update_metadata(self, key, metadata):
 
         """
         self._store[key][1].update(metadata)
-   
-      
+
+
     def transaction(self, notes):
         """ Provide a transaction context manager
-        
+
         This class does not support transactions, so it returns a dummy object.
-        
+
         Parameters
         ----------
         notes : string
@@ -393,11 +393,11 @@ def transaction(self, notes):
 
     def query(self, select=None, **kwargs):
         """ Query for keys and metadata matching metadata provided as keyword arguments
-        
+
         This provides a very simple querying interface that returns precise
         matches with the metadata.  If no arguments are supplied, the query
         will return the complete set of metadata for the key-value store.
-        
+
         Parameters
         ----------
         select : iterable of strings or None
@@ -413,7 +413,7 @@ def query(self, select=None, **kwargs):
         result : iterable
             An iterable of keys, metadata tuples where metadata matches
             all the specified values for the specified metadata keywords.
-        
+
         """
         if select is not None:
             for key, value in self._store.items():
@@ -426,18 +426,18 @@ def query(self, select=None, **kwargs):
                 metadata = value[1]
                 if all(metadata.get(arg) == value for arg, value in kwargs.items()):
                     yield key, metadata.copy()
-    
-    
+
+
     def query_keys(self, **kwargs):
         """ Query for keys matching metadata provided as keyword arguments
-        
+
         This provides a very simple querying interface that returns precise
         matches with the metadata.  If no arguments are supplied, the query
         will return the complete set of keys for the key-value store.
-        
+
         This is equivalent to ``self.query(**kwargs).keys()``, but potentially
         more efficiently implemented.
-        
+
         Parameters
         ----------
         kwargs :
@@ -449,17 +449,17 @@ def query_keys(self, **kwargs):
         result : iterable
             An iterable of key-value store keys whose metadata matches all the
             specified values for the specified metadata keywords.
-        
+
         """
         for key, value in self._store.items():
             metadata = value[1]
             if all(metadata.get(arg) == value for arg, value in kwargs.items()):
                 yield key
 
-        
+
     def to_file(self, key, path, buffer_size=1048576):
         """ Efficiently store the data associated with a key into a file.
-        
+
         Parameters
         ----------
         key : string
@@ -469,17 +469,17 @@ def to_file(self, key, path, buffer_size=1048576):
             A file system path to store the data to.
         buffer_size : int
             This is ignored.
-        
+
         """
         with open(path, 'wb') as fp:
             fp.write(self._store[key][0])
-    
-    
+
+
     def from_file(self, key, path, buffer_size=1048576):
         """ Efficiently read data from a file into a key in the key-value store.
-        
+
         This makes no attempt to set metadata.
-                
+
         Parameters
         ----------
         key : string
@@ -489,14 +489,14 @@ def from_file(self, key, path, buffer_size=1048576):
             A file system path to read the data from.
         buffer_size : int
             This is ignored.
-        
+
         """
         with open(path, 'rb') as fp:
             self.set(key, StringValue(fp.read()))
 
     def to_bytes(self, key, buffer_size=1048576):
         """ Efficiently store the data associated with a key into a bytes object.
-        
+
         Parameters
         ----------
         key : string
@@ -504,15 +504,15 @@ def to_bytes(self, key, buffer_size=1048576):
             identifier for the resource within the key-value store.
         buffer_size : int
             This is ignored.
-        
+
         """
         return self._store[key][0]
-        
+
     def from_bytes(self, key, data, buffer_size=1048576):
         """ Efficiently read data from a bytes object into a key in the key-value store.
-        
+
         This makes no attempt to set metadata.
-                
+
         Parameters
         ----------
         key : string
@@ -522,6 +522,6 @@ def from_bytes(self, key, data, buffer_size=1048576):
             The data as a bytes object.
         buffer_size : int
             This is ignored.
-        
+
         """
         self.set(key, StringValue(data))
diff --git a/encore/storage/dynamic_url_store.py b/encore/storage/dynamic_url_store.py
index 4d8bbfa..5e1d248 100644
--- a/encore/storage/dynamic_url_store.py
+++ b/encore/storage/dynamic_url_store.py
@@ -22,7 +22,7 @@
 from email.utils import parsedate_tz, mktime_tz
 import json
 
-from six.moves.urllib.parse import quote
+from urllib.parse import quote
 
 import requests
 
diff --git a/encore/storage/filesystem_store.py b/encore/storage/filesystem_store.py
index 596d91a..d6fafcd 100644
--- a/encore/storage/filesystem_store.py
+++ b/encore/storage/filesystem_store.py
@@ -20,8 +20,6 @@
 import json
 import os
 
-import six
-
 # ETS library imports.
 from .abstract_store import AbstractStore
 from .file_value import FileValue
@@ -35,7 +33,7 @@ class FileSystemStoreError(Exception):
 def init_shared_store(path, magic_fname='.FSStore'):
     """Create the magic file for the shared store.  Useful to initialize
     the store for the first time.
-    
+
     Parameters
     ----------
     path :
@@ -56,10 +54,10 @@ class FileSystemStore(AbstractStore):
     A store that uses a Shared file system to store the data/metadata.
     """
     def __init__(self, path, magic_fname='.FSStore'):
-        """Initializes the store given a path to a store. 
-        
+        """Initializes the store given a path to a store.
+
         Parameters
-        ----------        
+        ----------
         path : str:
             A path to the root of the file system store.
         magic_fname :
@@ -69,36 +67,36 @@ def __init__(self, path, magic_fname='.FSStore'):
         super(FileSystemStore, self).__init__()
         self._root = path
         self._magic_fname = magic_fname
-        
+
         if not os.path.exists(path):
             raise FileSystemStoreError('Unable to find path %s'%path)
         # The path should have a .FSStore file.
         if not (os.path.exists(os.path.join(path, self._magic_fname))):
             raise FileSystemStoreError('Path %s is not a valid store'%path)
-    
+
     def connect(self, credentials=None):
         """ Connect to the key-value store.
-        
+
         Parameters
         ----------
-        credentials : 
+        credentials :
             These are not used by default.
-            
+
         """
         self._connected = True
-                
+
     def disconnect(self):
         """ Disconnect from the key-value store
-        
+
         This store does not authenticate, and has no external resources, so this
         does nothing
 
         """
         self._connected = False
-    
+
     def is_connected(self):
         """ Whether or not the store is currently connected
-        
+
         Returns
         -------
         connected : bool
@@ -106,30 +104,30 @@ def is_connected(self):
 
         """
         return self._connected
-    
+
     def info(self):
         """ Get information about the key-value store
-        
+
         Returns
         -------
-        
+
         metadata : dict
             A dictionary of metadata giving information about the key-value store.
         """
         return {'type': 'FileSystemStore', 'version': 0}
-    
+
     ##########################################################################
     # Basic Create/Read/Update/Delete Methods
     ##########################################################################
     def get(self, key):
         """ Retrieve a stream of data and metdata from a given key in the key-value store.
-        
+
         Parameters
         ----------
         key : string
             The key for the resource in the key-value store.  They key is a unique
             identifier for the resource within the key-value store.
-        
+
         Returns
         -------
         data : file-like
@@ -137,7 +135,7 @@ def get(self, key):
             key-value store
         metadata : dictionary
             A dictionary of metadata for the key.
-        
+
         Raises
         ------
         KeyError :
@@ -147,13 +145,13 @@ def get(self, key):
         data_path = self._get_data_path(key)
         metadata = self.get_metadata(key)
         return FileValue(data_path, metadata)
-    
+
     def set(self, key, value, buffer_size=1048576):
         """ Store a stream of data into a given key in the key-value store.
-        
+
         This may be left unimplemented by subclasses that represent a read-only
         key-value store.
-        
+
         Parameters
         ----------
         key : string
@@ -167,7 +165,7 @@ def set(self, key, value, buffer_size=1048576):
             An optional indicator of the number of bytes to read at a time.
             Implementations are free to ignore this hint or use a different
             default if they need to.  The default is 1048576 bytes (1 MiB).
-        
+
         Events
         ------
         StoreProgressStartEvent :
@@ -182,7 +180,7 @@ def set(self, key, value, buffer_size=1048576):
         StoreSetEvent :
             On successful completion of a transaction, a StoreSetEvent should be
             emitted with the key & metadata
-        
+
         """
         update = self.exists(key)
         metadata_path = self._get_metadata_path(key)
@@ -196,9 +194,6 @@ def set(self, key, value, buffer_size=1048576):
             steps = value.size
 
         json_string = json.dumps(metadata, ensure_ascii=False)
-        if six.PY2:
-            # convert the string to a unicode object
-            json_string = json_string.decode('utf-8')
         with io.open(metadata_path, 'w', encoding='utf-8') as fh:
             fh.write(json_string)
 
@@ -215,30 +210,30 @@ def set(self, key, value, buffer_size=1048576):
                         bytes_written += len(buffer)
                         progress("Setting key '%s' (%d bytes written)"
                             % (key, bytes_written))
-        
+
         if update:
             self.event_manager.emit(StoreUpdateEvent(self, key=key, metadata=metadata))
         else:
             self.event_manager.emit(StoreSetEvent(self, key=key, metadata=metadata))
-    
+
     def delete(self, key):
         """ Delete a key from the repsository.
-        
+
         This may be left unimplemented by subclasses that represent a read-only
         key-value store.
-        
+
         Parameters
         ----------
         key : string
             The key for the resource in the key-value store.  They key is a unique
             identifier for the resource within the key-value store.
-        
+
         Events
         ------
         StoreDeleteEvent :
             On successful completion of a transaction, a StoreDeleteEvent should
             be emitted with the key.
-            
+
         """
         metadata = self.get_metadata(key)
         metadata_path = self._get_metadata_path(key)
@@ -248,22 +243,22 @@ def delete(self, key):
         if os.path.exists(data_path):
             os.remove(data_path)
         self.event_manager.emit(StoreDeleteEvent(self, key=key, metadata=metadata))
-    
+
     def get_data(self, key):
         """ Retrieve a stream from a given key in the key-value store.
-        
+
         Parameters
         ----------
         key : string
             The key for the resource in the key-value store.  They key is a unique
             identifier for the resource within the key-value store.
-        
+
         Returns
         -------
         data : file-like
             A readable file-like object the that provides stream of data from the
             key-value store.
-        
+
         Raises
         ------
         KeyError :
@@ -275,10 +270,10 @@ def get_data(self, key):
             raise KeyError('Key %s does not exist in store!'%key)
         else:
             return open(data_path, 'rb')
-    
+
     def get_metadata(self, key, select=None):
         """ Retrieve the metadata for a given key in the key-value store.
-        
+
         Parameters
         ----------
         key : string
@@ -287,7 +282,7 @@ def get_metadata(self, key, select=None):
         select : iterable of strings or None
             Which metadata keys to populate in the result.  If unspecified, then
             return the entire metadata dictionary.
-        
+
         Returns
         -------
         metadata : dict
@@ -295,7 +290,7 @@ def get_metadata(self, key, select=None):
             has keys as specified by the select argument.  If a key specified in
             select is not present in the metadata, then it will not be present
             in the returned value.
-        
+
         Raises
         ------
         KeyError :
@@ -314,7 +309,7 @@ def get_metadata(self, key, select=None):
 
     def set_data(self, key, data, buffer_size=1048576):
         """ Replace the data for a given key in the key-value store.
-        
+
         Parameters
         ----------
         key : string
@@ -351,14 +346,14 @@ def set_data(self, key, data, buffer_size=1048576):
         else:
             metadata = self._get_metadata(metadata_path)
         self.set(key, (data, metadata), buffer_size)
-    
-    
+
+
     def set_metadata(self, key, metadata):
         """ Set new metadata for a given key in the key-value store.
-        
+
         This replaces the existing metadata set for the key with a new set of
         metadata.
-        
+
         Parameters
         ----------
         key : string
@@ -373,20 +368,20 @@ def set_metadata(self, key, metadata):
         StoreSetEvent :
             On successful completion of a transaction, a StoreSetEvent should be
             emitted with the key & metadata
-            
+
         """
         metadata_path = self._get_metadata_path(key)
         metadata_str = json.dumps(metadata).encode('utf-8')
         with open(metadata_path, 'wb') as fh:
             fh.write(metadata_str)
         self._touch(key)
-    
+
     def update_metadata(self, key, metadata):
         """ Update the metadata for a given key in the key-value store.
-        
+
         This performs a dictionary update on the existing metadata with the
         provided metadata keys and values
-        
+
         Parameters
         ----------
         key : string
@@ -401,37 +396,34 @@ def update_metadata(self, key, metadata):
         StoreSetEvent :
             On successful completion of a transaction, a StoreSetEvent should be
             emitted with the key & metadata
-        
+
         """
         update = self.exists(key)
         metadata_path = self._get_metadata_path(key)
         new_metadata = self._get_metadata(metadata_path)
         new_metadata.update(metadata)
         json_string = json.dumps(metadata, ensure_ascii=False)
-        if six.PY2:
-            # convert the string to a unicode object
-            json_string = json_string.decode('utf-8')
         with io.open(metadata_path, 'w', encoding='utf-8') as fh:
             fh.write(json_string)
         if update:
             self.event_manager.emit(StoreUpdateEvent(self, key=key, metadata=metadata))
         else:
             self.event_manager.emit(StoreSetEvent(self, key=key, metadata=metadata))
-    
+
     def exists(self, key):
         """ Test whether or not a key exists in the key-value store
-        
+
         Parameters
         ----------
         key : string
             The key for the resource in the key-value store.  They key is a unique
             identifier for the resource within the key-value store.
-        
+
         Returns
         -------
         exists : bool
             Whether or not the key exists in the key-value store.
-            
+
         """
         metadata_path = self._get_metadata_path(key)
         if os.path.exists(metadata_path):
@@ -441,7 +433,7 @@ def exists(self, key):
 
     def transaction(self, notes):
         """ Provide a transaction context manager
-        
+
         This class does not support transactions, so it returns a dummy object.
 
         """
@@ -450,17 +442,17 @@ def transaction(self, notes):
 
     def query(self, select=None, **kwargs):
         """ Query for keys and metadata matching metadata provided as keyword arguments
-        
+
         This provides a very simple querying interface that returns precise
         matches with the metadata.  If no arguments are supplied, the query
         will return the complete set of metadata for the key-value store.
-        
+
         Parameters
         ----------
         select : iterable of strings or None
             An optional list of metadata keys to return.  If this is not None,
             then the metadata dictionaries will only have values for the specified
-            keys populated.        
+            keys populated.
         kwargs :
             Arguments where the keywords are metadata keys, and values are
             possible values for that metadata item.
@@ -472,7 +464,7 @@ def query(self, select=None, **kwargs):
             all the specified values for the specified metadata keywords.
             If a key specified in select is not present in the metadata of a
             particular key, then it will not be present in the returned value.
-        
+
         """
         all_metadata = glob.glob(os.path.join(self._root, '*.metadata'))
         items = [(os.path.splitext(os.path.basename(x))[0], x) for x in all_metadata]
@@ -487,17 +479,17 @@ def query(self, select=None, **kwargs):
                 metadata = self._get_metadata(path)
                 if all(metadata.get(arg) == value for arg, value in kwargs.items()):
                     yield key, metadata.copy()
-    
+
     def query_keys(self, **kwargs):
         """ Query for keys matching metadata provided as keyword arguments
-        
+
         This provides a very simple querying interface that returns precise
         matches with the metadata.  If no arguments are supplied, the query
         will return the complete set of keys for the key-value store.
-        
+
         This is equivalent to ``self.query(**kwargs).keys()``, but potentially
         more efficiently implemented.
-        
+
         Parameters
         ----------
         kwargs :
@@ -509,7 +501,7 @@ def query_keys(self, **kwargs):
         result : iterable
             An iterable of key-value store keys whose metadata matches all the
             specified values for the specified metadata keywords.
-        
+
         """
         all_metadata = glob.glob(os.path.join(self._root, '*.metadata'))
         if kwargs:
@@ -532,13 +524,13 @@ def _get_metadata_path(self, key):
     def _get_data_path(self, key):
         path = os.path.join(self._root, key + '.data')
         return os.path.normpath(path)
-        
+
     def _get_metadata(self, path):
         with open(path, 'rb') as fh:
             content = fh.read()
         md = json.loads(content.decode('utf-8'))
         return md
-    
+
     def _touch(self, key):
         path = self._get_data_path(key)
         if os.path.exists(path):
diff --git a/encore/storage/locking_filesystem_store.py b/encore/storage/locking_filesystem_store.py
index 03bce03..f904cfc 100644
--- a/encore/storage/locking_filesystem_store.py
+++ b/encore/storage/locking_filesystem_store.py
@@ -36,8 +36,6 @@
 import time
 import threading
 
-from six import string_types
-
 # ETS library imports.
 from .events import StoreSetEvent, StoreUpdateEvent,\
     StoreDeleteEvent, StoreKeyEvent
@@ -50,7 +48,7 @@ def transact(function, on_commit=True):
     """ Wrap a store method to add command to transaction instead of executing
     it immediately if within a transaction context. If not in a transaction
     context, the command is executed immediately.
-    
+
     Parameters
     ----------
     function - callable
@@ -157,12 +155,12 @@ def __init__(self, path, force_lock_timeout=10.0,
                  magic_fname='.FSStore',
                  remote_event_poll_interval=5.0,
                  max_time_delta=datetime.timedelta(minutes=1)):
-        """Initializes the store given a path to a store. 
-        
+        """Initializes the store given a path to a store.
+
         Parameters
-        ----------        
+        ----------
         event_manager : Event Manager instance
-            This is used to emit suitable events.        
+            This is used to emit suitable events.
         path : str:
             A path to the root of the file system store.
         force_lock_timeout: float
@@ -177,7 +175,7 @@ def __init__(self, path, force_lock_timeout=10.0,
             The maximum permissible timedelta between different clients.
             This value is used to return the keys in a query_key when the
             parameters are ``last_modified__gte=timedelta`` , in this case the
-            max_time_delta is subtracted from the given query timedelta. 
+            max_time_delta is subtracted from the given query timedelta.
 
         """
         super(LockingFileSystemStore, self).__init__(path, magic_fname)
@@ -231,17 +229,17 @@ def transaction(self, notes):
 
     def query(self, select=None, **kwargs):
         """ Query for keys and metadata matching metadata provided as keyword arguments
-        
+
         This provides a very simple querying interface that returns precise
         matches with the metadata.  If no arguments are supplied, the query
         will return the complete set of metadata for the key-value store.
-        
+
         Parameters
         ----------
         select : iterable of strings or None
             An optional list of metadata keys to return.  If this is not None,
             then the metadata dictionaries will only have values for the specified
-            keys populated.        
+            keys populated.
         kwargs :
             Arguments where the keywords are metadata keys, and values are
             possible values for that metadata item.
@@ -253,7 +251,7 @@ def query(self, select=None, **kwargs):
             all the specified values for the specified metadata keywords.
             If a key specified in select is not present in the metadata of a
             particular key, then it will not be present in the returned value.
-        
+
         """
         all_metadata = glob.glob(os.path.join(self._root, '*.metadata'))
         items = [(os.path.splitext(os.path.basename(x))[0], x) for x in all_metadata]
@@ -270,17 +268,17 @@ def query(self, select=None, **kwargs):
                 metadata = self._get_metadata(path)
                 if all(metadata.get(arg) == value for arg, value in kwargs.items()):
                     yield key, metadata.copy()
-    
+
     def query_keys(self, **kwargs):
         """ Query for keys matching metadata provided as keyword arguments
-        
+
         This provides a very simple querying interface that returns precise
         matches with the metadata.  If no arguments are supplied, the query
         will return the complete set of keys for the key-value store.
-        
+
         This is equivalent to ``self.query(**kwargs).keys()``, but potentially
         more efficiently implemented.
-        
+
         Parameters
         ----------
         kwargs :
@@ -292,7 +290,7 @@ def query_keys(self, **kwargs):
         result : iterable
             An iterable of key-value store keys whose metadata matches all the
             specified values for the specified metadata keywords.
-        
+
         """
         # Optimize for special cases.
         basename = os.path.basename
@@ -324,7 +322,7 @@ def get_modified_keys(self, since):
 
         """
         timestamp = since
-        if isinstance(timestamp, string_types):
+        if isinstance(timestamp, str):
             ISO_FORMAT = "%Y-%m-%d %H:%M:%S.%f"
             timestamp = datetime.datetime.strptime(timestamp, ISO_FORMAT)
         timestamp -= self._max_time_delta
@@ -534,4 +532,3 @@ def _log_iter(self):
 
     def __del__(self):
         self._remote_poll_thread = None
-
diff --git a/encore/storage/sqlite_store.py b/encore/storage/sqlite_store.py
index 22c8506..f169a0e 100644
--- a/encore/storage/sqlite_store.py
+++ b/encore/storage/sqlite_store.py
@@ -22,9 +22,8 @@
 import sqlite3
 import time
 
-import six
-from six import BytesIO
-from six.moves import cPickle
+from io import BytesIO
+import pickle
 
 from .abstract_store import AbstractStore
 from .string_value import StringValue
@@ -34,19 +33,15 @@
     add_context_manager_support
 )
 
-if six.PY3:
-    buffer = sqlite3.Binary
+buffer = sqlite3.Binary
 
 
 def adapt_dict(d):
-    return buffer(cPickle.dumps(d, protocol=2))
+    return buffer(pickle.dumps(d, protocol=2))
 
 
 def convert_dict(s):
-    if six.PY3:
-        return cPickle.loads(s, encoding='ascii')
-    else:
-        return cPickle.loads(s)
+    return pickle.loads(s, encoding='ascii')
 
 sqlite3.register_adapter(dict, adapt_dict)
 sqlite3.register_converter('dict', convert_dict)
@@ -481,7 +476,7 @@ def query(self, select=None, **kwargs):
                     ' and '.join('"'+column+'"=?' for column in columns))
             else:
                 query = 'select key, metadata from "%s"' % (self.table,)
-            rows = self._connection.execute(query, [buffer(cPickle.dumps(kwargs[column], protocol=2))
+            rows = self._connection.execute(query, [buffer(pickle.dumps(kwargs[column], protocol=2))
                 for column in columns])
         else:
             unindexed_columns = set(kwargs)
@@ -537,7 +532,7 @@ def query_keys(self, **kwargs):
                         ' and '.join('"'+column+'"=?' for column in columns))
                 else:
                     query = 'select key from "%s"' % (self.table,)
-            rows = self._connection.execute(query, [buffer(cPickle.dumps(kwargs[column], protocol=2))
+            rows = self._connection.execute(query, [buffer(pickle.dumps(kwargs[column], protocol=2))
                 for column in columns])
         else:
             unindexed_columns = set(kwargs)
@@ -694,7 +689,7 @@ def _update_index(self, key, metadata):
             self.index_columns |= missing_columns
 
         columns = [column for column in metadata if column in self.index_columns]
-        values = [buffer(cPickle.dumps(metadata[column], protocol=2)) for column in columns]
+        values = [buffer(pickle.dumps(metadata[column], protocol=2)) for column in columns]
         if columns:
             self._update_columns(key, columns, values)
 
diff --git a/encore/storage/static_url_store.py b/encore/storage/static_url_store.py
index 1ab4ad1..7d45b00 100644
--- a/encore/storage/static_url_store.py
+++ b/encore/storage/static_url_store.py
@@ -28,7 +28,7 @@
 
 import threading
 import json
-from six.moves import urllib
+import urllib
 import time
 
 from .abstract_store import AbstractReadOnlyStore
@@ -39,10 +39,10 @@
 
 def basic_auth_factory(**kwargs):
     """ A factory that creates a :py:class:`~.HTTPBasicAuthHandler` instance
-    
+
     The arguments are passed directly through to the :py:meth:`add_password`
     method of the handler.
-    
+
     """
     auth_handler = urllib.request.HTTPBasicAuthHandler()
     auth_handler.add_password(**kwargs)
@@ -50,40 +50,40 @@ def basic_auth_factory(**kwargs):
 
 class StaticURLStore(AbstractReadOnlyStore):
     """ A read-only key-value store that is a front end for data served via URLs
-    
+
     All data is assumed to be served from some root url.  In addition
-    the store requires knowledge of two paths: a data prefix URL which is a 
+    the store requires knowledge of two paths: a data prefix URL which is a
     partial URL to which the keys will be appended when requesting data, and a
     query URL which is a single URL which provides all metadata as a json
     encoded file.
-    
+
     For example, an HTTP server may store data at URLs of the form::
-    
+
         http://www.example.com/data/<key>
-    
+
     and may store the metadata at::
-    
+
         http://www.example.com/index.json
-    
+
     These would have a root url of "http://www.example.com/", a data path
     of "data/" and a query path of "index.json".
-    
+
     All queries are performed using urllib.urlopen, so this store can be
     implemented by an HTTP, FTP or file server which serves static files.  When
     connecting, if appropriate credentials are supplied then HTTP authentication
     will be used when connecting the remote server
-   
+
     .. warning::
-    
+
         Since we use urllib without any further modifications, HTTPS requests
         do not validate the server's certificate.
-    
+
     Because of the limited nature of the interface, this store implementation
     is read only, and handles updates via periodic polling of the query prefix
     URL.  This guarantees that the viewed data is always consistent, it just may
     not be current. Most of the work of querying is done on the client side
     using the cached metadata.
-     
+
     Parameters
     ----------
     event_manager :
@@ -97,8 +97,8 @@ class StaticURLStore(AbstractReadOnlyStore):
         The URL that the metadata is served from.
     poll : float
         The polling frequency for the polling thread.  Polls every 5 min by default.
-    
-        
+
+
     """
     def __init__(self, root_url, data_path, query_path, poll=300):
         super(StaticURLStore, self).__init__()
@@ -106,18 +106,18 @@ def __init__(self, root_url, data_path, query_path, poll=300):
         self.data_path = data_path
         self.query_path = query_path
         self.poll = poll
-        
+
         self._opener = None
         self._index = None
         self._index_lock = threading.Lock()
         self._index_thread = None
 
-                
+
     def connect(self, credentials=None, proxy_handler=None, auth_handler_factory=None):
         """ Connect to the key-value store, optionally with authentication
-        
+
         This method creates appropriate urllib openers for the store.
-        
+
         Parameters
         ----------
         credentials : dict
@@ -133,7 +133,7 @@ def connect(self, credentials=None, proxy_handler=None, auth_handler_factory=Non
             An optional factory to build urllib authenticators.  The credentials
             will be passed as keyword arguments to this handler's add_password
             method.
-            
+
         """
         if credentials is not None:
             if auth_handler_factory is None:
@@ -151,19 +151,19 @@ def connect(self, credentials=None, proxy_handler=None, auth_handler_factory=Non
                 self._opener = urllib.request.build_opener()
             else:
                 self._opener = urllib.request.build_opener(auth_handler)
-        
+
         self.update_index()
         if self.poll > 0:
             self._index_thread = threading.Thread(target=self._poll)
             self._index_thread.start()
 
-        
+
     def disconnect(self):
         """ Disconnect from the key-value store
-        
+
         This method disposes or disconnects to any long-lived resources that the
         store requires.
-        
+
         """
 
         if self._index_thread is not None:
@@ -174,7 +174,7 @@ def disconnect(self):
 
     def is_connected(self):
         """ Whether or not the store is currently connected
-        
+
         Returns
         -------
         connected : bool
@@ -183,10 +183,10 @@ def is_connected(self):
         """
         return self._opener is not None
 
-    
+
     def info(self):
         """ Get information about the key-value store
-        
+
         Returns
         -------
         metadata : dict
@@ -196,21 +196,21 @@ def info(self):
             'readonly': True
         }
 
-        
+
     ##########################################################################
     # Basic Create/Read/Update/Delete Methods
     ##########################################################################
-    
-    
+
+
     def get(self, key):
         """ Retrieve a stream of data and metdata from a given key in the key-value store.
-        
+
         Parameters
         ----------
         key : string
             The key for the resource in the key-value store.  They key is a unique
             identifier for the resource within the key-value store.
-        
+
         Returns
         -------
         data : file-like
@@ -219,7 +219,7 @@ def get(self, key):
             by urllib's urlopen function.
         metadata : dictionary
             A dictionary of metadata for the key.
-        
+
         Raises
         ------
         KeyError :
@@ -231,16 +231,16 @@ def get(self, key):
             metadata = self._index[key].copy()
         return URLValue(url, metadata, self._opener)
 
-    
+
     def get_data(self, key):
         """ Retrieve a stream from a given key in the key-value store.
-        
+
         Parameters
         ----------
         key : string
             The key for the resource in the key-value store.  They key is a unique
             identifier for the resource within the key-value store.
-        
+
         Returns
         -------
         data : file-like
@@ -264,7 +264,7 @@ def get_data(self, key):
 
     def get_metadata(self, key, select=None):
         """ Retrieve the metadata for a given key in the key-value store.
-        
+
         Parameters
         ----------
         key : string
@@ -273,7 +273,7 @@ def get_metadata(self, key, select=None):
         select : iterable of strings or None
             Which metadata keys to populate in the result.  If unspecified, then
             return the entire metadata dictionary.
-        
+
         Returns
         -------
         metadata : dict
@@ -281,7 +281,7 @@ def get_metadata(self, key, select=None):
             has keys as specified by the select argument.  If a key specified in
             select is not present in the metadata, then it will not be present
             in the returned value.
-        
+
         Raises
         ------
         KeyError :
@@ -294,44 +294,44 @@ def get_metadata(self, key, select=None):
             else:
                 metadata = self._index[key]
                 return dict((s, metadata[s]) for s in select if s in metadata)
-        
-                
+
+
     def exists(self, key):
         """ Test whether or not a key exists in the key-value store
-        
+
         Parameters
         ----------
         key : string
             The key for the resource in the key-value store.  They key is a unique
             identifier for the resource within the key-value store.
-        
+
         Returns
         -------
         exists : bool
             Whether or not the key exists in the key-value store.
-        
+
         """
         with self._index_lock:
             return key in self._index
 
-        
+
     ##########################################################################
     # Querying Methods
     ##########################################################################
-    
+
     def query(self, select=None, **kwargs):
         """ Query for keys and metadata matching metadata provided as keyword arguments
-        
+
         This provides a very simple querying interface that returns precise
         matches with the metadata.  If no arguments are supplied, the query
         will return the complete set of metadata for the key-value store.
-        
+
         Parameters
         ----------
         select : iterable of strings or None
             An optional list of metadata keys to return.  If this is not None,
             then the metadata dictionaries will only have values for the specified
-            keys populated.        
+            keys populated.
         kwargs :
             Arguments where the keywords are metadata keys, and values are
             possible values for that metadata item.
@@ -355,17 +355,17 @@ def query(self, select=None, **kwargs):
                     if all(metadata.get(arg) == value for arg, value in kwargs.items()):
                         yield key, metadata.copy()
 
-    
+
     def query_keys(self, **kwargs):
         """ Query for keys matching metadata provided as keyword arguments
-        
+
         This provides a very simple querying interface that returns precise
         matches with the metadata.  If no arguments are supplied, the query
         will return the complete set of keys for the key-value store.
-        
+
         This is equivalent to ``self.query(**kwargs).keys()``, but potentially
         more efficiently implemented.
-        
+
         Parameters
         ----------
         kwargs :
@@ -377,30 +377,30 @@ def query_keys(self, **kwargs):
         result : iterable
             An iterable of key-value store keys whose metadata matches all the
             specified values for the specified metadata keywords.
-        
+
         """
         with self._index_lock:
             for key, metadata in self._index.items():
                 if all(metadata.get(arg) == value for arg, value in kwargs.items()):
                     yield key
 
-    
+
     ##########################################################################
     # Utility Methods
     ##########################################################################
 
     def update_index(self):
         """ Request the most recent version of the metadata
-        
+
         This downloads the json file at the query_path location, and updates
         the local metadata cache with this information.  It then emits events
         that represent the difference between the old metadata and the new
         metadata.
-        
+
         This method is normally called from the polling thread, but can be called
         by other code when needed.  It locks the metadata index whilst performing
         the update.
-        
+
         """
         url = self.root_url + self.query_path
         with self._index_lock:
@@ -424,11 +424,11 @@ def update_index(self):
                 if old_index[key] != index[key]:
                     self.event_manager.emit(StoreUpdateEvent(self, key=key, metadata=index[key]))
 
-        
+
     ##########################################################################
     # Private Methods
     ##########################################################################
-    
+
     def _poll(self):
         t = time.time()
         while self._opener is not None:
@@ -437,4 +437,3 @@ def _poll(self):
                 t = time.time()
             # tick
             time.sleep(0.5)
-
diff --git a/encore/storage/string_value.py b/encore/storage/string_value.py
index 207163b..333ed38 100644
--- a/encore/storage/string_value.py
+++ b/encore/storage/string_value.py
@@ -5,7 +5,7 @@
 # This file is open source software distributed according to the terms in LICENSE.txt
 #
 
-from six import BytesIO
+from io import BytesIO
 import time
 
 from .abstract_store import Value, AuthorizationError
diff --git a/encore/storage/url_value.py b/encore/storage/url_value.py
index 3b8f4bd..e1d8617 100644
--- a/encore/storage/url_value.py
+++ b/encore/storage/url_value.py
@@ -7,7 +7,7 @@
 
 from email.utils import parsedate_tz, mktime_tz
 
-from six.moves import urllib
+import urllib
 
 
 from .abstract_store import Value, AuthorizationError
diff --git a/encore/storage/utils.py b/encore/storage/utils.py
index b5dee7b..61f2e13 100644
--- a/encore/storage/utils.py
+++ b/encore/storage/utils.py
@@ -15,9 +15,7 @@
 
 import sys
 import itertools
-
-import six
-from six import create_bound_method
+from types import MethodType
 
 from encore.events.api import ProgressManager
 from .events import (StoreTransactionStartEvent, StoreTransactionEndEvent,
@@ -42,9 +40,9 @@ def __exit__(self, *args):
         pass
 
     if not hasattr(obj, '__enter__'):
-        obj.__enter__ = create_bound_method(__enter__, obj)
+        obj.__enter__ = MethodType(__enter__, obj)
     if not hasattr(obj, '__exit__'):
-        obj.__exit__ = create_bound_method(__exit__, obj)
+        obj.__exit__ = MethodType(__exit__, obj)
 
     return obj
 
diff --git a/examples/storage/todo_list.py b/examples/storage/todo_list.py
index eb5897e..2b3ee7e 100644
--- a/examples/storage/todo_list.py
+++ b/examples/storage/todo_list.py
@@ -24,27 +24,27 @@
 
 class ToDoList(object):
     """ To-do list class
-    
+
     This is a to-do list which stores its data in an encore.storage store.
     Any writeable store can be used for this (and a read-only store can even
     be used if it is pre-filled with appropriate data).
-    
+
     Attributes
     ----------
     store : AbstractStore instance
         The store which holds the information.
-    
+
     """
-    
+
     def __init__(self, store=None):
         if store is None:
             store = DictMemoryStore()
         self.store = store
         self.store.connect()
-        
+
     def add_todo(self, who, what, where, when):
         """ Add an item to the to-do list
-        
+
         Parameters
         ----------
         who : string
@@ -55,7 +55,7 @@ def add_todo(self, who, what, where, when):
             The location of the item
         when : datetime
             The time when the item will occur
-        
+
         """
         metadata = {
             'who': who,
@@ -73,31 +73,31 @@ def add_todo(self, who, what, where, when):
         key = when.isoformat()+'-'+who
         self.store.set(key, value)
         return key
-    
+
     def remove_todo(self, when, who):
         """ Remove an item to the to-do list
-        
+
         Parameters
         ----------
         when : datetime
             The time when the item to be removed occurs.
         who : str
             The person involved in the datetime to be removed.
-        
+
         """
         keys = self.model.store.query_keys(year=when.year, month=when.month,
             day=when.day, hour=when.hour, minute=when.minute, second=when.second, who=who)
         for key in keys:
             self.store.delete(key)
-    
+
     def todo_for_date(self, date):
         """ Return the list of items for a given date
-        
+
         Parameters
         ----------
         date : datetime.date
             The date to be shown.
-        
+
         """
         items = self.store.query(year=date.year, month=date.month, day=date.day)
         return sorted((self.store.get(key) for key, metadata in items),
@@ -106,10 +106,10 @@ def todo_for_date(self, date):
 
 class ToDoView(object):
     """ To-do view class
-    
+
     This is a class which provides a simple shell-based interface to the
     ToDoList class.
-    
+
     Attributes
     ----------
     model : ToDoList instance
@@ -118,16 +118,16 @@ class ToDoView(object):
         A format string suitable for printing a time using strftime.
     date_format : str
         A format string suitable for printing a date using strftime.
-    
+
     """
-    
+
     def __init__(self, model=None, time_format='%I:%M %p', date_format='%d/%m/%y'):
         if model is None:
             model = ToDoList()
         self.model = model
         self.time_format = time_format
         self.date_format = date_format
-    
+
     def show_summary(self, value, show_date=False):
         """ Display an item's summary """
         metadata = value.metadata
@@ -138,14 +138,14 @@ def show_summary(self, value, show_date=False):
             time_str = ''
         time = datetime.time(*metadata['time'])
         time_str += time.strftime(self.time_format)
-        print '{0} - {who} @ {where}'.format(time_str, **metadata)
+        print('{0} - {who} @ {where}'.format(time_str, **metadata))
         what = value.data.read(73)
         if '\n' in what:
             what = what.split('\n')[0]+'...'
         elif len(what) == 73:
             what = what[:72]+'...'
-        print '   {0}'.format(what)
-    
+        print('   {0}'.format(what))
+
     def show_item(self, value, show_date=False):
         """ Display an item's full information """
         metadata = value.metadata
@@ -156,21 +156,21 @@ def show_item(self, value, show_date=False):
             time_str = ''
         time = datetime.time(*metadata['time'])
         time_str += time.strftime(self.time_format)
-        print '''Time:  {0}
+        print('''Time:  {0}
 Who:   {who}
 Where: {where}
-'''.format(time_str, **metadata)
+'''.format(time_str, **metadata))
         for chunk in value.iterdata():
             sys.stdout.write(chunk)
-    
+
     def show_day(self, date=None):
         """ Display all items in a day """
         if date is None:
             date = datetime.date.today()
-        print date.strftime(self.date_format)
+        print(date.strftime(self.date_format))
         for value in self.model.todo_for_date(date):
             self.show_summary(value)
-    
+
     def add_todo(self, date=None, time=None, who=None, where=None):
         """ Add an item to the to-do list"""
         if date is None:
@@ -184,7 +184,7 @@ def add_todo(self, date=None, time=None, who=None, where=None):
             where = raw_input('Where: ')
         what = sys.stdin.read()
         self.model.add_todo(who, what, where, when)
-    
+
     def remove_todo(self, date=None, time=None, who=None):
         """ Remove an item to the to-do list"""
         if date is None:
@@ -194,4 +194,3 @@ def remove_todo(self, date=None, time=None, who=None):
         if who is None:
             who = raw_input('Who:   ')
         self.model.remove_todo(datetime.datetime.combine(date, time), who)
-        
\ No newline at end of file
diff --git a/setup.py b/setup.py
index e3bef26..883a9d7 100644
--- a/setup.py
+++ b/setup.py
@@ -2,16 +2,11 @@
 # All rights reserved.
 
 import os.path
+import runpy
 from setuptools import setup, find_packages
-import sys
 
 encore_init = os.path.join('encore', '__init__.py')
-if sys.version_info.major == 3:
-    import runpy
-    d = runpy.run_path(encore_init)
-else:
-    d = {}
-    execfile(encore_init, d)
+d = runpy.run_path(encore_init)
 
 setup(
     name='encore',
@@ -24,4 +19,5 @@
     long_description=open('README.rst').read(),
     packages=find_packages(),
     requires=[],
+    python_requires=">=3.6",
 )


Test patch:
diff --git a/.github/workflows/test-with-pip.yml b/.github/workflows/test-with-pip.yml
new file mode 100644
index 0000000..d98f0be
--- /dev/null
+++ b/.github/workflows/test-with-pip.yml
@@ -0,0 +1,30 @@
+name: Test with pip
+
+on:
+- pull_request
+- workflow_dispatch
+
+jobs:
+  tests:
+    strategy:
+      matrix:
+        os: [ubuntu-latest, windows-latest, macos-latest]
+        python-version: ['3.6', '3.8', '3.10']
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+    - name: Get encore source
+      uses: actions/checkout@v3
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v4
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: Install dependencies and local packages
+      run: |
+        python -m pip install .
+    - name: Run tests
+      run: |
+        mkdir testdir
+        cd testdir
+        python -m unittest discover -v encore
diff --git a/encore/concurrent/futures/tests/test_enhanced_thread_pool_executor.py b/encore/concurrent/futures/tests/test_enhanced_thread_pool_executor.py
index 45124c1..b7e0592 100644
--- a/encore/concurrent/futures/tests/test_enhanced_thread_pool_executor.py
+++ b/encore/concurrent/futures/tests/test_enhanced_thread_pool_executor.py
@@ -7,8 +7,6 @@
 import unittest
 import weakref
 
-from six.moves import xrange
-
 from concurrent import futures
 from concurrent.futures._base import (
     PENDING, RUNNING, CANCELLED, CANCELLED_AND_NOTIFIED, FINISHED, Future)
@@ -124,7 +122,7 @@ def test_run_after_shutdown(self):
 
     def test_interpreter_shutdown(self):
         # Test the atexit hook for shutdown of worker threads and processes
-        rc, out, err = assert_python_ok('-c', """from __future__ import print_function
+        rc, out, err = assert_python_ok('-c', """\
 if 1:
     from encore.concurrent.futures.enhanced_thread_pool_executor import EnhancedThreadPoolExecutor
     from time import sleep
@@ -373,7 +371,7 @@ def test_name(self):
         executor = EnhancedThreadPoolExecutor(max_workers=5)
         with executor as e:
             # Start some threads.
-            for _ in xrange(10):
+            for _ in range(10):
                 e.submit(pow, 2, 2)
             expected_prefix = "{0}Worker-".format(type(e).__name__)
             for t in e._threads:
@@ -392,7 +390,7 @@ def test_name(self):
         with executor as e:
             self.assertEqual(e.name, "DeadParrotExecutioner")
             # Start some threads.
-            for _ in xrange(10):
+            for _ in range(10):
                 e.submit(pow, 2, 2)
             expected_prefix = "DeadParrotExecutionerWorker-"
             for t in e._threads:
diff --git a/encore/events/tests/test_event_manager.py b/encore/events/tests/test_event_manager.py
index 970e79e..e4b3c72 100644
--- a/encore/events/tests/test_event_manager.py
+++ b/encore/events/tests/test_event_manager.py
@@ -7,7 +7,7 @@
 
 # Standard library imports.
 import unittest
-import mock
+import unittest.mock as mock
 import weakref
 import threading
 
@@ -653,7 +653,7 @@ def test_trace_veto(self):
         """ Test whether vetoing of actions works. """
         callback1 = mock.Mock()
         callback2 = mock.Mock()
-        
+
         self.evt_mgr.set_trace(self.trace_func_veto)
         self.evt_mgr.connect(BaseEvent, callback1)
         self.evt_mgr.emit(BaseEvent())
diff --git a/encore/storage/tests/abstract_test.py b/encore/storage/tests/abstract_test.py
index 34f5c68..38b4331 100644
--- a/encore/storage/tests/abstract_test.py
+++ b/encore/storage/tests/abstract_test.py
@@ -12,7 +12,7 @@
 import time
 from unittest import skip
 
-from six import BytesIO
+from io import BytesIO
 
 from ..utils import add_context_manager_support
 
diff --git a/encore/storage/tests/sqlite_store_test.py b/encore/storage/tests/sqlite_store_test.py
index 1259e6d..a63c8bf 100644
--- a/encore/storage/tests/sqlite_store_test.py
+++ b/encore/storage/tests/sqlite_store_test.py
@@ -12,13 +12,10 @@
 import time
 from unittest import TestCase
 
-import six
-
 from .abstract_test import StoreReadTestMixin, StoreWriteTestMixin
 from ..sqlite_store import SqliteStore
 
-if six.PY3:
-    buffer = sqlite3.Binary
+buffer = sqlite3.Binary
 
 class SqliteStoreReadTest(TestCase, StoreReadTestMixin):
 
diff --git a/encore/storage/tests/static_url_store_test.py b/encore/storage/tests/static_url_store_test.py
index e07854b..47420bb 100644
--- a/encore/storage/tests/static_url_store_test.py
+++ b/encore/storage/tests/static_url_store_test.py
@@ -5,19 +5,16 @@
 # This file is open source software distributed according to the terms in LICENSE.txt
 #
 
+from http.server import HTTPServer, SimpleHTTPRequestHandler
+import itertools
 import json
 import os
 from shutil import rmtree
+import socketserver
 from tempfile import mkdtemp
 import threading
 from unittest import TestCase
-
-
-from six import itertools, next
-from six.moves import urllib
-from six.moves import socketserver as SocketServer
-from six.moves.BaseHTTPServer import HTTPServer
-from six.moves.SimpleHTTPServer import SimpleHTTPRequestHandler
+import urllib
 
 from .abstract_test import StoreReadTestMixin, StoreWriteTestMixin
 from ..static_url_store import StaticURLStore
@@ -111,7 +108,7 @@ def _write_index(self, filename, data):
         with open(os.path.join(self.path, filename), 'wb') as fp:
             fp.write(data.encode('ascii'))
 
-class ThreadedHTTPServer(SocketServer.ThreadingMixIn, HTTPServer):
+class ThreadedHTTPServer(socketserver.ThreadingMixIn, HTTPServer):
     pass
 
 class StaticURLStoreHTTPReadTest(StaticURLStoreReadTest):
@@ -141,4 +138,4 @@ def _tear_down_server(self):
 
 if __name__ == '__main__':
     import unittest
-    unittest.main()
\ No newline at end of file
+    unittest.main()
diff --git a/encore/storage/tests/test_file_lock.py b/encore/storage/tests/test_file_lock.py
index a774854..ce5a86e 100644
--- a/encore/storage/tests/test_file_lock.py
+++ b/encore/storage/tests/test_file_lock.py
@@ -31,14 +31,14 @@ def cleanup_files():
 class FileLockTest(unittest.TestCase):
 
     def setUp(self):
-        _, self.path = tempfile.mkstemp(
+        self.fd, self.path = tempfile.mkstemp(
             suffix='share', dir=tempfile.gettempdir()
         )
         self.lock = FileLock(self.path)
 
     def tearDown(self):
-        if os.path.exists(self.path):
-            os.remove(self.path)
+        os.close(self.fd)
+        os.remove(self.path)
 
     # This is needed to clean up stray lock file in case of killing a test run.
     # Note: tempnam seems buggy on Windows msys system; existing filenames are
@@ -85,7 +85,7 @@ def test_force_timeout(self):
         self.assertTrue(self.lock.locked())
         lock2.release()
         self.assertFalse(self.lock.locked())
-        
+
     def test_data(self):
         self.lock = FileLock(self.path, data=b"%i\n" % os.getpid())
         self.lock.acquire()