diff --git a/.gitignore b/.gitignore index b8e085d..7aae853 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.tox/* .DS_Store *.pyc *.egg @@ -8,4 +9,4 @@ config.py build _build -dist +dist \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 6eaab61..e82a9ea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,12 +3,16 @@ language: python python: - "2.6" - "2.7" + - "3.3" + - "3.4" + - "3.5" install: - - pip install . --quiet --use-mirrors - - "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install importlib --quiet --use-mirrors; fi" - - pip install nose simplejson Flask-SQLAlchemy Flask-MongoEngine Flask-Peewee Flask-Mail mock MySQL-python --quiet --use-mirrors - - pip install oauth2client google-api-python-client foursquare python-twitter --quiet --use-mirrors + - pip install . --quiet + - pip install setuptools nose simplejson Flask-SQLAlchemy Flask-MongoEngine Flask-Peewee Flask-Mail mock --quiet + - pip install oauth2client google-api-python-client + - pip install https://pypi.python.org/packages/source/f/foursquare/foursquare-1!2015.5.26.tar.gz#md5=cf22b486e38dab5ad584521112e405b4 + - pip install https://github.com/bear/python-twitter/archive/v3.0.zip - pip install http://github.com/pythonforfacebook/facebook-sdk/tarball/master --quiet before_script: diff --git a/README.rst b/README.rst index 7528f7f..47dd022 100644 --- a/README.rst +++ b/README.rst @@ -23,3 +23,9 @@ Resources - `Development Version `_ - `Example Application `_ + +Developers info +--------------- + +- `Easy way for local testing is Tox `_ +- `Tests required MongoDB avaliable on localhost `_ diff --git a/flask_social/__init__.py b/flask_social/__init__.py index f3400fa..477f950 100644 --- a/flask_social/__init__.py +++ b/flask_social/__init__.py @@ -10,10 +10,13 @@ :license: MIT, see LICENSE for more details. """ -__version__ = '1.6.2' +__version__ = '1.6.3.dev1' + +from future import standard_library +standard_library.install_aliases() from .core import Social from .datastore import SQLAlchemyConnectionDatastore, \ - MongoEngineConnectionDatastore, PeeweeConnectionDatastore + MongoEngineConnectionDatastore, PeeweeConnectionDatastore from .signals import connection_created, connection_failed, login_failed, \ - connection_removed, login_completed + connection_removed, login_completed diff --git a/flask_social/core.py b/flask_social/core.py index 47fc741..12ec1af 100644 --- a/flask_social/core.py +++ b/flask_social/core.py @@ -10,7 +10,11 @@ """ from importlib import import_module -from flask import current_app +from flask import current_app, _request_ctx_stack +try: + from flask import _app_ctx_stack +except ImportError: + _app_ctx_stack = None from flask_oauthlib.client import OAuthRemoteApp as BaseRemoteApp from flask.ext.security import current_user from werkzeug.local import LocalProxy @@ -18,6 +22,9 @@ from .utils import get_config, update_recursive from .views import create_blueprint +# Select stack, in flask 0.9 _app_ctx_stack is used +_ctx_stack = _app_ctx_stack or _request_ctx_stack + _security = LocalProxy(lambda: current_app.extensions['security']) _social = LocalProxy(lambda: current_app.extensions['social']) @@ -58,7 +65,7 @@ def get_api(self): consumer_secret=self.consumer_secret) -def _get_state(app, datastore, providers, **kwargs): +def create_state(app, datastore, providers, **kwargs): config = get_config(app) for key in providers.keys(): @@ -96,14 +103,23 @@ def _get_token(): return None +def get_state(app): + """Get social state of the app""" + # TODO check if right to use assert + assert 'social' in app.extensions, "The Social extension was not registered to the current application." \ + "Please make sure to call init_app() first." + return app.extensions['social'] + + class Social(object): def __init__(self, app=None, datastore=None): + self._state = None self.app = app self.datastore = datastore if app is not None and datastore is not None: - self._state = self.init_app(app, datastore) + self.init_app(app, datastore) def init_app(self, app, datastore=None): """Initialize the application with the Social extension @@ -132,12 +148,15 @@ def init_app(self, app, datastore=None): providers[config['id']] = OAuthRemoteApp(**config) providers[config['id']].tokengetter(_get_token) - state = _get_state(app, datastore, providers) - - app.register_blueprint(create_blueprint(state, __name__)) - app.extensions['social'] = state + self._state = create_state(app, datastore, providers) - return state + app.register_blueprint(create_blueprint(self._state, __name__)) + app.extensions['social'] = self._state def __getattr__(self, name): - return getattr(self._state, name, None) + if self._state is not None: # shortcut + return getattr(self._state, name, None) + ctx = _ctx_stack.top + if ctx is not None: + self._state = get_state(ctx.app) + return getattr(self._state, name) diff --git a/flask_social/providers/foursquare.py b/flask_social/providers/foursquare.py index 57a4f73..77399cf 100644 --- a/flask_social/providers/foursquare.py +++ b/flask_social/providers/foursquare.py @@ -12,7 +12,7 @@ from __future__ import absolute_import import foursquare -import urlparse +from urllib.parse import urljoin config = { 'id': 'foursquare', @@ -47,8 +47,7 @@ def get_connection_values(response, **kwargs): api = foursquare.Foursquare(access_token=access_token) user = api.users()['user'] profile_url = 'http://www.foursquare.com/user/' + user['id'] - image_url = urlparse.urljoin(user['photo']['prefix'], - user['photo']['suffix']) + image_url = urljoin(user['photo']['prefix'], user['photo']['suffix']) return dict( provider_id=config['id'], diff --git a/flask_social/utils.py b/flask_social/utils.py index fd37021..acea74f 100644 --- a/flask_social/utils.py +++ b/flask_social/utils.py @@ -48,10 +48,12 @@ def get_connection_values_from_oauth_response(provider, oauth_response): consumer_key=provider.consumer_key, consumer_secret=provider.consumer_secret) + def get_token_pair_from_oauth_response(provider, oauth_response): module = import_module(provider.module) return module.get_token_pair_from_response(oauth_response) + def get_config(app): """Conveniently get the social configuration for the specified application without the annoying 'SOCIAL_' prefix. @@ -68,7 +70,7 @@ def strip_prefix(tup): def update_recursive(d, u): - for k, v in u.iteritems(): + for k, v in u.items(): if isinstance(v, collections.Mapping): r = update_recursive(d.get(k, {}), v) d[k] = r diff --git a/setup.py b/setup.py index 1aafcc6..5deb41d 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup( name='Flask-Social', - version='1.6.2', + version='1.6.3.dev1', url='https://github.com/mattupstate/flask-social', license='MIT', author='Matthew Wright', @@ -34,7 +34,8 @@ include_package_data=True, platforms='any', install_requires=[ - 'Flask-Security>=1.6.9', + 'future>=0.15.2', + 'Flask-Security>=1.7.5', 'Flask-OAuthlib==0.5.0' ], test_suite='nose.collector', diff --git a/tests/functional_tests.py b/tests/functional_tests.py index 136ecce..b79ded0 100644 --- a/tests/functional_tests.py +++ b/tests/functional_tests.py @@ -4,6 +4,7 @@ from tests.test_app.mongoengine import create_app as create_mongo_app from tests.test_app.peewee_app import create_app as create_peewee_app + def get_mock_twitter_response(): return { 'oauth_token_secret': 'the_oauth_token_secret', @@ -26,18 +27,21 @@ def get_mock_twitter_connection_values(): 'image_url': 'https://cdn.twitter.com/something.png' } + def get_mock_twitter_token_pair(): return { 'access_token': 'the_oauth_token', 'secret': 'the_oauth_token_secret' } + def get_mock_twitter_updated_token_pair(): return { 'access_token': 'the_updated_oauth_token', 'secret': 'the_updated_oauth_token_secret' } + class SocialTest(unittest.TestCase): SOCIAL_CONFIG = None @@ -81,7 +85,8 @@ def authenticate(self, email="matt@lp.com", password="password", endpoint=None, def assertIn(self, member, container, msg=None): if hasattr(unittest.TestCase, 'assertIn'): - return unittest.TestCase.assertIn(self, member, container, msg) + # container from bytes -> str + return unittest.TestCase.assertIn(self, member, str(container), msg) return self.assertTrue(member in container) @@ -278,5 +283,6 @@ def test_remove_connection(self, class MongoEngineTwitterSocialTests(TwitterSocialTests): APP_TYPE = 'mongo' + class PeeweeTwitterSocialTests(TwitterSocialTests): APP_TYPE = 'peewee' diff --git a/tests/test_app/__init__.py b/tests/test_app/__init__.py index 9de208a..61717d2 100644 --- a/tests/test_app/__init__.py +++ b/tests/test_app/__init__.py @@ -2,8 +2,13 @@ from flask import Flask, render_template, current_app from flask.ext.security import login_required +from flask.ext.social import Social from werkzeug import url_decode + +social = Social() + + class Config(object): WTF_CSRF_ENABLED = False @@ -86,15 +91,15 @@ def index(): @app.route('/profile') @login_required def profile(): - twitter = current_app.social.twitter + twitter = social.twitter twitter.get_api() return render_template( 'profile.html', content='Profile Page', twitter_conn=twitter.get_connection(), - google_conn=current_app.social.google.get_connection(), - facebook_conn=current_app.social.facebook.get_connection(), - foursquare_conn=current_app.social.foursquare.get_connection()) + google_conn=social.google.get_connection(), + facebook_conn=social.facebook.get_connection(), + foursquare_conn=social.foursquare.get_connection()) return app diff --git a/tests/test_app/sqlalchemy.py b/tests/test_app/sqlalchemy.py index 88cac7a..a2bc489 100644 --- a/tests/test_app/sqlalchemy.py +++ b/tests/test_app/sqlalchemy.py @@ -8,10 +8,11 @@ from flask.ext.security import Security, UserMixin, RoleMixin, \ SQLAlchemyUserDatastore -from flask.ext.social import Social, SQLAlchemyConnectionDatastore +from flask.ext.social import SQLAlchemyConnectionDatastore from flask.ext.sqlalchemy import SQLAlchemy -from tests.test_app import create_app as create_base_app, populate_data +from tests.test_app import create_app as create_base_app, populate_data, \ + social def create_app(config=None, debug=True): @@ -53,7 +54,7 @@ class Connection(db.Model): rank = db.Column(db.Integer) app.security = Security(app, SQLAlchemyUserDatastore(db, User, Role)) - app.social = Social(app, SQLAlchemyConnectionDatastore(db, Connection)) + social.init_app(app, SQLAlchemyConnectionDatastore(db, Connection)) @app.before_first_request def before_first_request(): @@ -61,7 +62,6 @@ def before_first_request(): db.create_all() populate_data() - app.get_user = lambda: User.query.first() return app diff --git a/tests/test_app/templates/_nav.html b/tests/test_app/templates/_nav.html index 0aeab1d..26cfc77 100644 --- a/tests/test_app/templates/_nav.html +++ b/tests/test_app/templates/_nav.html @@ -1,11 +1,11 @@ -{%- if current_user.is_authenticated() -%} +{%- if current_user.is_authenticated -%}

Hello {{ current_user.email }}

{%- endif %}
  • Index
  • Profile
  • - {%- if current_user.is_authenticated() -%} + {%- if current_user.is_authenticated -%} Log out {%- else -%} Log in diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..06c1008 --- /dev/null +++ b/tox.ini @@ -0,0 +1,17 @@ +[tox] +envlist = py26,py27,py33,py34,py35 + +[testenv] +deps = nose + future + mock + httplib2 + Flask-SQLAlchemy + Flask-MongoEngine + Flask-Peewee + https://github.com/bear/python-twitter/archive/v3.0.zip + http://github.com/pythonforfacebook/facebook-sdk/tarball/master + foursquare + oauth2client + google-api-python-client +commands = nosetests []