为MAC打造舒适的Python开发环境

最近准备着手新的Django项目,想着建立新的virtualenv开发环境,再又考虑到尝试使用Python3+的可能,google了一些MAC上建立多版本Python开发环境的文章,尝试了一些新东东,和大家分享。

不要覆盖系统自带的Python版本!

MAC系统本身自带有Python库,且和系统集成度较高,为系统稳定考虑,无特殊需求不要覆盖系统自带的Python库。

若有疑问可google各种覆盖系统带来的奇葩问题。

配置栈

1. Xcode Command Line Tools

这是MAC开发上必备的工具库,提供gcc/llvm编译库及工具,没有这个是无法在MAC上编译相关程序的。如果MAC没做过开发机或新升级,请安装更新该工具库。

可在developer.apple.com登录后下载安装包安装,或在app store下载xcode安装,或运行下面命令行尝试安装

$ xcode-select --install

检查安装是否成功

$ xcode-select -p
/Applications/Xcode.app/Contents/Developer #显示此结果则说明已安装成功

或运行gcc查看工具版本

$ gcc -v
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 6.0 (clang-600.0.51) (based on LLVM 3.5svn)
Target: x86_64-apple-darwin14.1.0
Thread model: posix

特别声明,若你有靠谱VPN,下面请全程开启……

2. MAC下软件包安装管理工具brew(ruby)

MAC下软件包安装管理工具常用 MACports / brew,两者优劣如何选择请自行google判断,这里用的是brew。

由于brew是基于ruby开发,因此需要基本的ruby环境。如果你对ruby开发相关毫无兴趣,可通过Ruby官网下载包直接安装。若有可能使用ruby开发或尝试ruby环境下的各种程序请使用更规范的ruby环境配置程序——rvm

$ curl -sSL https://get.rvm.io | bash
$ source ~/.rvm/scripts/rvm

检查是否安装正确

