diff --git a/CHANGES.rst b/CHANGES.rst index 76e4be7..11097fb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,10 @@ Change Log ========== +0.17.2 +------ +* Python 3 fixes using `six` compatibility library (#16, #17). + 0.17.1 ------ * Fix minor issue where some subscription queries still used slow queries. diff --git a/README.rst b/README.rst index b6911ea..0f5712c 100644 --- a/README.rst +++ b/README.rst @@ -18,12 +18,21 @@ Peer servers subscribe to aggregate broadcast events which are de-multiplexed an Limitations ----------- -* No support for the SockJS protocol to support browsers that - don't have WebSockets_ (see http://caniuse.com/websockets for - supported browsers). +* No support for the SockJS XHR fallback protocol to support browsers + that don't have WebSockets_ (see http://caniuse.com/websockets for + supported browsers). It is noteworthy that the only current browser + listed that doesn't support WebSockets_ is Opera Mini, which doesn't + support pages that use EcmaScript (JavaScript) for interactivity + anyway. Offering SockJS XHR fallback wouldn't help to substantially + increase browser support: if Opera Mini is excluded then all current + browser versions including IE, Edge, Firefox, Chrome, Safari, Opera, + iOS Safari, Android Browser Android and Chrome for Android are + supported. Having said all that, pull requests are welcome. * Changes must be made via the Django ORM as django-ddp uses `Django - signals`_ to receive model save/update signals. + signals`_ to receive model save/update signals. There are no + technical reasons why database triggers couldn't be used - pull + requests are welcome. Installation diff --git a/bumpversion.sh b/bumpversion.sh index cb8ac50..2ec0b95 100755 --- a/bumpversion.sh +++ b/bumpversion.sh @@ -6,6 +6,8 @@ NEW="$( git rev-parse --abbrev-ref HEAD | cut -d / -f 2 )" echo "Bumping version ${OLD} -> ${NEW}...\n" sed -e "s/^ version='${OLD}'/ version='${NEW}'/" setup.py > .setup.py mv .setup.py setup.py +sed -e "s/^__version__ = '${OLD}'$/__version__ = '${NEW}'/" dddp/__init__.py > .__init__.py +mv .__init__.py dddp/__init__.py sed -e "s/^version = '${OLD%.*}'/version = '${NEW%.*}'/" -e "s/^release = '${OLD}'/release = '${NEW}'/" docs/conf.py > docs/.conf.py mv docs/.conf.py docs/conf.py -git diff setup.py docs/conf.py +git diff setup.py dddp/__init__.py docs/conf.py diff --git a/dddp/__init__.py b/dddp/__init__.py index f60ad98..6278adb 100644 --- a/dddp/__init__.py +++ b/dddp/__init__.py @@ -2,19 +2,10 @@ from __future__ import unicode_literals import os.path import sys -from pkg_resources import get_distribution, DistributionNotFound from gevent.local import local from dddp import alea -try: - _dist = get_distribution('django-ddp') - if not __file__.startswith(os.path.join(_dist.location, 'django-ddp', '')): - # not installed, but there is another version that *is* - raise DistributionNotFound -except DistributionNotFound: - __version__ = 'development' -else: - __version__ = _dist.version +__version__ = '0.17.2' default_app_config = 'dddp.apps.DjangoDDPConfig' @@ -27,9 +18,10 @@ _GREEN = {} def greenify(): """Patch threading and psycopg2 modules for green threads.""" - # no need to greenify twice. + # don't greenify twice. if _GREEN: return + _GREEN[True] = True from gevent.monkey import patch_all, saved if ('threading' in sys.modules) and ('threading' not in saved): @@ -39,12 +31,10 @@ def greenify(): from psycogreen.gevent import patch_psycopg patch_psycopg() - # ensure we don't greenify again - _GREEN[True] = True - try: # Use psycopg2 by default import psycopg2 + del psycopg2 except ImportError: # Fallback to psycopg2cffi if required (eg: pypy) from psycopg2cffi import compat @@ -81,28 +71,40 @@ class ThreadLocal(local): def get(self, name, factory, *factory_args, **factory_kwargs): """Get attribute, creating if required using specified factory.""" update_thread_local = getattr(factory, 'update_thread_local', True) - if (not update_thread_local) or (not hasattr(self, name)): + if (not update_thread_local) or (name not in self.__dict__): obj = factory(*factory_args, **factory_kwargs) if update_thread_local: setattr(self, name, obj) return obj return getattr(self, name) + @staticmethod + def get_factory(name): + """Get factory for given name.""" + return THREAD_LOCAL_FACTORIES[name] + class RandomStreams(object): + """Namespaced PRNG generator.""" + def __init__(self): + """Initialize with random PRNG state.""" self._streams = {} self._seed = THREAD_LOCAL.alea_random.hex_string(20) def get_seed(self): + """Retrieve current PRNG seed.""" return self._seed + def set_seed(self, val): + """Set current PRNG seed.""" self._streams = {} self._seed = val random_seed = property(get_seed, set_seed) def __getitem__(self, key): + """Get namespaced PRNG stream.""" if key not in self._streams: return self._streams.setdefault(key, alea.Alea(self._seed, key)) return self._streams[key] @@ -124,6 +126,7 @@ METEOR_ID_CHARS = u'23456789ABCDEFGHJKLMNPQRSTWXYZabcdefghijkmnopqrstuvwxyz' def meteor_random_id(name=None, length=17): + """Generate a new ID, optionally using namespace of given `name`.""" if name is None: stream = THREAD_LOCAL.alea_random else: @@ -132,6 +135,7 @@ def meteor_random_id(name=None, length=17): def autodiscover(): + """Import all `ddp` submodules from `settings.INSTALLED_APPS`.""" from django.utils.module_loading import autodiscover_modules from dddp.api import API autodiscover_modules('ddp', register_to=API) diff --git a/dddp/alea.py b/dddp/alea.py index 22abd5d..b8a91ee 100755 --- a/dddp/alea.py +++ b/dddp/alea.py @@ -26,28 +26,30 @@ This implementation of Alea defaults to a more secure initial internal state. >>> random = Alea("my", 3, "seeds") ->>> random.random_string(17, UNMISTAKABLE) -'JYRduBwQtjpeCkqP7' +>>> random.random_string(17, UNMISTAKABLE) == 'JYRduBwQtjpeCkqP7' +True ->>> random.random_string(17, UNMISTAKABLE) -'HLxYtpZBtSain84zj' +>>> random.random_string(17, UNMISTAKABLE) == 'HLxYtpZBtSain84zj' +True ->>> random.random_string(17, UNMISTAKABLE) -'s9XrbWaDC4yCL5NCW' +>>> random.random_string(17, UNMISTAKABLE) == 's9XrbWaDC4yCL5NCW' +True ->>> random.random_string(17, UNMISTAKABLE) -'SCiymgNnZpwda9vSH' +>>> random.random_string(17, UNMISTAKABLE) == 'SCiymgNnZpwda9vSH' +True ->>> random.random_string(17, UNMISTAKABLE) -'hui3ThSoZrFrdFDTT' +>>> random.random_string(17, UNMISTAKABLE) == 'hui3ThSoZrFrdFDTT' +True >>> random = Alea("my", 3, "seeds") ->>> random.random_string(43, BASE64) -'tHBM5k8z4TZOmU0zgsv9H4ZIl4CJSXic_T3iF2KFJnm' +>>> random.random_string(43, BASE64) == \ + 'tHBM5k8z4TZOmU0zgsv9H4ZIl4CJSXic_T3iF2KFJnm' +True """ +from __future__ import unicode_literals from math import floor import os @@ -78,8 +80,8 @@ class Mash(object): def __call__(self, data): """Return mash, updating internal state.""" - data = bytes(data) - for byte in bytes(data): + data = str(data) + for byte in data: self.n += ord(byte) h = 0.02519603282416938 * self.n self.n = floor(h) @@ -112,7 +114,7 @@ class Alea(object): # a much more secure seed by default to avoid hash collisions. seed_ids = [int, str, random, self, values, self.__class__] random.shuffle(seed_ids) - values = map(id, seed_ids) + [time.time(), os.urandom(512)] + values = list(map(id, seed_ids)) + [time.time(), os.urandom(512)] mash = Mash() self.c = 1 diff --git a/dddp/api.py b/dddp/api.py index 2681bc8..fc0a745 100644 --- a/dddp/api.py +++ b/dddp/api.py @@ -25,6 +25,7 @@ from django.utils.module_loading import import_string from django.db import DatabaseError from django.db.models import signals import ejson +import six # django-ddp from dddp import ( @@ -69,12 +70,11 @@ class Array(aggregates.Aggregate): def add_to_query(self, query, alias, col, source, is_summary): """Override source field internal type so the raw array is returned.""" + @six.add_metaclass(dbarray.ArrayFieldMetaclass) class ArrayField(dbarray.ArrayFieldBase, source.__class__): """ArrayField for override.""" - __metaclass__ = dbarray.ArrayFieldMetaclass - @staticmethod def get_internal_type(): """Return ficticious type so Django doesn't cast as int.""" @@ -203,7 +203,9 @@ class APIMixin(object): """Clear out cache for api_path_map.""" self._api_path_cache = None for api_provider in self.api_providers: - if api_provider.clear_api_path_map_cache.im_self is not None: + if six.get_method_self( + api_provider.clear_api_path_map_cache, + ) is not None: api_provider.clear_api_path_map_cache() def api_endpoint(self, api_path): @@ -241,12 +243,11 @@ class CollectionMeta(APIMeta): return super(CollectionMeta, mcs).__new__(mcs, name, bases, attrs) +@six.add_metaclass(CollectionMeta) class Collection(APIMixin): """DDP Model Collection.""" - __metaclass__ = CollectionMeta - name = None model = None qs_filter = None @@ -522,12 +523,11 @@ class PublicationMeta(APIMeta): return super(PublicationMeta, mcs).__new__(mcs, name, bases, attrs) +@six.add_metaclass(PublicationMeta) class Publication(APIMixin): """DDP Publication (a set of queries).""" - __metaclass__ = PublicationMeta - name = None queries = None @@ -553,12 +553,11 @@ class Publication(APIMixin): ) +@six.add_metaclass(APIMeta) class DDP(APIMixin): """Django DDP API.""" - __metaclass__ = APIMeta - pgworker = None _in_migration = False diff --git a/docs/conf.py b/docs/conf.py index 058a98d..b9b5206 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -58,7 +58,7 @@ copyright = u'2015, Tyson Clugg' # The short X.Y version. version = '0.17' # The full version, including alpha/beta/rc tags. -release = '0.17.1' +release = '0.17.2' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/requirements.txt b/requirements.txt index 08a16e3..e0617b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ Django==1.8.5 gevent==1.0.2 gevent-websocket==0.9.5 psycopg2==2.6.1 +six==1.10.0 diff --git a/setup.py b/setup.py index 8d5468a..eef06fe 100644 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ CLASSIFIERS = [ setup( name='django-ddp', - version='0.17.1', + version='0.17.2', description=__doc__, long_description=open('README.rst').read(), author='Tyson Clugg', @@ -62,6 +62,7 @@ setup( 'psycogreen>=1.0', 'django-dbarray>=0.2', 'pybars3>=0.9.1', + 'six>=1.10.0', ], entry_points={ 'console_scripts': [