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
建立應用,初始化資料庫,使用的是測試配置和測試資料。
-
tempfile.mkstemp()
建立了一個臨時檔案,返回檔案描述符和檔案路徑。並且把臨時檔案路徑傳入了DATABASE
,接著插入測試資料。測試結束後關閉和移除臨時檔案。fixture的yield前面的程式碼相當於setup,yield後面的程式碼相當於teardown。
-
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/
所有文章公眾號首發!
如果你覺得這篇文章寫的還不錯的話,關注公眾號“dongfanger”,你的支援就是我寫文章的最大動力。
版權申明:本文為博主原創文章,轉載請保留原文連結及作者。