$ rvm -v
rvm 1.26.11 (latest) by Wayne E. Seguin <wayneeseguin@gmail.com>, Michal Papis <mpapis@gmail.com> [https://rvm.io/]

安装最新的ruby版本

$ rvm install 2.2.1

设置默认版本

$ rvm 2.2.1 --default

检查下

$ ruby -v
ruby 2.2.1p85 (2015-02-26 revision 49769) [x86_64-darwin14]

安装brew

$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

3. 安装 pyenv

pyenv是管理Python版本安装管理工具

$ brew install pyenv

将下面代码加入shell启动脚本 .bash_profile / .zshrc

export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

重启shell

$ exec $SHELL

安装管理多版本Python常用操作

$ pyenv install --list  # 查看可安装版本
$ pyenv install 2.6.9
$ pyenv install 2.7.9
$ pyenv install 3.4.3
$ pyenv versions 
* system (set by /Volumes/Home/linnchord/.pyenv/version)
  2.6.9
  2.7.9
  3.4.3
$ pyenv uninstall 2.6.9 # 移除
$ pyenv global 3.4.3    # 设置为全局,覆盖系统,慎用!
$ pyenv global system   # 恢复系统版本
$ pyenv local 3.4.3     # 设置当前目录生效Python版本
$ pyenv local --unset   # 取消
$ pyenv rehash          # 安装新版本或其他软件包后需运行
$ pyenv shell 3.4.3     # 设置当前脚本生效版本

4. 安装 pyenv-virtualenv

pyenv-virtualenv 是作为 pyenv 一个插件存在的虚拟环境管理工具,它统一将虚拟环境配置到 ~/.pyenv/versions。

$ brew install pyenv-virtualenv

将下面代码加入shell启动脚本 .bash_profile / .zshrc

eval "$(pyenv virtualenv-init -)"

重启shell

$ exec $SHELL

常用管理操作

$ pyenv virtualenv 3.4.3 env343
$ pyenv virtualenv 2.7.9 venv279
$ pyenv virtualenvs                 # 显示当前虚拟环境列表
env279 (created from $HOME/.pyenv/versions/2.7.9)
env343 (created from $HOME/.pyenv/versions/3.4.3)

$ ls ~/.pyenv/versions
2.7.9  3.4.3  env343 venv279

$ pyenv activate env343             # 激活指定虚拟环境
$ pyenv deactivate                  # 关闭当前虚拟环境
$ pyenv uninstall env343            # 删除

5. 开发流程

$ pyenv virtualenv 3.4.3 newproject
$ pyenv activate newproject
$ pip install -r reuirements.txt
$ mkdir newproject
$ cd newproject

Code lucky!

uwsgi 1.9+ mountpoint error

最近团队开发新产品,顺带折腾更新了服务器和整个开发环境版本。

运行python的服务uwsgi从0.9+升级到了最新的1.9+,发现一个bug,折腾了我一个通宵……

uwsgi原有的版本,vhost模式下,可以在nginx中配置SERVER_NAME来指定服务名。


location / {
    include uwsgi_params;
    uwsgi_pass unix:/var/run/uwsgi-python/sock;
    uwsgi_param UWSGI_PYHOME /usr/local/pyenv/app;
    uwsgi_param UWSGI_CHDIR  $document_root;
    uwsgi_param UWSGI_SCRIPT app_name:app;
    uwsgi_param SERVER_NAME app_name;
}

该配置会指定服务名并在uwsgi启动日志中显示

WSGI app 0 (mountpoint='app_name')

但是在最新的uwsgi v1.9.12中,该配置失效,无法正确的配置mountpoint,日志会显示

WSGI app 0 (mountpoint='')

这会导致访问指定地址时在不同的app之间跳来跳去,无法正常使用。经测试1.9.12~1.9.7均存在同样问题,1.9.5版本以下会指定server+port替代

WSGI app 2 (mountpoint='domain_name:8080|')

我在github上提交了该bug,https://github.com/unbit/uwsgi/issues/321,作者修复之前建议大家先使用1.9.5版本。

NOTE:
作者已经修复了该bug,不过仍使用了server+port的模式,不知道是出于什么理由。目前版本已更新到1.9.13,通过pip install -U uwsgi可更新安装。

PIL MemoryError with uWSGI

用PIL库处理图片,作缩略图的时候需要先裁减为正方形,再比例缩小,当裁减时不定报错。


img = img.transform((r,r), Image.EXTENT, area)

#ERROR
File "/usr/local/lib/python2.7/dist-packages/PIL/ImageFile.py", line 231, in load_prepare
    self.im = Image.core.new(self.mode, self.size)
MemoryError

google PIL+MemoryError,主要提到2个可能错误。一个是PIL库本身ctype代码问题,一个是传递裁减范围为负值导致内存问题。仔细检查了一下,都没有异常。后来考虑到上面的代码为创建新图像时内存错误,直接调整了uwsgi参数。

-M -p 2 --no-orphans --logdate --chmod-socket=666 --uid admin --gid admin --limit-as 384 --harakiri 60 --max-requests 2000

[–limit-as]参数从256调整到384,系统恢复正常。

反思查找原因,发现图片文件虽然不大仅3M+,但是该图片像素巨大为4096*5210,将该图片转换为BMP,有60M+。估计库在处理图片裁减时会将jpg文件重新读取为完整位图,再考虑到操作位图消耗和程序本身需要的内存空间,原设定256M限制肯定不足了,需要设定更大。

目前程序运行在一个虚拟机上,内存2G……看来需要尽快切换主机了……

Flask下的session认证和cache header设置

Flask是个非常优秀的python microframework,基于Werkzeug、Jinja 2,使用非常方便,性能也非常不错,最近公司创业新项目一直在用。

这里和大家分享2个简单的decorator,分别用于session认证和响应头cache设置


# -*- coding: utf-8 -*-
"""
    lib/decorator.py
    ~~~~~~~~~~~~~~

    some decorator defined 

    :author: linnchord@gmail.com
    :date:2011-8-4

"""
from functools import wraps
from flask import session, redirect, url_for, flash, request, make_response
from models.user import User
from util import helper


def login_required(f):
    
    @wraps(f)
    def do(*args, **kwargs):
        if 'userId' not in session:
            #cookie自动登录机制
            token=request.cookies.get('auto_login')
            if token:
                userId, token = token.split('_')
                user = User.get_user_by_id(userId)
                from matrix import app
                if user and helper.md5(str(user.id) + user.password + app.config['SECURIY_KEY'])==token:
                    session['userId']=user.id
                    session['phone']=user.phone
                    session['nickName']=user.nickName
                else:
                    return redirect('/logout')
            else:
                return redirect('/')

        return f(*args, **kwargs)
            
    return do


def no_cache_header(f):

    @wraps(f)
    def do(*args, **kwargs):
        response = make_response(f(*args, **kwargs))
        response.headers['pragma'] = 'no-cache'
        response.headers['Cache-Control'] = 'no-cache, must-revalidate'
        return response
    return do

使用很简单方便


@app.route('/user/follow/', methods=['GET'])
@login_required
@no_cache_header
def follow_user(userId,appId):
    pass

需要注意的是@no_cache_header修饰应该放在最后,否则逻辑无法正确完成。这个修饰经常用于ajax响应,因为……坑爹的IE总是要缓存第一次请求结果。

这个session auto login机制有个问题,他的auto login cookie始终是不变的,长期使用会有一定安全隐患。更安全的方法是用户每次登录生成一个security key来进行hash,不过这样对于经常换浏览器甚至操作系统的人会造成一定障碍。例如我……经常使用chrome/firefox/ie,还在mac/win下切来切去,某个小说网站每次切了就得重新登录,让我很蛋疼……

Flask SessionStore by redis with msgpack serialized

参考老外代码写了个用于Flask框架的基于redis的session实现,采用msgpack进行序列化。


# -*- coding:utf-8 -*-
"""

lib/RedisSessionStore.py
~~~~~~~~~~~~~~

Flask, session store in redis

ref by https://gist.github.com/994937

:author: linnchord@gmail.com
:date:2011-08-04

"""
import redis
from flask import Flask, request, session, json
from werkzeug.contrib.sessions import Session, SessionStore
import msgpack

class RedisSessionStore(SessionStore):

    def __init__(self, key_prefix=None, host='127.0.0.1', port=6379, dbindex=1, expire=1800):
        SessionStore.__init__(self)

        self.redis = redis.Redis(host,port,dbindex)
        self.key_prefix = key_prefix
        self.expire = expire

    def save(self,session):
        key = self._get_session_key(session.sid)
        data = msgpack.Packer().pack(dict(session))
        #print "set session {0}:{1}".format(key, data)
        self.redis.setex(key, data, self.expire)

    def delete(self, session):
        key = self._get_session_key(session.sid)
        self.redis.delete(key)

    def get(self, sid):
        key = self._get_session_key(sid)
        data = self.redis.get(key)
        if data is not None:
            self.redis.setex(key, data, self.expire)
            un = msgpack.Unpacker()
            un.feed(data)
            data = un.unpack()
        else:
            data = {}

        return self.session_class(data, sid, False)


    def _get_session_key(self,sid):
        key = self.key_prefix + sid
        if isinstance(key, unicode):
          key = key.encode('utf-8')
        return key

    
    @staticmethod
    def init_app(app):
        app.session_store = RedisSessionStore(
            app.config['SESSION_KEY_PREFIX'],
            app.config['SESSION_REDIS_HOST'],
            app.config['SESSION_REDIS_PORT'],
            app.config['SESSION_REDIS_DB'],
            app.config['SESSION_LIFETIME']
        )
        app.session_key = app.config['SESSION_KEY']
    

class SessionMixin(object):

  __slots__ = ('session_key', 'session_store')

  def open_session(self, request):
    sid = request.cookies.get(self.session_key, None)
    if sid is None:
        return self.session_store.new()
    else:
        return self.session_store.get(sid)

  def save_session(self, session, response):
    if session.should_save:
        self.session_store.save(session)
        response.set_cookie(self.session_key, session.sid)
    return response


class MyFlask(SessionMixin, Flask): pass

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

HOW TO USE

app = MyFlask(__name__)
RedisSessionStore.init_app(app)

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

APP CONFIG:

SESSION_REDIS_HOST = '127.0.0.1'
SESSION_REDIS_PORT = 6379
SESSION_REDIS_DB = 1

SESSION_KEY = '_myapp_sid'
SESSION_KEY_PREFIX = 'session_myapp_'

SESSION_LIFETIME = 900