1. 程式人生 > 其它 >Flask專案釋出流程

Flask專案釋出流程

本文繼續對Flask官方教程進行學習,我就直接跳過Templates、Static Files、Blog Blueprint三小節了,因為基本不會在實際專案中用到這些技術,有時間多學習下前端才是。這篇文章把Make the Project Installable、Test Coverage、Deploy to Production這三小節彙總來學習。我覺得這是官方給出的一個Flask專案釋出流程,如下圖所示:

這跟我在大型專案中接觸到的釋出流程大同小異。尤其是對於我們測試來說,跑單元測試這個環節還是有必要好好了解一下的,幸運的是,得益於Python的簡單,理解起來會更容易些。所謂一通百通,Flask的單測懂了,其他語言的單測也通了。

專案打包

建立setup.py檔案:

from setuptools import find_packages, setup

setup(
    name='flaskr',
    version='1.0.0',
    packages=find_packages(),
    include_package_data=True,
    zip_safe=False,
    install_requires=[
        'flask',
    ],
)
  • packages指定Python包,find_packages()函式會自動查詢。
  • include_package_data表示包括其他檔案比如靜態(static)檔案和模板(templates)檔案。

如果還需要包括其他資料,那麼就建立MANIFEST.in檔案:

include flaskr/schema.sql
graft flaskr/static
graft flaskr/templates
global-exclude *.pyc

其中global-exclude排除了*.pyc檔案。

接著就可以使用pip命令安裝了:

$ pip install -e .

安裝以後就能用pip list命令檢視:

$ pip list

Package        Version   Location
-------------- --------- ----------------------------------
click          6.7
Flask          1.0
flaskr         1.0.0     /home/user/Projects/flask-tutorial
itsdangerous   0.24
Jinja2         2.10
MarkupSafe     1.0
pip            9.0.3
setuptools     39.0.1
Werkzeug       0.14.1
wheel          0.30.0

這個過程是這樣的:pip在當前目錄找到setup.py檔案,然後根據檔案描述把專案檔案打包後安裝到本地。安裝以後就能在任何位置使用flask run來啟動應用了,而不僅僅是在flask-turorial目錄下。

跑單元測試

單元測試不能保證程式沒有Bug,但卻是在開發階段保障程式碼質量的有效手段。拿我們公司舉例來說,開發提測和上線,都會把單元測試作為卡點,單測覆蓋率沒有達到45%是不能提測和上線的。

Flask專案的單元測試要用到兩個工具,一個是我們非常熟悉的pytest,還有一個是coverage,先安裝它們:

$ pip install pytest coverage

新建tests/data.sql檔案,插入一些測試資料:

INSERT INTO user (username, password)
VALUES
  ('test', 'pbkdf2:sha256:50000$TCI4GzcX$0de171a4f4dac32e3364c7ddc7c14f3e2fa61f2d17574483f7ffbb431b4acb2f'),
  ('other', 'pbkdf2:sha256:50000$kJPKsz6N$d2d4784f1b030a9761f5ccaeeaca413f27f2ecb76d6168407af962ddce849f79');

INSERT INTO post (title, body, author_id, created)
VALUES
  ('test title', 'test' || x'0a' || 'body', 1, '2018-01-01 00:00:00');

pytest的fixture相當於setup,可以做一些測試前的初始化工作,新建tests/conftest.py,編寫fixture:

import os
import tempfile

import pytest
from flaskr import create_app
from flaskr.db import get_db, init_db

with open(os.path.join(os.path.dirname(__file__), 'data.sql'), 'rb') as f:
    _data_sql = f.read().decode('utf8')


@pytest.fixture
def app():
    db_fd, db_path = tempfile.mkstemp()

    app = create_app({
        'TESTING': True,
        'DATABASE': db_path,
    })

    with app.app_context():
        init_db()
        get_db().executescript(_data_sql)

    yield app

    os.close(db_fd)
    os.unlink(db_path)


@pytest.fixture
def client(app):
    return app.test_client()


@pytest.fixture
def runner(app):
    return app.test_cli_runner()

app

建立應用,初始化資料庫,使用的是測試配置和測試資料。

  1. tempfile.mkstemp()建立了一個臨時檔案,返回檔案描述符和檔案路徑。並且把臨時檔案路徑傳入了DATABASE,接著插入測試資料。測試結束後關閉和移除臨時檔案。

    fixture的yield前面的程式碼相當於setup,yield後面的程式碼相當於teardown。

  2. TESTING: True將Flask置為測試模式,Flask內部會進行一些調整以便於進行測試。

client

呼叫app.test_client返回一個測試客戶端,可以用這個客戶端給應用傳送請求。

runner

呼叫app.test_cli_runner()返回一個可以執行應用已註冊命令的runner。

測試一下Factory:

# tests/test_factory.py
from flaskr import create_app


def test_config():
    assert not create_app().testing
    assert create_app({'TESTING': True}).testing


def test_hello(client):
    response = client.get('/hello')
    assert response.data == b'Hello, World!'

測試一下Database:

# tests/test_db.py
import sqlite3

import pytest
from flaskr.db import get_db


def test_get_close_db(app):
    with app.app_context():
        db = get_db()
        assert db is get_db()

    with pytest.raises(sqlite3.ProgrammingError) as e:
        db.execute('SELECT 1')

    assert 'closed' in str(e.value)
    

def test_init_db_command(runner, monkeypatch):
    class Recorder(object):
        called = False

    def fake_init_db():
        Recorder.called = True

    # monkeypatch是pytest內建的一個fixture,也就是猴子補丁。
    monkeypatch.setattr('flaskr.db.init_db', fake_init_db)
    result = runner.invoke(args=['init-db'])
    assert 'Initialized' in result.output
    assert Recorder.called

