-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improvements #3
Comments
臨時整理了一下,python 的放出來了 |
8pm 大師~ |
我覺得首先是目標吧,你應該有腹稿的了,確實有必要寫出來,但不一定在 README,比如 總體原則: 計劃要完成的 features:
可能有用的 features:
deployment & hosting 方案:
coding 策略與 coding style:
這些先大概定下來會好很多,而且基本上都影響 lib 的選擇和 coding 方式 README 主要是簡單說 what, how 就行了 下面分開說一下細節,想到哪寫到哪 |
首先說架構吧 考慮儘可能自動化,比如新開發者怎麼配置好環境等 至少要準備 requirements.txt and/or setup.py,requirements 甚至可以細分,比如 end user 的和開發者的,開發者的應該多了 testing 工具的依賴之類 初始數據庫等也應早考慮寫成腳本 我將那個半成品放出來目的就是覺得應該對你有參考價值,用 make, fabric, flask-script 等自動化這些過程,其中 flask-script 目前有可能被 flask 0.11 開始的功能取代,比如 flask 這個 command 和 click 寫 cli。 目的就是新用戶/開發者應該能:
馬上就能用之類的 |
然後是一些 code 的問題吧,我一個個文件按順序來看 app.py: import Lines 4 to 5 in 1b92a23
我個人覺得 \ 不好看也容易出錯,可以用括號,這樣怎麼分行都方便,如: from flask import (Flask, Response, request, session, redirect, url_for,
render_template, send_file, abort) flask app Lines 7 to 8 in 1b92a23
建議使用 app factory,方便以後測試和 deploy local config Lines 26 to 39 in 1b92a23
config 最好單獨一個 module,比如 def _ Lines 49 to 51 in 1b92a23
每個文件裡的 _,其實可以放在一個工具 module 裡面,叫 utils.py 或者 common.py 或者 tools.py 什麼的 from .utils import _ 等 i18n 設好以後,將這一行的 .utils 改成 i18n 的就行 另外,配置好 i18n 也是很簡單的,提取/更新 .po 文件也可以用 make 或 fabric 或 manage.py 自動起來 def send_mail Lines 54 to 76 in 1b92a23
Flask-Mail 可以簡化這個 def check_permission Lines 79 to 102 in 1b92a23
首先我認為這個應該屬於 auth 或 user 或 utils 甚至 db (其實我認為 models 可能更合適) 的 module 里,app.py 應該只有關於 app 創建等的內容 然後,如果這個函數都在 view function 里用,可以直接 abort(401),401/404 的界面可以做得很 fancy 的。 def json_response Lines 105 to 114 in 1b92a23
http://flask.pocoo.org/docs/0.11/api/#flask.json.jsonify error message handling Lines 117 to 147 in 1b92a23
還沒看到怎麼用的,直覺感覺沒必要,one-liner made config public to templates Lines 150 to 152 in 1b92a23
還沒看到其他文件,不過一般其他 module, template 知道的越少越好,不會有意外的依賴,so,先看下去 smart date repr. Lines 155 to 202 in 1b92a23
第一反映是這個不需要自己寫的,比如 dev 版本的 babel 有: flask_babel.format_timedelta 然後是這個應該放在 utils.py 之類的 所以如果我要使用 smart date 的,一般用 front-end 手段,如 Moment.js,有 extension 方便使用: view index Lines 205 to 207 in 1b92a23
either redirect or: @app.route('/')
@app.route('/board/<name>')
def board(name=''):
.... view board Lines 210 to 242 in 1b92a23
看到這裡,我發現我犯了個錯誤,光這個文件就 1300+ loc,看來我不能這樣搞 :(轉下一貼 |
現在開始就粗一點說 鼠標亂飄 :) 看到什麼說什麼自己寫 pagination,其實 peewee 有 flask extension,http://pypi.python.org/pypi/Flask-Peewee 應該盡量避免用 magic number,特別是兩層或以上的 collection 嵌套,解決方法我會這用兩種
多利用 python 的 first-class function 這個特點,比如這裡 Lines 34 to 35 in 1b92a23
instead of def now():
return datetime.datetime.now() 可以這樣 now = datetime.datetime.now 沒必要多一層 if len(lst) == 0:
pass
# 直接用
if not lst:
pass 等等 User handling,用現有的 extension 可以省很多代碼,而且和其他 extension 和 flask 本身整合更好 最基礎的 login/logout 有 Flask-Login http://pypi.python.org/pypi/Flask-Login Sending email encryption 等 from werkzeug.security import check_password_hash, generate_password_hash Uploading User input validation, form handling 永遠不要信任用戶輸入,存入時應該 sanitize,去掉所有 html tags,特別是 script 和 iframe,escape sequence,處理麻煩的 unicode entity,目前的代碼就沒做好 一個相關的內容是,我強烈建議給用戶輸入 Markdown,並且支持 code highlighting,再 sanitize 掉有害的內容(Markdown 支持直接 html),最後再轉換 @ 的連接,除了最後一步,具體怎麼做可參考我那個 forum。 即使自己寫轉換,也很多地方可以精簡的,比如: Lines 66 to 113 in 1b92a23
這種情況,現在是寫成 imperative 的 FSM 還不如寫成 functional 的 data pipeline 可以隨意搭配 每步處理多的用 generator 也不特別耗費資源 e.g # in pipeline.py
def normalize_newline(text):
return '\n'.join(text.splitlines())
BEGIN_CODE = '/***#'
END_CODE = '#***/'
def normalize_space(text):
in_code_block = 0
for line in text:
if line.strip().startswith(BEGIN_CODE):
in_code_block += 1
if not in_code_block:
yield ' '.join(line.split())
else:
yield line
if line.strip().endswith(END_CODE):
in_code_block -= 1
def other_transformation(text):
for line in text:
# blah blah blah
yield line
def pipeline(*processors):
def process(text):
for proc = processors:
text = proc(text)
return text
return process
# in forum.py
from .pipeline import (
normalize_newline,
normalize_space,
other_transformation,
pipeline,
)
process_content = pipeline(
normalize_newline,
normalize_space,
other_transformation,
)
...
return process_content(result[0]['content'])) 當然,上面說了,這個具體情況下,直接用 Markdown parser 更方便,client side preview 有很多選擇 Rendering
至於 RESTful API,還是可以提供,只要使用 flask extension 的 ORM 和 REST 框架,可以自動生成 API 的,一般並不需要重複寫 views 用 blueprint 模塊化 我寫過一個類似規模的 project,連 100% test coverage 的 tests 代碼,連注釋,連其他 script,總共才 3000 loc python,其中 1000 多是 tests,200 是 script。各個 module 20-80 不等,只有 models 和 views 我為了減少相互 import 故意放在一起,一個 400 多,一個 200 多,但都很好閱讀,因為都是同類的。 最後 目前這個代碼太多了,建議先合理分類好模塊,重構一下,精簡代碼,現在是 3695 行 python,我估計可以精簡至少一半,然後再看看。 |
信息量略大…… |
試着重構了幾天,感覺腦子要炸了,搞不下去了……
然後我就想把 於是我想,把原來的代碼先拆開,把要緊的問題小修一下,再把剩下的功能完成(基本就是些前端的事),先能用再說,然後再研究怎麽重構。 |
我沒注意到原來在 dev branch 有改動
這個會造成 technical debt,你現在代碼這麼新鮮都看/改不下去,六個月後的自己更無法理解了,也就是說,一個根據經驗很合理的推測是,現在這個 code base,開發者以後基本上很難會花比現在少的力氣改進,或者在這基礎上更加一些新功能,或者定位查找 bugs,換言之,無法維護。 因爲我上次留言還沒看到 README,沒法本地測試(這就是項目架構先搞好的重要性,我想測試一下,發些小 pull request,沒門路才開始寫上面一大段的),不知道完成度怎麼樣,如果你覺得基本功能都完成了,那我覺得最好的下一步是重寫,完全重頭來。在沒有 deadline 的情況下,依照之前寫過的經驗,重寫一個往往比全盤重構更省事,而且最終會得到更好的代碼,因爲之前走過的(時間和設計上的)彎路都能避免了。 一步步重頭來過,看似麻煩,其實更快,如果是我,會這樣做 如果之前沒試過,先通讀一遍 Flask 的文檔,包括後面的 cookbook 和 extension 介紹,需時大概一兩天吧,主要瞭解有什麼提供的和有什麼注意事項,best practice 什麼的,效果達到需要時知道大概哪裏找出來看細節就行。(題外話,閱讀也能提高 coding 水平的,我近十年來的提高,大量閱讀是最重要的因素)
雖然目錄架構,各文件等沒有 100% 通用的標準,相信你看過我上面發的那個論壇應該對你怎麼構建項目的結構(文件,模塊等)會有幫助。 由於具體代碼我不方便公開,我貼一些片段,或許可以給你一些靈感 例如測試 user model 的 tests/test_user.py 全文是: # -*- coding: utf-8 -*-
import pytest
import sqlalchemy.exc
from carucate.models import db, User
def test_create_user(app):
john = User(email='john@example.com', password='1234')
db.session.add(john)
db.session.commit()
user = User.query.filter_by(email='john@example.com').one()
assert user.check_password('1234')
def test_unique_user_email(app):
john = User(email='john@example.com', password='1234')
db.session.add(john)
db.session.commit()
john2 = User(email='john@example.com', password='5678')
db.session.add(john2)
with pytest.raises(sqlalchemy.exc.IntegrityError):
db.session.commit()
def test_user_password_unreadable(app):
john = User(email='john@example.com', password='1234')
with pytest.raises(AttributeError):
'password is: ' + john.password
def test_user_check_password(app):
john = User(email='john@example.com', password='1234')
assert john.check_password('1234')
assert not john.check_password('1234.') 例如 測試 auth 這個 blueprint 的 views 大約是這樣的: # -*- coding: utf-8 -*-
import re
from flask import url_for
from carucate.mail import mail
from carucate.models import db, User
def parse_url(text):
return re.findall(r'http://localhost/\S+', text)[0]
def test_change_email(app, user, login):
with app.test_client() as client, mail.record_messages() as outbox:
login(client)
response = client.post(
url_for('auth.change_email'),
follow_redirects=True,
data=dict(email='jane@example.com'))
html = response.data.decode()
assert 'confirmation email has been sent to you' in html
response = client.get(url_for('auth.logout'), follow_redirects=True)
html = response.data.decode()
assert 'Logged out.' in html
assert len(outbox) == 1
url = parse_url(outbox[0].body)
response = client.get(url, follow_redirects=True)
html = response.data.decode()
assert response.status_code == 200
assert 'email address confirmed' in html
response = client.post(
url_for('auth.login'),
follow_redirects=True,
data=dict(email='john@example.com', password='1234'))
html = response.data.decode()
assert 'Invalid email or password' in html
response = client.post(
url_for('auth.login'),
follow_redirects=True,
data=dict(email='jane@example.com', password='1234'))
html = response.data.decode()
assert 'Logged in successfully.' in html
def test_change_email_with_get_should_fail(app, user, login):
with app.test_client() as client:
login(client)
response = client.get(
url_for('auth.change_email'),
follow_redirects=True,
query_string=dict(email='jane@example.com'))
assert response.status_code == 200
assert User.query.one().email == 'john@example.com'
def test_change_password(app, user, login):
with app.test_client() as client:
login(client)
response = client.post(
url_for('auth.change_password'),
follow_redirects=True,
data=dict(password='incorrect', newpass='4321', confirm='4321'))
html = response.data.decode()
assert 'Incorrect password' in html
assert user.check_password('1234')
client.post(
url_for('auth.change_password'),
follow_redirects=True,
data=dict(password='1234', newpass='4321', confirm='4321'))
assert user.check_password('4321')
...
def test_invalid_activation_token(app):
with app.test_client() as client:
response = client.get(
url_for('auth.activate_account', token='invalid'),
follow_redirects=True)
assert response.status_code == 404
def test_invalid_login(app):
with app.test_client() as client:
response = client.post(
url_for('auth.login'),
follow_redirects=True,
data=dict(email='john@example.com', password='1234'))
html = response.data.decode()
assert 'Invalid email or password' in html
def test_login_logout(app):
john = User(email='john@example.com', password='1234')
john.is_active = True
john.email_confirmed = True
db.session.add(john)
db.session.commit()
with app.test_client() as client:
response = client.post(
url_for('auth.login'),
follow_redirects=True,
data=dict(email='john@example.com', password='1234'))
html = response.data.decode()
assert 'Logged in successfully.' in html
response = client.get(url_for('auth.logout'), follow_redirects=True)
html = response.data.decode()
assert 'Logged out.' in html
... 比如 auth blueprint 的 views (auth/views.py) 裏部分內容是這樣的 from flask import (
Blueprint,
abort,
current_app,
flash,
redirect,
render_template,
request,
url_for,
)
from flask_login import (
LoginManager,
current_user,
login_required,
login_user,
logout_user,
)
from itsdangerous import URLSafeTimedSerializer
from .forms import (
ChangeEmailForm,
ChangeNameForm,
ChangePasswordForm,
ForgetPasswordForm,
LoginForm,
ResetPasswordForm,
SignupForm,
)
from .. import mail
from ..constants import DEFAULT_TOKEN_MAX_AGE, MAX_USER_NAME_LENGTH
from ..models import User, db
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
def init_app(app):
login_manager.init_app(app)
auth = Blueprint('auth', __name__)
...
@auth.route('/activate/<token>/')
def activate_account(token):
salt = current_app.config['CARUCATE_ACTIVATION_SALT']
max_age = current_app.config['CARUCATE_ACTIVATION_TOKEN_MAX_AGE']
email = validate_token_or_404(token, salt, max_age)
user = User.query.filter_by(email=email).first_or_404()
user.email_confirmed = True
user.is_active = True
db.session.add(user)
db.session.commit()
flash('account activated, please login with your email')
return redirect(url_for('.login'))
...
@auth.route('/login/', methods=['GET', 'POST'])
def login():
if not current_user.is_anonymous:
return redirect(url_for('home.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if check_password(user, form.password.data):
login_user(user, form.remember_me.data)
flash('Logged in successfully.')
next_ = request.args.get('next')
return redirect(next_ or url_for('dashboard.index'))
flash('Invalid email or password')
return render_template('auth/login.html', form=form)
@auth.route('/logout/')
@login_required
def logout():
logout_user()
flash('Logged out.')
return redirect(url_for('.login'))
... 而 app.py 全文裏是大概這樣的 from flask import Flask
from flask_gravatar import Gravatar
from flask_moment import Moment
from flask_pure import Pure
from flask_simplemde import SimpleMDE
from .config import DefaultConfig
from .auth.views import auth, init_app as auth_init_app
...
from .dashboard.views import dashboard
...
from .group.views import group
from .home.views import home
...
from .user.views import user
from .mail import init_app as mail_init_app
from .models import init_app as models_init_app
def create_app(config=None, config_filename=None):
"""application factory"""
app = Flask(__name__, instance_relative_config=True)
if config is None:
config = DefaultConfig
app.config.from_object(config)
if config_filename is not None:
app.config.from_pyfile(config_filename)
app.config.from_envvar('CARUCATE_SETTINGS', silent=True)
mail_init_app(app)
models_init_app(app)
auth_init_app(app)
Gravatar(app, default='identicon')
Moment(app)
Pure(app)
SimpleMDE(app)
app.register_blueprint(auth, url_prefix='/auth')
...
app.register_blueprint(dashboard, url_prefix='/dashboard')
...
app.register_blueprint(group, url_prefix='/groups')
app.register_blueprint(home)
...
app.register_blueprint(user, url_prefix='/user')
return app 部分代碼大小,這是功能完整的了,就是我前面提到的原則,實際上每個 module 超過 200 loc 就變得難以查找和理解(除非是 utils 這類每個 function 之前沒有很緊密結合的工具 module) wc -l `hg manifest | grep '\.py'`
0 carucate/__init__.py
50 carucate/app.py
0 carucate/auth/__init__.py
67 carucate/auth/forms.py
235 carucate/auth/views.py
...
36 carucate/config.py
53 carucate/constants.py
0 carucate/dashboard/__init__.py
13 carucate/dashboard/views.py
52 carucate/decorators.py
...
9 carucate/forms.py
0 carucate/group/__init__.py
52 carucate/group/forms.py
65 carucate/group/views.py
0 carucate/home/__init__.py
28 carucate/home/views.py
...
43 carucate/mail.py
11 carucate/mixins.py
414 carucate/models.py
...
0 carucate/user/__init__.py
25 carucate/user/views.py
34 carucate/utils.py
35 fabfile.py
84 manage.py
30 scripts/create_dev_config.py
30 scripts/create_production_config.py
26 setup.py
... |
我覺得帖子的回覆,要麼就傳統論壇的引用,要麼就支持多層嵌套,像貼吧那種只有一層樓中樓的,我認爲是高不成低不就的做法。 多層嵌套就是每個帖子的 model 做成樹的形式,爲了避免多次遞歸造成的訪問數據庫次數過多,有好些方法避免,上面貼的那個片段的項目我用的是其中一種叫 materialized path,簡單的說就是不用自引用的 foreign key 而是用一個專門的 field 來記錄 path,比如 path='123/456/789',其中數字是 id。materialized path 非常適合用在支持多層嵌套的帖子,排序,插入,刪除,查找子樹,都很低 overhead。 |
那么窝们就先来规划一下, 然后 PR 什么的慢慢来咯? Mark @pyx 大神的建议好实用 |
待我慢慢研究研究…… |
強烈建議 @1dot75cm 的建議,通讀一遍 Flask Web Development,再開始寫,如果找不到本書,作者有一系列的 blog post,書的內容就是從那裡提煉出來的。 裡面有不少好的建議,包括怎樣組織項目結構,選用 extensions,best practices,雖然有些建議我不見得認同[1],但 Flask 和其他 full stack (Django,RoR,etc.) 比起來,就是 all about choices,值得一看。 [1]:
|
窝刚把 Flask 资源汇总了一下,供参考。 @910JQK |
好久不見,決定做輪子啦。
我剛剛看了一下現在的 code base,覺得好多地方可以更好,包括風格,架構,減少重複代碼,更 pythonic 等,有時間我可以一個個 pull request 來。
在此之前,我決定將上次弄的半成品(我發過截圖那個)放出來,或許你可以參考一下。
純 python 和 hy 的 port 都放。
i18n 和 log 可以很簡單寫好 production-ready 的代碼,大概幾十行內就行,但我不確定什麼時候有時間準備 pull request,
另外一些設計決定,如果你有興趣的話我們可以再深入討論一下。
The text was updated successfully, but these errors were encountered: