diff --git a/.gitignore b/.gitignore index 84ec61b..20d18b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.egg-info +*.bak *.pyc .*.sw? build diff --git a/baph/__init__.py b/baph/__init__.py index 921098f..cd91a2e 100644 --- a/baph/__init__.py +++ b/baph/__init__.py @@ -1,28 +1,67 @@ +from importlib import import_module +import os +import sys +from types import ModuleType + +import six +from six.moves import html_parser + + +try: + from collections import MutableMapping +except ImportError: + import collections + import collections.abc + collections.MutableMapping = collections.abc.MutableMapping + collections.Mapping = collections.abc.Mapping + +try: + HTMLParseError = html_parser.HTMLParseError +except AttributeError: + # create a dummy class for Python 3.5+ where it's been removed + class HTMLParseError(Exception): + pass + + html_parser.HTMLParseError = HTMLParseError + + +if six.PY3: + # coffin/template/__init__.py uses 'import library' for a relative import, + # which doesn't work in python3. To get around this we create an actual + # 'library' module so the import works + pass + + def setup(): - from baph.apps import apps - from baph.conf import settings - from baph.utils.log import configure_logging + from baph.apps import AppConfig, apps + from baph.conf import settings + from baph.utils.log import configure_logging + + configure_logging(settings.LOGGING_CONFIG, settings.LOGGING) + + module = ModuleType('django.apps') + setattr(module, 'AppConfig', AppConfig) + sys.modules['django.apps'] = module + + apps.populate(settings.INSTALLED_APPS) - configure_logging(settings.LOGGING_CONFIG, settings.LOGGING) - apps.populate(settings.INSTALLED_APPS) def replace_settings_class(): - from django import conf - from baph.conf import settings - conf.settings = settings + from django import conf + from baph.conf import settings + conf.settings = settings + def apply_patches(): - import os - from importlib import import_module - - patch_dir = os.path.join(os.path.dirname(__file__), 'patches') - for mod_name in os.listdir(patch_dir): - filename = os.path.join(patch_dir, mod_name) - with open(filename, 'rt') as fp: - src = fp.read() - code = compile(src, filename, 'exec') - mod = import_module(mod_name) - exec(code, mod.__dict__) + patch_dir = os.path.join(os.path.dirname(__file__), 'patches') + for mod_name in os.listdir(patch_dir): + filename = os.path.join(patch_dir, mod_name) + with open(filename, 'rt') as fp: + src = fp.read() + code = compile(src, filename, 'exec') + mod = import_module(mod_name) + exec(code, mod.__dict__) + replace_settings_class() -apply_patches() \ No newline at end of file +apply_patches() diff --git a/baph/apps/__init__.py b/baph/apps/__init__.py index ce57dee..79091dc 100644 --- a/baph/apps/__init__.py +++ b/baph/apps/__init__.py @@ -1,4 +1,4 @@ from .config import AppConfig from .registry import apps -__all__ = ['AppConfig', 'apps'] \ No newline at end of file +__all__ = ['AppConfig', 'apps'] diff --git a/baph/apps/registry.py b/baph/apps/registry.py index 1fd9f62..57c3251 100644 --- a/baph/apps/registry.py +++ b/baph/apps/registry.py @@ -1,7 +1,10 @@ import threading from collections import Counter, OrderedDict, defaultdict -from functools32 import lru_cache +try: + from functools32 import lru_cache +except ImportError: + from functools import lru_cache from .config import AppConfig diff --git a/baph/auth/models/__init__.py b/baph/auth/models/__init__.py index 5cc65e2..c92ab3e 100644 --- a/baph/auth/models/__init__.py +++ b/baph/auth/models/__init__.py @@ -1,11 +1,7 @@ from .permission import Permission -print 'Permission imported' from .organization import Organization -print 'Organization imported' from .group import Group -print 'Group imported' from .user import AnonymousUser, User -print 'User imported' from .usergroup import UserGroup from .permissionassociation import PermissionAssociation from .oauth_ import OAuthConsumer, OAuthNonce diff --git a/baph/auth/models/oauth_/__init__.py b/baph/auth/models/oauth_/__init__.py index b5c2fec..b39a657 100644 --- a/baph/auth/models/oauth_/__init__.py +++ b/baph/auth/models/oauth_/__init__.py @@ -1,5 +1,5 @@ from django.conf import settings -from oauth import oauth +import oauth2 as oauth from sqlalchemy import (Column, DateTime, ForeignKey, Integer, String, UniqueConstraint) from sqlalchemy.orm import relationship @@ -30,7 +30,7 @@ def as_consumer(self): '''Creates an oauth.OAuthConsumer object from the DB data. :rtype: oauth.OAuthConsumer ''' - return oauth.OAuthConsumer(self.key, self.secret) + return oauth.Consumer(self.key, self.secret) class OAuthNonce(Base): diff --git a/baph/auth/registration/forms.py b/baph/auth/registration/forms.py index 12beeba..1419edc 100644 --- a/baph/auth/registration/forms.py +++ b/baph/auth/registration/forms.py @@ -11,6 +11,7 @@ from django import forms from django.contrib.auth import authenticate from django.utils.translation import ugettext_lazy as _ +import six from sqlalchemy.orm import joinedload from baph.auth.models import User, Organization @@ -188,7 +189,7 @@ def save(self): """ Generate a random username before falling back to parent signup form """ session = orm.sessionmaker() while True: - username = unicode(sha_constructor(str(random.random())).hexdigest()[:5]) + username = six.text_type(sha_constructor(str(random.random()).encode('utf8')).hexdigest()[:5]) user = session.query(User).filter(User.username==username).first() if not user: break @@ -211,7 +212,7 @@ def __init__(self, user, *args, **kwargs): """ super(ChangeEmailForm, self).__init__(*args, **kwargs) if not isinstance(user, User): - raise TypeError, "user must be an instance of %s" % User._meta.model_name + raise TypeError("user must be an instance of %s" % User._meta.model_name) else: self.user = user def clean_email(self): diff --git a/baph/auth/registration/managers.py b/baph/auth/registration/managers.py index 45c1335..2360744 100644 --- a/baph/auth/registration/managers.py +++ b/baph/auth/registration/managers.py @@ -1,6 +1,7 @@ from datetime import datetime import re +import six from sqlalchemy.orm import joinedload from baph.auth.models import User, Organization @@ -19,7 +20,7 @@ class SignupManager(object): @staticmethod def create_user(username, email, password, active=False, send_email=True, **kwargs): - uname = username.encode('utf-8') if isinstance(username, unicode) else username + uname = username.encode('utf-8') if isinstance(username, six.text_type) else username salt, activation_key = generate_sha1(uname) #org_key = Organization._meta.verbose_name diff --git a/baph/auth/registration/tests/decorators.py b/baph/auth/registration/tests/decorators.py index 61a5c43..2722d73 100644 --- a/baph/auth/registration/tests/decorators.py +++ b/baph/auth/registration/tests/decorators.py @@ -27,7 +27,7 @@ def test_secure_required(self): # Test if the redirected url contains 'https'. Couldn't use # ``assertRedirects`` here because the redirected to page is # non-existant. - self.assertTrue('https' in str(response)) + self.assertTrue('https' in response['location']) # Set back to the old settings auth_settings.BAPH_USE_HTTPS = False diff --git a/baph/auth/registration/tests/forms.py b/baph/auth/registration/tests/forms.py index 6cb8032..8c0f7c2 100644 --- a/baph/auth/registration/tests/forms.py +++ b/baph/auth/registration/tests/forms.py @@ -175,7 +175,7 @@ class ChangeEmailFormTests(TestCase): def test_change_email_form(self): session = orm.sessionmaker() user = session.query(User).get(1) - session.close() + #session.close() invalid_data_dicts = [ # No change in e-mail address {'data': {'email': 'john@example.com'}, diff --git a/baph/auth/registration/tests/managers.py b/baph/auth/registration/tests/managers.py index 017541e..8c13248 100644 --- a/baph/auth/registration/tests/managers.py +++ b/baph/auth/registration/tests/managers.py @@ -161,6 +161,7 @@ def test_delete_expired_users(self): expired_user = SignupManager.create_user(**self.user_info) expired_user.date_joined -= datetime.timedelta(days=auth_settings.BAPH_ACTIVATION_DAYS + 1) expired_user.save() + self.session.expunge_all() deleted_users = SignupManager.delete_expired_users() diff --git a/baph/auth/registration/tests/views.py b/baph/auth/registration/tests/views.py index b559d93..f261d52 100644 --- a/baph/auth/registration/tests/views.py +++ b/baph/auth/registration/tests/views.py @@ -38,7 +38,7 @@ def test_valid_activation(self): # reverse('baph_profile_detail', kwargs={'username': user.username})) user = session.query(User).filter_by(email='alice@example.com').first() - session.close() + #session.close() self.failUnless(user.is_active) def test_activation_expired_retry(self): @@ -61,7 +61,7 @@ def test_activation_expired_retry(self): self.assertContains(response, "Request a new activation link") user = session.query(User).filter_by(email='alice@example.com').first() - session.close() + #session.close() self.failUnless(not user.is_active) auth_settings.BAPH_ACTIVATION_RETRY = False @@ -86,12 +86,12 @@ def test_retry_activation_ask(self): # We must reload the object from database to get the new key user = session.query(User).filter_by(email='alice@example.com').first() new_key = user.signup.activation_key - session.close() + #session.close() self.assertContains(response, "Account re-activation succeded") self.failIfEqual(old_key, new_key) user = session.query(User).filter_by(email='alice@example.com').first() - session.close() + #session.close() self.failUnless(not user.is_active) self.failUnlessEqual(len(mail.outbox), 2) @@ -103,7 +103,7 @@ def test_retry_activation_ask(self): session = orm.sessionmaker() user = session.query(User).filter_by(email='alice@example.com').first() - session.close() + #session.close() self.failUnless(user.is_active) auth_settings.BAPH_ACTIVATION_RETRY = False @@ -332,6 +332,7 @@ def test_signout_view(self): def test_change_email_view(self): """ A ``GET`` to the change e-mail view. """ + auth_settings.BAPH_AUTH_WITHOUT_USERNAMES = False response = self.client.get(reverse('baph_email_change')) # Anonymous user should not be able to view the profile page @@ -349,6 +350,7 @@ def test_change_email_view(self): self.assertTemplateUsed(response, 'registration/email_form.html') + auth_settings.BAPH_AUTH_WITHOUT_USERNAMES = True def test_change_valid_email_view(self): """ A ``POST`` with a valid e-mail address """ @@ -369,8 +371,10 @@ def test_change_password_view(self): self.failUnless(response.context['form'], PasswordChangeForm) + #@override_settings(BAPH_AUTH_WITHOUT_USERNAMES=False) def test_change_password_view_success(self): """ A valid ``POST`` to the password change view """ + auth_settings.BAPH_AUTH_WITHOUT_USERNAMES = False self.client.login(identification='john', password='blowfish') new_password = 'suckfish' @@ -384,7 +388,7 @@ def test_change_password_view_success(self): # Check that the new password is set. session = orm.sessionmaker() john = session.query(User).filter_by(username='john').first() - session.close() + #session.close() self.failUnless(john.check_password(new_password)) ''' diff --git a/baph/auth/utils.py b/baph/auth/utils.py index ca344e5..8116994 100644 --- a/baph/auth/utils.py +++ b/baph/auth/utils.py @@ -19,7 +19,7 @@ def generate_sha1(string, salt=None): """ if not salt: - salt = sha1(str(random.random())).hexdigest()[:5] - hash = sha1(salt+str(string)).hexdigest() + salt = sha1(str(random.random()).encode('utf8')).hexdigest()[:5] + hash = sha1(salt.encode('utf8') + str(string).encode('utf8')).hexdigest() return (salt, hash) diff --git a/baph/conf/__init__.py b/baph/conf/__init__.py index fb931a0..9e115de 100644 --- a/baph/conf/__init__.py +++ b/baph/conf/__init__.py @@ -7,10 +7,14 @@ import pkgutil import sys -from chainmap import ChainMap +try: + from collections import ChainMap +except ImportError: + from chainmap import ChainMap from django.conf import global_settings from django.core.exceptions import ImproperlyConfigured from django.utils.functional import LazyObject, empty +import six from baph.core.preconfig.loader import PreconfigLoader @@ -47,8 +51,7 @@ def __new__(cls, name, bases, attrs): attrs['__module__'] = 'django.conf' return super(SettingsMeta, cls).__new__(cls, name, bases, attrs) -class LazySettings(LazyObject): - __metaclass__ = SettingsMeta +class LazySettings(six.with_metaclass(SettingsMeta, LazyObject)): def _setup(self, name=None): settings_module = os.environ.get(ENVIRONMENT_VARIABLE) @@ -164,6 +167,10 @@ def load_settings_module(self, module, explicit=True): if setting.isupper(): setting_value = getattr(module, setting) self.apply_setting(setting, setting_value, explicit) + if hasattr(module, 'apply'): + # call the apply func, passing the current settings dict + module.apply(self.__dict__) + self.actions = self.actions.parents logger.info(msg.ljust(64) + 'SUCCESS') @@ -201,7 +208,9 @@ def get_package_path(self, package): return None if not loader.is_package(package): raise ValueError('%r is not a package' % package) - self.package_paths[package] = loader.filename + fullpath = loader.get_filename() + path, filename = fullpath.rsplit('/', 1) + self.package_paths[package] = path return self.package_paths[package] @staticmethod @@ -222,7 +231,7 @@ def compile_module(module): content = fp.read() node = ast.parse(content, path) code = compile(node, path, 'exec') - exec code in module.__dict__ + exec(code, module.__dict__) def load_module_settings(self, module_name): msg = ' %s' % module_name diff --git a/baph/context_processors/__init__.py b/baph/context_processors/__init__.py deleted file mode 100644 index 53c374b..0000000 --- a/baph/context_processors/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- -'''\ -:mod:`baph.context_processors` -- Template Context Processors -============================================================= - -.. moduleauthor:: Mark Lee -''' - - -def http(request): - '''A restricted set of variables from the - :class:`~django.http.HttpResponse` object. - ''' - gzip = 'gzip' in request.META.get('HTTP_ACCEPT_ENCODING', '') - return { - 'gzip_acceptable': gzip, - 'is_secure': request.is_secure(), - } diff --git a/baph/core/cache/backends/memcached.py b/baph/core/cache/backends/memcached.py index 7db3750..b1b76ed 100644 --- a/baph/core/cache/backends/memcached.py +++ b/baph/core/cache/backends/memcached.py @@ -1,66 +1,181 @@ +from collections import defaultdict +from functools import partial +import operator import time from django.core.cache.backends.memcached import MemcachedCache +from six.moves import map, reduce +from six.moves.urllib.parse import unquote -class BaphMemcachedCache(MemcachedCache): - """ - An extension of the django memcached Cache class - """ - def __init__(self, server, params): - super(BaphMemcachedCache, self).__init__(server, params) - self.version = params.get('VERSION', 0) +def parse_stats_line(line): + # line format: STAT + return (line[1], line[2]) + +def parse_metadump_line(line): + # line format: =* + split = operator.methodcaller('split', '=', 1) + + data = dict(list(map(split, line))) + data['key'] = unquote(data['key']) + return data + +def parse_cachedump_line(line, **kwargs): + data = dict(kwargs) + key, metadata = line + data['key'] = key #.encode('utf8') + metadata = metadata[1:-1] # strip brackets + for key in ('size', 'exp'): + stat, sep, metadata = metadata.partition(';') + value, unit = stat.strip().split(' ', 1) + data[key] = int(value) + return data + +def parse_stats(response): + """ parse the response of a stats command """ + return list(map(parse_stats_line, response)) + +def parse_slabs(response): + slabs = defaultdict(dict) + for key, value in response: + # key format: items:: + _, slab_id, key = key.split(':', 2) + slabs[slab_id][key] = value + return slabs + +def parse_slab_stats(response): + slab_stats = defaultdict(dict) + for key, value in parse_stats(response): + # key format 1: : + # key format 2: + if ':' in key: + slab_id, key = key.split(':') + else: + slab_id = 'totals' + slab_stats[slab_id][key] = value + return slab_stats - def delete_many_raw(self, keys): +def parse_cachedump(response): + return list(map(parse_cachedump_line, response)) + +def parse_metadump(response): + return list(map(parse_metadump_line, response)) + + +class MemcacheServer(object): + def __init__(self, server): + self.server = server + self.settings = dict(self.get_stats('settings')) + if self.settings.get('lru_crawler') == 'yes': + self._get_keys = self.get_keys_from_metadump + else: + self._get_keys = self.get_keys_from_cachedump + + @property + def slabs(self): + return self.get_slabs() + + def send_command(self, cmd, row_len=0, expect=None): """ - Deletes the specified keys (does not run them through make_key) + send a command to the server, and returns a parsed response + + the response is a list of lines + each line in the list is a list of space-delimited strings """ - self._cache.delete_multi(keys) + if not self.server.connect(): + return + self.server.send_cmd(cmd) + if expect: + self.server.expect(expect) + return None + lines = [] + while True: + line = self.server.readline() + if not line: + break + line = line.decode('ascii').strip() + while line: + # readline splits on '\r\n', we still need to split on '\n' + item, sep, line = line.partition('\n') + if item == 'END': + return lines + else: + lines.append(item.split(' ', row_len-1)) + return lines - def flush_all(self): - for s in self._cache.servers: - if not s.connect(): continue - s.send_cmd('flush_all') - s.expect("OK") - self.delete_many_raw(self.get_server_keys(s)) + # base commands - def get_all_keys(self): - keys = set() - for s in self._cache.servers: - keys.update(self.get_server_keys(s)) - return keys + def get_stats(self, *args): + """ returns a list of (key, value) tuples for a stats command """ + cmd = ' '.join(('stats',) + args) + rsp = self.send_command(cmd, 3) + return parse_stats(rsp) - def get_server_keys(self, s): - keys = set() - slab_ids = self.get_server_slab_ids(s) - for slab_id in slab_ids: - keys.update(self.get_slab_keys(s, slab_id)) - return keys + def get_metadump(self, slab_id, limit): + # metadump doesn't support a limit param, so we need to handle it + limit = limit or None + cmd = 'lru_crawler metadump %s' % slab_id + rsp = self.send_command(cmd) + return parse_metadump(rsp)[:limit] - def get_slab_keys(self, s, slab_id): - keys = set() - s.send_cmd('stats cachedump %s 100' % slab_id) - readline = s.readline + # STATS command variants + + def get_slabs(self): + """ gets configuration settings for active slabs """ + rsp = self.get_stats('items') + return parse_slabs(rsp) + + def get_slab_stats(self): + """ gets statistics for active slabs """ + rsp = self.get_stats('slabs') + return parse_slab_stats(rsp) + + def get_cachedump(self, slab_id, limit): + limit = limit or 0 + cmd = 'cachedump %s %s' % (slab_id, limit) + rsp = self.get_stats(cmd) + return parse_cachedump(rsp) + + # other commands + + def get_keys_from_metadump(self, limit=None): + """ returns all keys on the server using 'lru_crawler metadump' """ + return self.get_metadump('all', limit) + + def get_keys_from_cachedump(self, limit=None): + """ returns all keys on the server using 'stats cachedump' """ + func = partial(self.get_cachedump, limit=limit) + if self.slabs: + return reduce(operator.concat, list(map(func, self.slabs))) + return [] + + def get_keys(self, limit=None, include_expired=False): + """ returns all keys on the server, as a list of strings """ + getter = operator.itemgetter('key') ts = time.time() - while 1: - line = readline() - if not line or line.strip() == 'END': break - frags = line.split(' ') - key = frags[1] - expire = int(frags[4]) - if expire > ts: - keys.add(key) + items = self._get_keys(limit) + if not include_expired: + items = filter(lambda x: float(x['exp']) > ts, items) + return list(map(getter, items)) + + +class BaphMemcachedCache(MemcachedCache): + """ An extension of the django memcached Cache class """ + def __init__(self, server, params): + super(BaphMemcachedCache, self).__init__(server, params) + self.version = params.get('VERSION', 0) + self.alias = params.get('ALIAS', None) + self.servers = [MemcacheServer(s) for s in self._cache.servers] + + def get_all_keys(self): + keys = set() + for server in self.servers: + try: + del server.slabs + except AttributeError: + pass + keys.update(server.get_keys()) return keys - def get_server_slab_ids(self, s): - if not s.connect(): - return set() - slab_ids = set() - s.send_cmd('stats items') - readline = s.readline - while 1: - line = readline() - if not line or line.strip() == 'END': break - frags = line.split(':') - slab_ids.add(frags[1]) - return slab_ids + def flush_all(self): + self.clear() diff --git a/baph/core/handlers/base.py b/baph/core/handlers/base.py index bf91f4e..6743559 100644 --- a/baph/core/handlers/base.py +++ b/baph/core/handlers/base.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import from __future__ import unicode_literals import logging @@ -11,13 +12,13 @@ from django.core.urlresolvers import get_urlconf, set_urlconf, RegexURLResolver from django.db import connections, transaction from django.utils import six -from baph.utils.module_loading import import_string +from baph.utils.module_loading import import_string from .exception import ( convert_exception_to_response, get_exception_response, handle_uncaught_exception, ) -from utils import get_resolver +from .utils import get_resolver logger = logging.getLogger('django.request') diff --git a/baph/core/management/base.py b/baph/core/management/base.py index 125c6e3..6c802cb 100644 --- a/baph/core/management/base.py +++ b/baph/core/management/base.py @@ -3,7 +3,6 @@ be executed through ``django-admin.py`` or ``manage.py``). """ -from cStringIO import StringIO from optparse import make_option, OptionParser import os import sys @@ -13,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured from django.core.management.color import color_style #from django.utils.encoding import force_str -#from django.utils.six import StringIO +from django.utils.six import StringIO from baph.core.preconfig.loader import PreconfigLoader diff --git a/baph/core/management/commands/flush.py b/baph/core/management/commands/flush.py index fc538dc..e439e57 100644 --- a/baph/core/management/commands/flush.py +++ b/baph/core/management/commands/flush.py @@ -9,7 +9,7 @@ from baph.core.management.new_base import BaseCommand, CommandError from baph.core.management.sql import emit_post_sync_signal from baph.db import ORM, DEFAULT_DB_ALIAS -from baph.db.models import signals, get_apps, get_models +from baph.db.models import get_apps, get_models orm = ORM.get() diff --git a/baph/core/management/commands/loaddata.py b/baph/core/management/commands/loaddata.py index 2ef98f8..f244dd6 100644 --- a/baph/core/management/commands/loaddata.py +++ b/baph/core/management/commands/loaddata.py @@ -69,6 +69,7 @@ def get_deferred_updates(session): deferred.append((type(obj), filters, update)) return deferred + class Command(BaseCommand): help = 'Installs the named fixture(s) in the database.' missing_args_message = ("No database fixture specified. Please provide " @@ -76,49 +77,49 @@ class Command(BaseCommand): "line.") def add_arguments(self, parser): - parser.add_argument( - 'args', metavar='fixture', nargs='+', help='Fixture labels.') - parser.add_argument( - '--database', action='store', dest='database', - default=DEFAULT_DB_ALIAS, - help='Nominates a specific database to load fixtures into. ' + parser.add_argument( + 'args', metavar='fixture', nargs='+', help='Fixture labels.') + parser.add_argument( + '--database', action='store', dest='database', + default=DEFAULT_DB_ALIAS, + help='Nominates a specific database to load fixtures into. ' 'Defaults to the "default" database.' - ) - parser.add_argument( - '--app', action='store', dest='app_label', default=None, - help='Only look for fixtures in the specified app.', - ) - parser.add_argument( - '--ignorenonexistent', '-i', action='store_true', - dest='ignore', default=False, help='Ignores entries in the ' - 'serialized data for fields that do not currently exist ' - 'on the model.' - ) - parser.add_argument( - '--format', action='store', dest='format', default=None, - help='Format of serialized data when reading from stdin.', - ) + ) + parser.add_argument( + '--app', action='store', dest='app_label', default=None, + help='Only look for fixtures in the specified app.', + ) + parser.add_argument( + '--ignorenonexistent', '-i', action='store_true', + dest='ignore', default=False, help='Ignores entries in the ' + 'serialized data for fields that do not currently exist ' + 'on the model.' + ) + parser.add_argument( + '--format', action='store', dest='format', default=None, + help='Format of serialized data when reading from stdin.', + ) def handle(self, *fixture_labels, **options): - self.ignore = options['ignore'] - self.using = options['database'] - self.app_label = options['app_label'] - self.verbosity = options['verbosity'] - #self.excluded_models, self.excluded_apps = parse_apps_and_model_labels(options['exclude']) - self.format = options['format'] - - ''' - with transaction.atomic(using=self.using): + self.ignore = options['ignore'] + self.using = options['database'] + self.app_label = options['app_label'] + self.verbosity = options['verbosity'] + #self.excluded_models, self.excluded_apps = parse_apps_and_model_labels(options['exclude']) + self.format = options['format'] + + ''' + with transaction.atomic(using=self.using): self.loaddata(fixture_labels) - # Close the DB connection -- unless we're still in a transaction. This - # is required as a workaround for an edge case in MySQL: if the same - # connection is used to create tables, load data, and query, the query - # can return incorrect results. See Django #7572, MySQL #37735. - if transaction.get_autocommit(self.using): + # Close the DB connection -- unless we're still in a transaction. This + # is required as a workaround for an edge case in MySQL: if the same + # connection is used to create tables, load data, and query, the query + # can return incorrect results. See Django #7572, MySQL #37735. + if transaction.get_autocommit(self.using): connections[self.using].close() - ''' - self.loaddata(fixture_labels) + ''' + self.loaddata(fixture_labels) def loaddata(self, fixture_labels): #connection = connections[self.using] @@ -153,10 +154,11 @@ def loaddata(self, fixture_labels): self.load_label(fixture_label) ''' session = orm.sessionmaker() - session.close() + #session.close() for fixture_label in fixture_labels: self.load_label(fixture_label) - session.commit() + #session.commit() + session.flush() # Since we disabled constraint checks, we must manually check for # any invalid keys that might have been added @@ -342,35 +344,35 @@ def _find_fixtures(self, fixture_label): @cached_property def fixture_dirs(self): - """ - Return a list of fixture directories. - - The list contains the 'fixtures' subdirectory of each installed - application, if it exists, the directories in FIXTURE_DIRS, and the - current directory. - """ - dirs = [] - fixture_dirs = settings.FIXTURE_DIRS - if len(fixture_dirs) != len(set(fixture_dirs)): - raise ImproperlyConfigured("settings.FIXTURE_DIRS contains " - "duplicates.") - for path in get_app_paths(): - app_dir = os.path.join(os.path.dirname(path), 'fixtures') - if app_dir in fixture_dirs: - raise ImproperlyConfigured( - "'%s' is a default fixture directory for the '%s' app " - "and cannot be listed in settings.FIXTURE_DIRS." - % (app_dir, app_label) - ) - - if self.app_label and app_label != self.app_label: - continue - if os.path.isdir(app_dir): - dirs.append(app_dir) - dirs.extend(list(fixture_dirs)) - dirs.append('') - dirs = [upath(os.path.abspath(os.path.realpath(d))) for d in dirs] - return dirs + """ + Return a list of fixture directories. + + The list contains the 'fixtures' subdirectory of each installed + application, if it exists, the directories in FIXTURE_DIRS, and the + current directory. + """ + dirs = [] + fixture_dirs = settings.FIXTURE_DIRS + if len(fixture_dirs) != len(set(fixture_dirs)): + raise ImproperlyConfigured("settings.FIXTURE_DIRS contains " + "duplicates.") + for path in get_app_paths(): + app_dir = os.path.join(os.path.dirname(path), 'fixtures') + if app_dir in fixture_dirs: + raise ImproperlyConfigured( + "'%s' is a default fixture directory for the '%s' app " + "and cannot be listed in settings.FIXTURE_DIRS." + % (app_dir, app_label) + ) + + if self.app_label and app_label != self.app_label: + continue + if os.path.isdir(app_dir): + dirs.append(app_dir) + dirs.extend(list(fixture_dirs)) + dirs.append('') + dirs = [upath(os.path.abspath(os.path.realpath(d))) for d in dirs] + return dirs def parse_name(self, fixture_name): """ diff --git a/baph/core/management/commands/purge.py b/baph/core/management/commands/purge.py index ab3f3c4..1a1955c 100644 --- a/baph/core/management/commands/purge.py +++ b/baph/core/management/commands/purge.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import print_function from copy import deepcopy from optparse import make_option import sys @@ -12,6 +13,7 @@ from django.dispatch import Signal from django.utils.datastructures import SortedDict from django.utils.importlib import import_module +from six.moves import input from sqlalchemy import MetaData, inspect, create_engine from sqlalchemy.orm.session import Session from sqlalchemy.schema import (CreateSchema, DropSchema, @@ -20,7 +22,7 @@ from baph.core.management.new_base import BaseCommand from baph.db import DEFAULT_DB_ALIAS -from baph.db.models import signals, get_apps, get_models +from baph.db.models import get_apps, get_models from baph.db.orm import ORM @@ -92,13 +94,13 @@ def handle(self, **options): db_info = orm.settings_dict is_test_db = db_info.get('TEST', False) if not is_test_db: - print 'Database "%s" cannot be purged because it is not a test ' \ + print('Database "%s" cannot be purged because it is not a test ' \ 'database.\nTo flag this as a test database, set TEST to ' \ - 'True in the database settings.' % db + 'True in the database settings.' % db) sys.exit() if interactive: - confirm = raw_input('\nYou have requested a purge of database ' \ + confirm = input('\nYou have requested a purge of database ' \ '"%s" (%s). This will IRREVERSIBLY DESTROY all data ' \ 'currently in the database, and DELETE ALL TABLES AND ' \ 'SCHEMAS. Are you sure you want to do this?\n\n' \ diff --git a/baph/core/management/commands/shell.py b/baph/core/management/commands/shell.py index 432115f..338beed 100644 --- a/baph/core/management/commands/shell.py +++ b/baph/core/management/commands/shell.py @@ -1,26 +1,32 @@ import os from optparse import make_option -from baph.core.management.base import NoArgsCommand +#from baph.core.management.base import NoArgsCommand +from baph.core.management.new_base import BaseCommand -class Command(NoArgsCommand): - shells = ['ipython', 'bpython'] +SHELLS = ['ipython', 'bpython'] - option_list = NoArgsCommand.option_list + ( - make_option('--plain', action='store_true', dest='plain', - help='Tells Django to use plain Python, not IPython or bpython.'), - make_option('--no-startup', action='store_true', dest='no_startup', +class Command(BaseCommand): + help = "Runs a Python interactive interpreter. Tries to use IPython or " \ + "bpython, if one of them is available." + + def add_arguments(self, parser): + parser.add_argument( + '--plain', action='store_true', dest='plain', + help='Tells Django to use plain Python, not IPython or bpython.' + ) + parser.add_argument( + '--no-startup', action='store_true', dest='no_startup', help='When using plain Python, ignore the PYTHONSTARTUP ' - 'environment variable and ~/.pythonrc.py script.'), - make_option('-i', '--interface', action='store', type='choice', - choices=shells, dest='interface', help='Specify an interactive ' + 'environment variable and ~/.pythonrc.py script.' + ) + parser.add_argument( + '-i', '--interface', action='store', choices=SHELLS, + dest='interface', help='Specify an interactive ' 'interpreter interface. Available options: "ipython" and ' - '"bpython"'), - ) - help = "Runs a Python interactive interpreter. Tries to use IPython or " \ - "bpython, if one of them is available." - requires_model_validation = True + '"bpython"' + ) def ipython(self): try: @@ -45,7 +51,7 @@ def bpython(self): bpython.embed() def run_shell(self, shell=None): - available_shells = [shell] if shell else self.shells + available_shells = [shell] if shell else SHELLS for shell in available_shells: try: @@ -54,12 +60,7 @@ def run_shell(self, shell=None): pass raise ImportError - def handle_noargs(self, **options): - # XXX: (Temporary) workaround for ticket #1796: force early loading of - # all models from installed apps. - #from baph.db.models.loading import get_models - #get_models() - + def handle(self, **options): use_plain = options.get('plain', False) no_startup = options.get('no_startup', False) interface = options.get('interface', None) diff --git a/baph/core/management/commands/syncdb.py b/baph/core/management/commands/syncdb.py index b78a199..9a650ae 100644 --- a/baph/core/management/commands/syncdb.py +++ b/baph/core/management/commands/syncdb.py @@ -13,7 +13,7 @@ from baph.core.management.base import NoArgsCommand from baph.core.management.sql import emit_post_sync_signal from baph.db import DEFAULT_DB_ALIAS -from baph.db.models import signals, get_apps, get_models +from baph.db.models import get_apps, get_models from baph.db.orm import ORM, Base diff --git a/baph/core/preconfig/config.py b/baph/core/preconfig/config.py index 5db7e51..cc5a37a 100644 --- a/baph/core/preconfig/config.py +++ b/baph/core/preconfig/config.py @@ -115,7 +115,7 @@ def package_metavars(self): @property def package_tpls(self): " returns the package name templates " - packages = [self.package] + map(templatize, self.package_args) + packages = [self.package] + list(map(templatize, self.package_args)) return packages @property @@ -222,7 +222,7 @@ def load_options(self, data): else: raise ValueError('Invalid scope %r (must be "package" or "module")') self.arg_map[name] = opt - self.module_options = sorted(modules, key=lambda x: x.order) + self.module_options = sorted(modules, key=lambda x: str(x.order)) self.package_options = packages def add_to_parser(self, parser): diff --git a/baph/core/serializers/json.py b/baph/core/serializers/json.py index c17287e..2e82272 100644 --- a/baph/core/serializers/json.py +++ b/baph/core/serializers/json.py @@ -1,17 +1,17 @@ from __future__ import absolute_import from __future__ import unicode_literals - -from StringIO import StringIO import datetime import decimal import json import sys -from baph.core.serializers.python import Serializer as PythonSerializer -from baph.core.serializers.python import Deserializer as PythonDeserializer from django.core.serializers.base import DeserializationError from django.core.serializers.json import DjangoJSONEncoder from django.utils import six +from six.moves import StringIO + +from baph.core.serializers.python import Serializer as PythonSerializer +from baph.core.serializers.python import Deserializer as PythonDeserializer class Serializer(PythonSerializer): diff --git a/baph/core/serializers/python.py b/baph/core/serializers/python.py index f496b91..eb9d1c0 100644 --- a/baph/core/serializers/python.py +++ b/baph/core/serializers/python.py @@ -1,4 +1,5 @@ -from django.utils.encoding import smart_text, is_protected_type +from django.conf import settings +from django.utils.encoding import smart_unicode, is_protected_type from sqlalchemy.orm.util import identity_key from baph.core.serializers import base @@ -55,7 +56,7 @@ def Deserializer(object_list, **options): # Handle each field for (field_name, field_value) in d.iteritems(): - if isinstance(field_value, str): + if isinstance(field_value, bytes): field_value = smart_unicode(field_value, options.get("encoding", settings.DEFAULT_CHARSET), strings_only=True) diff --git a/baph/core/validators.py b/baph/core/validators.py index d34e44a..6b949e8 100644 --- a/baph/core/validators.py +++ b/baph/core/validators.py @@ -1,9 +1,10 @@ from django.core import validators +import six class MaxLengthValidator(validators.MaxLengthValidator): - def clean(self, content): - if isinstance(content, unicode): - return len(content.encode('utf8')) - else: - return len(content) + def clean(self, content): + if isinstance(content, six.text_type): + return len(content.encode('utf8')) + else: + return len(content) diff --git a/baph/db/backends/__init__.py b/baph/db/backends/__init__.py index a791eb2..e1b3160 100644 --- a/baph/db/backends/__init__.py +++ b/baph/db/backends/__init__.py @@ -44,7 +44,7 @@ def django_config_to_sqla_config(config): 'database': config.get('NAME', None), 'query': config.get('OPTIONS', None), } - for k, v in params.items(): + for k, v in list(params.items()): if not v: del params[k] return params @@ -99,8 +99,14 @@ def __init__(self, settings_dict, alias=DEFAULT_DB_ALIAS): self.engine = load_engine(settings_dict) self.Base = get_declarative_base(bind=self.engine) self.session_factory = sessionmaker(bind=self.engine) + + if getattr(settings, 'USE_TRANSACTIONS', False): + kw = {'scopefunc': scopefunc} + else: + kw = {} + self.sessionmaker = scoped_session(sessionmaker( - bind=self.engine, autoflush=False)) + bind=self.engine, autoflush=False), **kw) # TODO: uncomment line below once transactional tests are ready # bind=self.engine, autoflush=False), scopefunc=scopefunc) ''' diff --git a/baph/db/models/base.py b/baph/db/models/base.py index aeae760..ae0f3ee 100644 --- a/baph/db/models/base.py +++ b/baph/db/models/base.py @@ -296,7 +296,11 @@ def __new__(cls, name, bases, attrs): return super_new(cls, name, bases, attrs) module = attrs.pop('__module__') - new_class = super_new(cls, name, bases, {'__module__': module}) + new_attrs = {'__module__': module} + classcell = attrs.pop('__classcell__', None) + if classcell is not None: + new_attrs['__classcell__'] = classcell + new_class = super_new(cls, name, bases, new_attrs) # check the class registry to see if we created this already if name in new_class._decl_class_registry: @@ -350,23 +354,6 @@ def __new__(cls, name, bases, attrs): remove_class(b, name) return model - if attrs.get('__tablename__') and not attrs.get('__abstract__', None): - # build the table_args for the current model - # print('[%s]' % name) - base_args = getattr(settings, 'BAPH_DEFAULT_TABLE_ARGS', ()) - _, kwargs = normalize_args(base_args) - for p in reversed(parents): - if not hasattr(p, '__table_args__'): - continue - _, _kwargs = normalize_args(p.__table_args__) - kwargs.update(_kwargs) - table_args = attrs.pop('__table_args__', None) - # print(' old:', table_args) - args, _kwargs = normalize_args(table_args) - kwargs.update(_kwargs) - attrs['__table_args__'] = args + (kwargs,) - # print(' new:', attrs['__table_args__']) - # Add all attributes to the class. for obj_name, obj in attrs.items(): new_class.add_to_class(obj_name, obj) @@ -374,6 +361,17 @@ def __new__(cls, name, bases, attrs): if attrs.get('__abstract__', None): return new_class + table = getattr(new_class, '__table__', None) + tablename = getattr(table, 'name', None) + if tablename != getattr(new_class, '__tablename__', None): + base_table_args = getattr(settings, 'BAPH_DEFAULT_TABLE_ARGS', ()) + args, kwargs = normalize_args(base_table_args) + table_args = getattr(new_class, '__table_args__', None) + _args, _kwargs = normalize_args(table_args) + args = args + _args + kwargs.update(_kwargs) + new_class.__table_args__ = args + (kwargs,) + signals.class_prepared.send(sender=new_class) register_models(new_class._meta.app_label, new_class) return get_model(new_class._meta.app_label, name, diff --git a/baph/db/models/fields.py b/baph/db/models/fields.py index 0423696..bb09863 100644 --- a/baph/db/models/fields.py +++ b/baph/db/models/fields.py @@ -81,6 +81,21 @@ def __init__(self, verbose_name=None, name=None, primary_key=False, self.creation_counter = Field.creation_counter Field.creation_counter += 1 + def __eq__(self, other): + # Needed for @total_ordering + if isinstance(other, Field): + return self.creation_counter == other.creation_counter + return NotImplemented + + def __lt__(self, other): + # This is needed because bisect does not take a comparison function. + if isinstance(other, Field): + return self.creation_counter < other.creation_counter + return NotImplemented + + def __hash__(self): + return hash(self.creation_counter) + @property def unique(self): return self._unique or self.primary_key @@ -154,7 +169,7 @@ def formfield(self, form_class=None, **kwargs): if form_class is None: form_class = fields.NullCharField field = form_class(**defaults) - field.validators = map(self.modify_validator, field.validators) + field.validators = list(map(self.modify_validator, field.validators)) return field def clean(self, value): diff --git a/baph/db/models/mixins.py b/baph/db/models/mixins.py index 3224d24..2538d22 100644 --- a/baph/db/models/mixins.py +++ b/baph/db/models/mixins.py @@ -693,7 +693,12 @@ def kill_cache(self, force=False): if not v: cache.set(key, int(time.time())) else: - cache.incr(key) + try: + # attempt an increment of the key value + cache.incr(key) + except ValueError: + # if the key no longer exists, set a new key + cache.set(key, int(time.time())) class ModelPermissionMixin(object): @@ -744,7 +749,7 @@ def get_fks(cls, include_parents=True, remote_key=None): if new_key in pairs: value = pairs[new_key] else: - primary_key, value = pairs.items()[0] + primary_key, value = list(pairs.items())[0] keys.append( (limiter, primary_key, value, col_key, cls_name) ) if not include_parents: diff --git a/baph/db/models/options.py b/baph/db/models/options.py index a5b36e2..e75123a 100644 --- a/baph/db/models/options.py +++ b/baph/db/models/options.py @@ -6,6 +6,7 @@ from django.utils.functional import cached_property from django.utils.translation import (string_concat, get_language, activate, deactivate_all) +import six from sqlalchemy import inspect, Integer from sqlalchemy.orm import configure_mappers from sqlalchemy.ext.hybrid import HYBRID_PROPERTY, HYBRID_METHOD @@ -192,8 +193,8 @@ def contribute_to_class(self, cls, name): base_model_name = base._meta.base_model_name base_model_name_plural = base._meta.base_model_name_plural break - self.base_model_name = unicode(base_model_name) - self.base_model_name_plural = unicode(base_model_name_plural) + self.base_model_name = six.text_type(base_model_name) + self.base_model_name_plural = six.text_type(base_model_name_plural) del self.meta diff --git a/baph/db/models/properties.py b/baph/db/models/properties.py index 71e9c6f..96deece 100644 --- a/baph/db/models/properties.py +++ b/baph/db/models/properties.py @@ -1,6 +1,7 @@ import itertools from django.template.defaultfilters import slugify +import six from sqlalchemy import * from sqlalchemy import inspect, func @@ -19,7 +20,7 @@ def __init__(self, *args, **kwargs): self.populate_from = kwargs.pop('populate_from', None) self.index_sep = kwargs.pop('sep', '-') self.unique_with = kwargs.pop('unique_with', ()) - if isinstance(self.unique_with, basestring): + if isinstance(self.unique_with, six.string_types): self.unique_with = (self.unique_with,) self.slugify = kwargs.pop('slugify', slugify) diff --git a/baph/db/types.py b/baph/db/types.py index b07436e..e797a27 100644 --- a/baph/db/types.py +++ b/baph/db/types.py @@ -82,7 +82,7 @@ def process_bind_param(self, value, dialect): if value is None: return None #return json.dumps(value) - return unicode(json.dumps(value)) + return json.dumps(value) def process_result_value(self, value, dialect): if not value: diff --git a/baph/decorators/__init__.py b/baph/decorators/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/baph/decorators/db.py b/baph/decorators/db.py deleted file mode 100644 index 7febf48..0000000 --- a/baph/decorators/db.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -'''\ -:mod:`baph.decorators.db` -- Database-related Decorators -======================================================== - -.. moduleauthor:: Mark Lee -''' - -from baph.db.orm import ORM -from functools import wraps - -orm = ORM.get() - - -def sqlalchemy_session(f): - '''Decorator that automatically attaches a SQLAlchemy session to a - function. - ''' - - @wraps(f) - def _handler(*args, **kwargs): - if not kwargs.get('session'): - kwargs['session'] = orm.sessionmaker() - try: - return f(*args, **kwargs) - except Exception: - kwargs['session'].rollback() - raise - - return _handler diff --git a/baph/decorators/json.py b/baph/decorators/json.py deleted file mode 100644 index cd46d55..0000000 --- a/baph/decorators/json.py +++ /dev/null @@ -1,134 +0,0 @@ -# -*- coding: utf-8 -*- -'''\ -:mod:`baph.decorators.json` -- JSON-related decorators -====================================================== - -.. moduleauthor:: Mark Lee -.. moduleauthor:: JT Thibault -''' - -from __future__ import absolute_import - -from baph.utils.importing import import_any_module, import_attr -render_to_response = import_attr(['coffin.shortcuts'], 'render_to_response') -from django.http import ( - HttpResponse, HttpResponseRedirect, HttpResponseForbidden) -RequestContext = import_attr(['django.template'], 'RequestContext') -from functools import wraps -json = import_any_module(['json', 'simplejson', 'django.utils.simplejson']) - - -def data_to_json_response(data, **kwargs): - '''Takes any input and converts it to JSON, wraps it in an - :class:`~django.http.HttpResponse`, and sets the proper MIME type. - - :param data: The data to be serialized. - :param \*\*kwargs: extra keyword parameters given to :func:`json.dumps`. - :rtype: :class:`~django.http.HttpResponse` - ''' - return HttpResponse(json.dumps(data, **kwargs), - mimetype='application/json') - - -def render_to_json(func, **json_kwargs): - '''A decorator that takes the return value of the given function and - converts it to JSON, wraps it in an :class:`~django.http.HttpResponse`, - and sets the proper MIME type. - ''' - - @wraps(func) - def handler(request, *args, **kwargs): - '''Creates the wrapped function/method.''' - return data_to_json_response(func(request, *args, **kwargs), - **json_kwargs) - - return handler - - -class JSONify(object): - '''Generic decorator that uses Django's - :meth:`~django.http.HttpRequest.is_ajax` request method to return either: - - * A JSON dictionary containing a ``content`` key with the rendered data, - embedded in an :class:`~django.http.HttpResponse` object. - * An :class:`~django.http.HttpResponse`, using a template which wraps the - data in a given HTML file. - - :param alternate_renderer: An alternate function which renders the content - into HTML and wraps it in an - :class:`~django.http.HttpResponse`. - Alternatively, if you specify a template name, - a default Jinja2-based renderer is used. - :type alternate_renderer: :func:`callable` or :class:`str` - :param method: Whether or not the wrapped function is actually a method. - :type method: :class:`bool` - :param \*\*kwargs: extra keyword parameters given to :func:`json.dumps`. - ''' - - def __init__(self, alternate_renderer=None, method=False, **kwargs): - if alternate_renderer is None: - self.renderer = self.render - elif isinstance(alternate_renderer, basestring): - # assume Jinja2 template - self.renderer = self.render_jinja - self.template = alternate_renderer - else: - self.renderer = alternate_renderer - self.method = method - self.json_kwargs = kwargs - - def __call__(self, func): - '''Creates the wrapped function/method.''' - - @wraps(func) - def func_handler(request, *args, **kwargs): - data = func(request, *args, **kwargs) - return self._handler(data, request) - - @wraps(func) - def method_handler(method_self, request, *args, **kwargs): - data = func(method_self, request, *args, **kwargs) - return self._handler(data, request) - - if self.method: - return method_handler - else: - return func_handler - - def _handler(self, data, request): - if isinstance(data, basestring): - data = { - 'content': data, - } - elif isinstance(data, (HttpResponseRedirect, HttpResponseForbidden)): - return data - if not (isinstance(data, dict) and 'content' in data): - raise ValueError('''\ -Your view needs to return a string, a dict with a "content" key, or a 301/403 -HttpResponse object.''') - - if request.is_ajax(): - return data_to_json_response(data, **self.json_kwargs) - else: - return self.renderer(request, data) - - def render(self, request, data): - '''The default renderer for the HTML content. - - :type request: :class:`~django.http.HttpRequest` - :param data: The data returned from the wrapped function. - :type data: :class:`dict` (must have a ``content`` key) - :rtype: :class:`~django.http.HttpResponse` - ''' - return HttpResponse(data['content']) - - def render_jinja(self, request, data): - '''The default Jinja2 renderer. - - :type request: :class:`~django.http.HttpRequest` - :param data: The data returned from the wrapped function. - :type data: :class:`dict` - :rtype: :class:`~django.http.HttpResponse` - ''' - return render_to_response(self.template, data, - context_instance=RequestContext(request)) diff --git a/baph/forms/fields.py b/baph/forms/fields.py index 465ead8..69b2ae2 100644 --- a/baph/forms/fields.py +++ b/baph/forms/fields.py @@ -5,6 +5,7 @@ from django.core import validators from django.core.exceptions import ImproperlyConfigured from django.utils.translation import ugettext_lazy as _ +import six from baph.utils.collections import duck_type_collection @@ -108,7 +109,7 @@ def __init__(self, *args, **kwargs): def _as_string(self, value): if isinstance(value, basestring): return value - return unicode(value) + return six.text_type(value) def _get_content_length(self, value): """ diff --git a/baph/forms/forms.py b/baph/forms/forms.py index 8c3e4fb..ea661f3 100644 --- a/baph/forms/forms.py +++ b/baph/forms/forms.py @@ -2,6 +2,7 @@ from django.core.exceptions import ImproperlyConfigured from django.utils.datastructures import SortedDict from django.utils.translation import ugettext_lazy as _ +import six from sqlalchemy import * from sqlalchemy import inspect from sqlalchemy.ext.associationproxy import AssociationProxy @@ -267,9 +268,8 @@ def save(self, commit=False): return save_instance(self, self.instance, self._meta.fields, fail_message, commit, self._meta.exclude) -class SQLAModelForm(BaseSQLAModelForm): - __metaclass__ = SQLAModelFormMetaclass +class SQLAModelForm(six.with_metaclass(SQLAModelFormMetaclass, BaseSQLAModelForm)): def clean_unique_field(self, key, **kwargs): orm = ORM.get() value = self.cleaned_data[key] diff --git a/baph/localflavor/generic/forms.py b/baph/localflavor/generic/forms.py index 9e1696c..b11c5de 100644 --- a/baph/localflavor/generic/forms.py +++ b/baph/localflavor/generic/forms.py @@ -17,7 +17,7 @@ from django import forms from django.conf import settings -from django.utils.encoding import force_unicode +from django.utils.encoding import force_text from django.utils.html import escape, conditional_escape from django.utils.importlib import import_module from django.utils.translation import ugettext_lazy as _ @@ -112,16 +112,16 @@ def render_options(self, choices, selected_choices): ''' def render_option(country, value, label): - value = force_unicode(value) + value = force_text(value) if value in selected_choices: selected_html = u' selected="selected"' else: selected_html = u'' return u'' % ( country.lower(), escape(value), selected_html, - conditional_escape(force_unicode(label))) + conditional_escape(force_text(label))) # Normalize to strings. - selected_choices = set([force_unicode(v) for v in selected_choices]) + selected_choices = set([force_text(v) for v in selected_choices]) output = [] for country, options in chain(self.choices, choices): for value, label in options: diff --git a/baph/middleware/orm.py b/baph/middleware/orm.py index d6b35bd..3f3ed47 100755 --- a/baph/middleware/orm.py +++ b/baph/middleware/orm.py @@ -21,7 +21,10 @@ def process_request(self, request): def process_response(self, request, response): if hasattr(request, 'orm'): session = request.orm.sessionmaker() - session.close() + if response.status_code >= 400: + session.expunge_all() + + session.flush() return response def process_exception(self, request, exception): diff --git a/baph/template/__init__.py b/baph/template/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/baph/template/ext/__init__.py b/baph/template/ext/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/baph/template/shortcuts.py b/baph/template/shortcuts.py deleted file mode 100644 index 12aa9f9..0000000 --- a/baph/template/shortcuts.py +++ /dev/null @@ -1,46 +0,0 @@ -# -*- coding: utf-8 -*- - -from baph.utils.importing import import_attr -base_render_to_response = import_attr(['coffin.shortcuts'], - ['render_to_response']) -RequestContext = import_attr(['coffin.template'], 'RequestContext') -get_template, select_template = \ - import_attr(['coffin.template.loader'], - ['get_template', 'select_template']) - -__all__ = ['render_to_string', 'render_to_response'] - - -def render_to_response(template_name, dictionary=None, request=None, - mimetype=None): - '''Render a template into a response object. Meant to be compatible with - the function in :mod:`djangojinja2` of the same name, distributed with - Jinja2, as opposed to the shortcut from Django. For that, see - :func:`coffin.shortcuts.render_to_response`. - ''' - request_context = RequestContext(request) if request else None - return base_render_to_response(template_name, dictionary=dictionary, - context_instance=request_context, - mimetype=mimetype) - - -def render_to_string(template_or_template_name, dictionary=None, request=None): - '''Render a template into a string. Meant to be compatible with the - function in :mod:`djangojinja2` of the same name, distributed with Jinja2, - as opposed to the shortcut from Django. For that, see - :func:`coffin.shortcuts.render_to_string`. - ''' - dictionary = dictionary or {} - request_context = RequestContext(request) if request else None - if isinstance(template_or_template_name, (list, tuple)): - template = select_template(template_or_template_name) - elif isinstance(template_or_template_name, basestring): - template = get_template(template_or_template_name) - else: - # assume it's a template - template = template_or_template_name - if request_context: - request_context.update(dictionary) - else: - request_context = dictionary - return template.render(request_context) diff --git a/baph/test/mixins/query_logging.py b/baph/test/mixins/query_logging.py index d107d79..cd8eabe 100644 --- a/baph/test/mixins/query_logging.py +++ b/baph/test/mixins/query_logging.py @@ -116,7 +116,7 @@ def callback(self, conn, cursor, stmt, params, context, executemany): info = self.process_stack() self.queries.append((stmt, params, info)) if self.emit: - print '\n[QUERY]:', self.queries[-1] + print('\n[QUERY]:', self.queries[-1]) @property def count(self): diff --git a/baph/test/simple.py b/baph/test/simple.py index b20664a..ad56ae4 100644 --- a/baph/test/simple.py +++ b/baph/test/simple.py @@ -1,3 +1,4 @@ +from __future__ import print_function from copy import deepcopy import sys import unittest as real_unittest @@ -230,7 +231,7 @@ def setup_databases(self, **kwargs): conflicts = schemas.intersection(existing_schemas) if conflicts: for c in conflicts: - print 'drop schema %s;' % c + print('drop schema %s;' % c) sys.exit('The following schemas are already present: %s. ' \ 'TestRunner cannot proceeed' % ','.join(conflicts)) diff --git a/baph/test/testcases.py b/baph/test/testcases.py index e2bedad..fe9c94f 100644 --- a/baph/test/testcases.py +++ b/baph/test/testcases.py @@ -1,13 +1,14 @@ +from __future__ import absolute_import +from __future__ import print_function from collections import defaultdict -import json -import time +import timeit from django.conf import settings from django.core.cache import get_cache from django.db import DEFAULT_DB_ALIAS, connections, transaction from django import test from django.test.testcases import connections_support_transactions -from sqlalchemy import create_engine +from sqlalchemy import create_engine, event from sqlalchemy.orm.session import Session from sqlalchemy.orm import sessionmaker @@ -16,200 +17,78 @@ from .signals import add_timing +if not hasattr(test.SimpleTestCase, 'assertItemsEqual'): + test.SimpleTestCase.assertItemsEqual = test.SimpleTestCase.assertCountEqual + +from contextlib import contextmanager + + +@contextmanager +def timer(key): + start = timeit.default_timer() + try: + yield + finally: + elapsed = timeit.default_timer() - start + add_timing.send(None, key=key, time=elapsed) + +use_transactions = getattr(settings, 'USE_TRANSACTIONS', False) + PRINT_TEST_TIMINGS = getattr(settings, 'PRINT_TEST_TIMINGS', False) #Session = sessionmaker() orm = ORM.get() -class BaphFixtureMixin(object): - reset_sequences = False +def db_debug(msg): + "SELECTs a debug message so it appears in the db log" + session = orm.sessionmaker() + session.execute("/* %s */" % msg) + + +class TransactionTestCase(test.TransactionTestCase): test_start_time = None test_end_time = None tests_run = 0 timings = None - ''' - @classmethod - def _databases_names(cls, include_mirrors=True): - # If the test case has a multi_db=True flag, act on all databases, - # including mirrors or not. Otherwise, just on the default DB. - if getattr(cls, 'multi_db', False): - return [alias for alias in connections - if include_mirrors or not connections[alias].settings_dict['TEST']['MIRROR']] - else: - return [DEFAULT_DB_ALIAS] - - @classmethod - def _enter_atomics(cls): - """Helper method to open atomic blocks for multiple databases""" - print ' _enter atomics', cls - atomics = {} - for db_name in cls._databases_names(): - atomics[db_name] = transaction.atomic(using=db_name) - atomics[db_name].__enter__() - return atomics - - @classmethod - def _rollback_atomics(cls, atomics): - """Rollback atomic blocks opened through the previous method""" - print ' _rollback atomics', cls - for db_name in reversed(cls._databases_names()): - transaction.set_rollback(True, using=db_name) - atomics[db_name].__exit__(None, None, None) - - def _should_reload_connections(self): - return False - - def _pre_setup(self): - print '\npre setup', connections['default'].connection - """Performs any pre-test setup. This includes: - * If the class has an 'available_apps' attribute, restricting the app - registry to these applications, then firing post_migrate -- it must - run with the correct set of applications for the test case. - * If the class has a 'fixtures' attribute, installing these fixtures. - """ - #print 'pre super setup' - #super(BaphFixtureMixin, self)._pre_setup() - #print 'post super setup' - if self.available_apps is not None: - apps.set_available_apps(self.available_apps) - setting_changed.send(sender=settings._wrapped.__class__, - setting='INSTALLED_APPS', - value=self.available_apps, - enter=True) - for db_name in self._databases_names(include_mirrors=False): - emit_post_migrate_signal(verbosity=0, interactive=False, db=db_name) - try: - self._fixture_setup() - except Exception: - if self.available_apps is not None: - apps.unset_available_apps() - setting_changed.send(sender=settings._wrapped.__class__, - setting='INSTALLED_APPS', - value=settings.INSTALLED_APPS, - enter=False) - - raise - - def _post_teardown(self): - """Performs any post-test things. This includes: - * Flushing the contents of the database, to leave a clean slate. If - the class has an 'available_apps' attribute, post_migrate isn't fired. - * Force-closing the connection, so the next test gets a clean cursor. - """ - print '\npost teardown', connections['default'].connection - try: - self._fixture_teardown() - #super(BaphFixtureMixin, self)._post_teardown() - if self._should_reload_connections(): - # Some DB cursors include SQL statements as part of cursor - # creation. If you have a test that does a rollback, the effect - # of these statements is lost, which can affect the operation of - # tests (e.g., losing a timezone setting causing objects to be - # created with the wrong time). To make sure this doesn't - # happen, get a clean connection at the start of every test. - for conn in connections.all(): - print 'closing:', conn - conn.close() - finally: - if self.available_apps is not None: - apps.unset_available_apps() - setting_changed.send(sender=settings._wrapped.__class__, - setting='INSTALLED_APPS', - value=settings.INSTALLED_APPS, - enter=False) - @classmethod def setUpClass(cls): - print 'setupclass start', cls - #super(BaphFixtureMixin, cls).setUpClass() - cls.test_start_time = time.time() - cls.timings = defaultdict(list) + #print('BaphTTest.setupClass start') + super(TransactionTestCase, cls).setUpClass() + cls.session = orm.sessionmaker() if PRINT_TEST_TIMINGS: + cls.timings = defaultdict(list) + cls.test_start_time = timeit.default_timer() add_timing.connect(cls.add_timing) - - if not connections_support_transactions(): - return - print ' enter atomics' - cls.cls_atomics = cls._enter_atomics() - print ' enter atomics done' - - if cls.fixtures: - for db_name in cls._databases_names(include_mirrors=False): - print 'loaddata start', db_name - try: - call_command('loaddata', *cls.fixtures, **{ - 'verbosity': 0, - 'commit': False, - 'database': db_name, - }) - print 'loaddata end', db_name - except Exception as e: - print 'loaddata failed', db_name - cls._rollback_atomics(cls.cls_atomics) - raise - - try: - cls.setUpTestData() - except Exception: - cls._rollback_atomics(cls.cls_atomics) - raise + #print('BaphTTest.setupClass end') @classmethod def tearDownClass(cls): - print 'teardownclass', cls - if connections_support_transactions(): - cls._rollback_atomics(cls.cls_atomics) - for conn in connections.all(): - conn.close() - #super(BaphFixtureMixin, cls).tearDownClass() + #print('BaphTTest.teardownClass start') + cls.session.close() if PRINT_TEST_TIMINGS: add_timing.disconnect(cls.add_timing) - cls.test_end_time = time.time() - if PRINT_TEST_TIMINGS: + cls.test_end_time = timeit.default_timer() cls.print_timings() + super(TransactionTestCase, cls).tearDownClass() + #print('BaphTTest.teardownClass end') - @classmethod - def setUpTestData(cls): - """Load initial data for the TestCase""" - pass - - def _fixture_setup(self): - if not connections_support_transactions(): - self.setUpTestData() - if hasattr(self, 'fixtures'): - self.load_fixtures(*self.fixtures) - #return super(BaphFixtureMixin, self)._fixture_setup() - - assert not self.reset_sequences, \ - 'reset_sequences cannot be used on TestCase instances' - self.atomics = self._enter_atomics() - #if hasattr(self, 'fixtures'): - # self.load_fixtures(*self.fixtures) - - def _fixture_teardown(self): - if not connections_support_transactions(): - if hasattr(self, 'fixtures'): - self.purge_fixtures(*self.fixtures) - #return super(BaphFixtureMixin, self)._fixture_teardown() - self._rollback_atomics(self.atomics) - #if hasattr(self, 'fixtures'): - # self.purge_fixtures(*self.fixtures) - ''' + def run(self, *args, **kwargs): + #print('\nBaphTest.run:', self) + #db_debug(str(self)) + type(self).tests_run += 1 + super(TransactionTestCase, self).run(*args, **kwargs) + #print('BaphTest.run end:\n') @classmethod def add_timing(cls, sender, key, time, **kwargs): cls.timings[key].append(time) - def run(self, *args, **kwargs): - type(self).tests_run += 1 - super(BaphFixtureMixin, self).run(*args, **kwargs) - @classmethod def print_timings(cls): total = cls.test_end_time - cls.test_start_time - print '\n%s timings:' % cls.__name__ - print ' %d test(s) run, totalling %.03fs' % (cls.tests_run, total) + print('\n%s timings:' % cls.__name__) + print(' %d test(s) run, totalling %.03fs' % (cls.tests_run, total)) if not cls.timings: return items = sorted(cls.timings.items()) @@ -222,20 +101,19 @@ def print_timings(cls): keys[i] = ' %s' % end max_key_len = max(len(k) for k in keys) for i, (k, v) in enumerate(items): - print ' %s: %d calls, totalling %.03fs (%.02f%%)' % ( - keys[i].ljust(max_key_len), len(v), sum(v), 100.0*sum(v)/total) + print(' %s: %d calls, totalling %.03fs (%.02f%%)' % ( + keys[i].ljust(max_key_len), len(v), sum(v), 100.0*sum(v)/total)) @classmethod def load_fixtures(cls, *fixtures): params = { 'verbosity': 0, 'database': None, - #'skip_checks': True, } - start = time.time() - call_command('loaddata', *fixtures, **params) - if PRINT_TEST_TIMINGS: - add_timing.send(None, key='loaddata', time=time.time()-start) + with timer('loaddata'): + call_command('loaddata', *fixtures, **params) + if not use_transactions: + cls.session.commit() @classmethod def purge_fixtures(cls, *fixtures): @@ -243,66 +121,21 @@ def purge_fixtures(cls, *fixtures): 'verbosity': 0, 'interactive': False, } - start = time.time() - call_command('flush', **params) - if PRINT_TEST_TIMINGS: - add_timing.send(None, key='flush', time=time.time()-start) - - @classmethod - def setUpClass(cls): - cls.test_start_time = time.time() - cls.timings = defaultdict(list) - super(BaphFixtureMixin, cls).setUpClass() - if PRINT_TEST_TIMINGS: - add_timing.connect(cls.add_timing) - cls.session = orm.session_factory() - if hasattr(cls, 'persistent_fixtures'): - cls.load_fixtures(*cls.persistent_fixtures) - ''' - cls.connection = orm.engine.connect() - cls.session = Session(bind=cls.connection, autoflush=False) - orm.sessionmaker.registry.set(cls.session) - cls.savepoint = cls.connection.begin() - if hasattr(cls, 'fixtures'): - cls.load_fixtures(*cls.fixtures) - ''' + with timer('flush'): + call_command('flush', **params) + if not use_transactions: + cls.session.flush() + cls.session.close() - @classmethod - def tearDownClass(cls): - super(BaphFixtureMixin, cls).tearDownClass() - cls.session.close() - ''' - cls.savepoint.rollback() - cls.connection.close() - ''' - if hasattr(cls, 'persistent_fixtures'): - cls.purge_fixtures(*cls.persistent_fixtures) - if PRINT_TEST_TIMINGS: - add_timing.disconnect(cls.add_timing) - cls.test_end_time = time.time() - if PRINT_TEST_TIMINGS: - cls.print_timings() - ''' - def setUp(self): - super(BaphFixtureMixin, self).setUp() - self.savepoint2 = self.connection.begin_nested() - self.savepoint3 = self.connection.begin_nested() - self.backup_bind = orm.session_factory.kw['bind'] - orm.session_factory.configure(bind=self.connection) - - def tearDown(self): - orm.session_factory.configure(bind=self.backup_bind) - self.savepoint2.rollback() - self.session.close() - super(BaphFixtureMixin, self).tearDown() - ''' def _fixture_setup(self): - if hasattr(self, 'fixtures'): - self.load_fixtures(*self.fixtures) + fixtures = getattr(self, 'persistent_fixtures', getattr(self, 'fixtures', None)) + if fixtures: + self.load_fixtures(*fixtures) def _fixture_teardown(self): - if hasattr(self, 'fixtures'): - self.purge_fixtures(*self.fixtures) + fixtures = getattr(self, 'persistent_fixtures', getattr(self, 'fixtures', None)) + if fixtures: + self.purge_fixtures() def assertItemsOrderedBy(self, items, field): if not items: @@ -327,16 +160,113 @@ def assertItemsReverseOrderedBy(self, items, field): self.assertEqual(items, ordered) +class TestCase(TransactionTestCase): + @classmethod + def setUpClass(cls): + #print('BaphTest.setupClass start') + super(TestCase, cls).setUpClass() + if not use_transactions: + return + + cls.done = False + + #print(' outer trans begin') + cls.outer = cls.session.begin_nested() + #print(' outer trans:', cls.outer._state) + + fixtures = getattr(cls, 'persistent_fixtures', getattr(cls, 'fixtures', None)) + + if fixtures: + try: + cls.load_fixtures(*fixtures) + except Exception: + cls.outer.rollback() + raise + try: + cls.setUpTestData() + except Exception: + cls.outer.rollback() + raise + + cls.inner = cls.session.begin_nested() + cls.inner2 = cls.session.begin_nested() + + def restart_transaction(session, trans): + #print('restart trans:', id(trans)) + #print(id(cls.outer), id(cls.inner), id(cls.inner2)) + #print(cls.outer.is_active, cls.inner.is_active, cls.inner2.is_active) + + if trans is cls.outer: + assert False + if not cls.inner.is_active: + cls.inner = cls.session.begin_nested() + cls.inner2 = cls.session.begin_nested() + + elif not cls.inner2.is_active: + cls.inner2 = cls.session.begin_nested() + cls.session.expire_all() + + cls.event_params = (cls.session, "after_transaction_end", restart_transaction) + event.listen(*cls.event_params) + #print('BaphTest.setupClass end') + + @classmethod + def tearDownClass(cls): + #print('BaphTest.teardownClass start') + if use_transactions: + event.remove(*cls.event_params) + cls.done = True + #print(' outer trans rollback') + with timer('rollback'): + cls.outer.rollback() + super(TestCase, cls).tearDownClass() + #print('BaphTest.teardownClass end') + + @classmethod + def setUpTestData(cls): + """Load initial data for the TestCase""" + pass + + def _fixture_setup(self): + if not use_transactions: + self.setUpTestData() + return super(TestCase, self)._fixture_setup() + + def _fixture_teardown(self): + #self.session.expunge_all() + if not use_transactions: + return super(TestCase, self)._fixture_teardown() + with timer('rollback'): + #print(' inner trans rollback') + self.inner.rollback() + self.session.expunge_all() + + class MemcacheMixin(object): + def _fixture_setup(self): + # clear all caches before loading fixtures + super(MemcacheMixin, self)._fixture_setup() + for c in settings.CACHES: + cache = get_cache(c) + cache.clear() + cache.close() + del cache + + def setUp(self): + super(MemcacheMixin, self).setUp() + self.initial = {} + def populate_cache(self, asset_aliases=None): """ reads the current key/value pairs from the cache and stores it in self.initial_data, for comparison with post-test results """ self.initial = {} - self.initial[None] = {k: self.cache._cache.get(k) - for k in self.cache.get_all_keys()} + keys = list(self.cache.get_all_keys()) + self.initial[None] = {k: self.cache._cache.get(k) for k in keys} + self.initial[None] = {k: v for k, v in self.initial[None].items() if v is not None} + for alias in asset_aliases or (): cache = get_cache(alias) self.initial[alias] = {k: cache._cache.get(k) @@ -376,13 +306,13 @@ def assertCacheKeyUnchanged(self, key, version=None, cache_alias=None): def assertCacheKeyCreated(self, key, version=None, cache_alias=None): cache = get_cache(cache_alias) if cache_alias else self.cache raw_key = cache.make_key(key, version=version) - self.assertNotIn(raw_key, self._initial(cache_alias).keys()) + self.assertNotIn(raw_key, list(self._initial(cache_alias).keys())) self.assertIn(raw_key, cache.get_all_keys()) def assertCacheKeyNotCreated(self, key, version=None, cache_alias=None): cache = get_cache(cache_alias) if cache_alias else self.cache raw_key = cache.make_key(key, version=version) - self.assertNotIn(raw_key, self._initial(cache_alias).keys()) + self.assertNotIn(raw_key, list(self._initial(cache_alias).keys())) self.assertNotIn(raw_key, cache.get_all_keys()) def assertCacheKeyIncremented(self, key, version=None, cache_alias=None): @@ -398,41 +328,37 @@ def assertCacheKeyIncrementedMulti(self, key, version=None, cache_alias=None): initial_value = self._initial(cache_alias)[raw_key] current_value = cache.get(key, version=version) self.assertGreater(current_value, initial_value) - + def assertCacheKeyInvalidated(self, key, version=None, cache_alias=None): cache = get_cache(cache_alias) if cache_alias else self.cache raw_key = cache.make_key(key, version=version) current_value = cache.get(key, version=version) - self.assertIn(raw_key, self._initial(cache_alias).keys()) + self.assertIn(raw_key, list(self._initial(cache_alias).keys())) self.assertEqual(current_value, None) def assertCacheKeyNotInvalidated(self, key, version=None, cache_alias=None): cache = get_cache(cache_alias) if cache_alias else self.cache raw_key = cache.make_key(key, version=version) current_value = cache.get(key, version=version) - self.assertIn(raw_key, self._initial(cache_alias).keys()) + self.assertIn(raw_key, list(self._initial(cache_alias).keys())) self.assertNotEqual(current_value, None) def assertPointerKeyInvalidated(self, key, version=None, cache_alias=None): cache = get_cache(cache_alias) if cache_alias else self.cache raw_key = cache.make_key(key, version=version) current_value = cache.get(key, version=version) - self.assertIn(raw_key, self._initial(cache_alias).keys()) + self.assertIn(raw_key, list(self._initial(cache_alias).keys())) self.assertEqual(current_value, 0) def assertPointerKeyNotInvalidated(self, key, version=None, cache_alias=None): cache = get_cache(cache_alias) if cache_alias else self.cache raw_key = cache.make_key(key, version=version) current_value = cache.get(key, version=version) - self.assertIn(raw_key, self._initial(cache_alias).keys()) + self.assertIn(raw_key, list(self._initial(cache_alias).keys())) self.assertNotEqual(current_value, 0) -class TestCase(BaphFixtureMixin, test.TestCase): - pass - - -class LiveServerTestCase(BaphFixtureMixin, test.LiveServerTestCase): +class LiveServerTestCase(test.LiveServerTestCase, TestCase): pass @@ -445,6 +371,7 @@ def setUp(self, objs={}, counts={}): self.initial = {} super(MemcacheTestCase, self).setUp() + class MemcacheLSTestCase(MemcacheMixin, LiveServerTestCase): def _fixture_setup(self): super(MemcacheLSTestCase, self)._fixture_setup() @@ -453,5 +380,3 @@ def _fixture_setup(self): def setUp(self, objs={}, counts={}): self.initial = {} super(MemcacheLSTestCase, self).setUp() - - diff --git a/baph/utils/collections.py b/baph/utils/collections.py index 3979e67..2f17efa 100644 --- a/baph/utils/collections.py +++ b/baph/utils/collections.py @@ -49,6 +49,9 @@ def flatten(l): return ltype(l) +OrderedDefaultDict = defaultdict + +""" class OrderedDefaultDict(defaultdict, OrderedDict): '''A :class:`dict` subclass with the characteristics of both :class:`~collections.defaultdict` and :class:`~collections.OrderedDict`. @@ -56,3 +59,4 @@ class OrderedDefaultDict(defaultdict, OrderedDict): def __init__(self, default_factory, *args, **kwargs): defaultdict.__init__(self, default_factory) OrderedDict.__init__(self, *args, **kwargs) +""" diff --git a/setup.py b/setup.py index 4085a76..88d6b86 100755 --- a/setup.py +++ b/setup.py @@ -9,11 +9,11 @@ install_requires=[ 'Coffin', 'Django >= 1.5', - 'funcy', + 'funcy == 1.13', 'SQLAlchemy >= 0.9.0', 'python-dotenv == 0.7.1', - 'functools32 == 3.2.3.post2', - 'chainmap == 1.0.2', + 'functools32 == 3.2.3.post2; python_version == "2.7"', + 'chainmap == 1.0.2; python_version == "2.7"', ], include_package_data=True, package_data={