測試一下Authentication:

# tests/conftest.py
class AuthActions(object):
    def __init__(self, client):
        self._client = client

    def login(self, username='test', password='test'):
        return self._client.post(
            '/auth/login',
            data={'username': username, 'password': password}
        )

    def logout(self):
        return self._client.get('/auth/logout')


# 這樣就可以使用auth.login()進行使用者登入
@pytest.fixture
def auth(client):
    return AuthActions(client)
# tests/test_auth.py
import pytest
from flask import g, session
from flaskr.db import get_db


def test_register(client, app):
    assert client.get('/auth/register').status_code == 200
    response = client.post(
        '/auth/register', data={'username': 'a', 'password': 'a'}
    )
    assert 'http://localhost/auth/login' == response.headers['Location']

    with app.app_context():
        assert get_db().execute(
            "SELECT * FROM user WHERE username = 'a'",
        ).fetchone() is not None


@pytest.mark.parametrize(('username', 'password', 'message'), (
    ('', '', b'Username is required.'),
    ('a', '', b'Password is required.'),
    ('test', 'test', b'already registered'),
))
def test_register_validate_input(client, username, password, message):
    response = client.post(
        '/auth/register',
        data={'username': username, 'password': password}
    )
    assert message in response.data
    

def test_login(client, auth):
    assert client.get('/auth/login').status_code == 200
    response = auth.login()
    assert response.headers['Location'] == 'http://localhost/'

    # 使用with後就能在上下文中訪問session
    with client:
        client.get('/')
        assert session['user_id'] == 1
        assert g.user['username'] == 'test'


@pytest.mark.parametrize(('username', 'password', 'message'), (
    ('a', 'test', b'Incorrect username.'),
    ('test', 'a', b'Incorrect password.'),
))
def test_login_validate_input(auth, username, password, message):
    response = auth.login(username, password)
    assert message in response.data
    
    
def test_logout(client, auth):
    auth.login()

    # 使用with後就能在上下文中訪問session
    with client:
        auth.logout()
        assert 'user_id' not in session

更多關於Blog的測試用例就不在此贅述了,感興趣的同學可以點選文章尾部連結到官網檢視。

最後用例寫完了,就該運行了。在setup.cfg檔案中新增一些配置,可以適當減少單測冗餘:

[tool:pytest]
testpaths = tests

[coverage:run]
branch = True
source =
    flaskr

然後就可以執行pytest了:

$ pytest

========================= test session starts ==========================
platform linux -- Python 3.6.4, pytest-3.5.0, py-1.5.3, pluggy-0.6.0
rootdir: /home/user/Projects/flask-tutorial, inifile: setup.cfg
collected 23 items

tests/test_auth.py ........                                      [ 34%]
tests/test_blog.py ............                                  [ 86%]
tests/test_db.py ..                                              [ 95%]
tests/test_factory.py ..                                         [100%]

====================== 24 passed in 0.64 seconds =======================

pytest -v可以顯示每個測試函式。

單測覆蓋率才是靈魂,所以建議這樣來跑單測:

$ coverage run -m pytest

然後檢視報告:

$ coverage report

Name                 Stmts   Miss Branch BrPart  Cover
------------------------------------------------------
flaskr/__init__.py      21      0      2      0   100%
flaskr/auth.py          54      0     22      0   100%
flaskr/blog.py          54      0     16      0   100%
flaskr/db.py            24      0      4      0   100%
------------------------------------------------------
TOTAL                  153      0     44      0   100%

也可以生成html報告:

$ coverage html

釋出上線

先安裝wheel庫:

$ pip install wheel

然後建立.whl檔案:

$ python setup.py bdist_wheel

命令執行後會生成一個dist/flaskr-1.0.0-py3-none-any.whl檔案,檔案格式是{project name}-{version}-{python tag} -{abi tag}-{platform tag}

在伺服器上就可以安裝了:

$ pip install flaskr-1.0.0-py3-none-any.whl

因為是新機器,所以需要初始化資料庫:

$ export FLASK_APP=flaskr
$ flask init-db

如果是Python虛擬環境,那麼可以在venv/var/flaskr-instance找到Flask例項。

最後設定下SECRET_KEY,Flask官網給出一種生成隨機SECRET_KEY的方法:

$ python -c 'import secrets; print(secrets.token_hex())'

'192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'

生成後新建venv/var/flaskr-instance/config.py檔案貼上即可:

SECRET_KEY = '192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf'

至於生產伺服器的選取,建議不要使用flask run,因為這是Werkzeug提供的開發伺服器,既不穩定,也不安全。

可以使用WSGI伺服器,比如Waitress:

$ pip install waitress
$ waitress-serve --call 'flaskr:create_app'

Serving on http://0.0.0.0:8080

標準的WSGI伺服器如下:

  • Gunicorn

  • uWSGI

  • Gevent,我們組就用的這個:

    from gevent.pywsgi import WSGIServer
    from yourapplication import app
    
    http_server = WSGIServer(('', 5000), app)
    http_server.serve_forever()
    
  • Twisted Web

  • Proxy Setups

參考資料:

https://flask.palletsprojects.com/en/2.0.x/tutorial/install/

https://flask.palletsprojects.com/en/2.0.x/tutorial/tests/

https://flask.palletsprojects.com/en/2.0.x/tutorial/deploy/


所有文章公眾號首發!
如果你覺得這篇文章寫的還不錯的話,關注公眾號“dongfanger”,你的支援就是我寫文章的最大動力。

版權申明:本文為博主原創文章,轉載請保留原文連結及作者。