https://github.com/jshwi https://stackoverflow.com/users/13316671/jshwi https://www.linkedin.com/in/stephen-whitlock/
Last seen on:
Supports reStructuredText (
Sphinx
),
NumPy
, and
Google
Find
docsig
source,
docsig
releases,
docsig
documentation, and
docsig
pre-commit hook
If you are interested in contributing to
docsig
please read about
contributing
here
$ pip install docsig
usage: docsig [-h] [-v] [-l] [-c | -C] [-D] [-m] [-o] [-p] [-P] [-i] [-a] [-k]
[-n] [-S] [-s STR] [-d LIST] [-t LIST]
[path [path ...]]
Check signature params for proper documentation
positional arguments:
path directories or files to check
optional arguments:
-h, --help show this help message and exit
-v, --version show program's version number and exit
-l, --list-checks display a list of all checks and their messages
-c, --check-class check class docstrings
-C, --check-class-constructor check __init__ methods. Note: mutually
incompatible with -c
-D, --check-dunders check dunder methods
-m, --check-protected-class-methods check public methods belonging to protected
classes
-o, --check-overridden check overridden methods
-p, --check-protected check protected functions and classes
-P, --check-property-returns check property return values
-i, --ignore-no-params ignore docstrings where parameters are not
documented
-a, --ignore-args ignore args prefixed with an asterisk
-k, --ignore-kwargs ignore kwargs prefixed with two asterisks
-n, --no-ansi disable ansi output
-S, --summary print a summarised report
-s STR, --string STR string to parse instead of files
-d LIST, --disable LIST comma separated list of rules to disable
-t LIST, --target LIST comma separated list of rules to target
Options can also be configured with the pyproject.toml file
If you find the output is too verbose then the report can be configured to display a summary
[tool.docsig]
check-dunders = false
check-overridden = false
check-protected = false
summary = true
disable = [
"E101",
"E102",
"E103",
]
target = [
"E102",
"E103",
"E104",
]
>>> from docsig import docsig
>>> string = """
... def function(param1, param2, param3) -> None:
... '''
...
... :param param1: About param1.
... :param param2: About param2.
... :param param3: About param3.
... '''
... """
>>> docsig(string=string)
0
>>> string = """
... def function(param1, param2) -> None:
... '''
...
... :param param1: About param1.
... :param param2: About param2.
... :param param3: About param3.
... '''
... """
>>> docsig(string=string)
2
-
def function(✓param1, ✓param2, ✖None) -> ✓None:
"""
:param param1: ✓
:param param2: ✓
:param param3: ✖
"""
<BLANKLINE>
E102: includes parameters that do not exist (params-do-not-exist)
<BLANKLINE>
1
A full list of checks can be found here
To control checks
docsig
accepts disable and enable directives
To disable individual function checks add an inline comment similar to the example below
>>> string = """
... def function_1(param1, param2, param3) -> None: # docsig: disable
... '''
...
... :param param2: Fails.
... :param param3: Fails.
... :param param1: Fails.
... '''
...
... def function_2(param1, param2) -> None:
... '''
...
... :param param1: Fails.
... :param param2: Fails.
... :param param3: Fails.
... '''
...
... def function_3(param1, param2, param3) -> None:
... '''
...
... :param param1: Fails.
... :param param2: Fails.
... '''
... """
>>> docsig(string=string)
10
--
def function_2(✓param1, ✓param2, ✖None) -> ✓None:
"""
:param param1: ✓
:param param2: ✓
:param param3: ✖
"""
<BLANKLINE>
E102: includes parameters that do not exist (params-do-not-exist)
<BLANKLINE>
18
--
def function_3(✓param1, ✓param2, ✖param3) -> ✓None:
"""
:param param1: ✓
:param param2: ✓
:param None: ✖
"""
<BLANKLINE>
E103: parameters missing (params-missing)
<BLANKLINE>
1
To disable all function checks add a module level comment similar to the example below
>>> string = """
... # docsig: disable
... def function_1(param1, param2, param3) -> None:
... '''
...
... :param param2: Fails.
... :param param3: Fails.
... :param param1: Fails.
... '''
...
... def function_2(param1, param2) -> None:
... '''
...
... :param param1: Fails.
... :param param2: Fails.
... :param param3: Fails.
... '''
...
... def function_3(param1, param2, param3) -> None:
... '''
...
... :param param1: Fails.
... :param param2: Fails.
... '''
... """
>>> docsig(string=string)
0
To disable multiple function checks add a module level disable and enable comment similar to the example below
>>> string = """
... # docsig: disable
... def function_1(param1, param2, param3) -> None:
... '''
...
... :param param2: Fails.
... :param param3: Fails.
... :param param1: Fails.
... '''
...
... def function_2(param1, param2) -> None:
... '''
...
... :param param1: Fails.
... :param param2: Fails.
... :param param3: Fails.
... '''
... # docsig: enable
...
... def function_3(param1, param2, param3) -> None:
... '''
...
... :param param1: Fails.
... :param param2: Fails.
... '''
... """
>>> docsig(string=string)
20
--
def function_3(✓param1, ✓param2, ✖param3) -> ✓None:
"""
:param param1: ✓
:param param2: ✓
:param None: ✖
"""
<BLANKLINE>
E103: parameters missing (params-missing)
<BLANKLINE>
1
The same can be done for disabling individual rules
>>> string = """
... # docsig: disable=E101
... def function_1(param1, param2, param3) -> int:
... '''E105.
...
... :param param1: Fails.
... :param param2: Fails.
... :param param3: Fails.
... '''
...
... def function_2(param1, param2, param3) -> None: # docsig: disable=E102,E106
... '''E101,E102,E106.
...
... :param param1: Fails.
... :param param1: Fails.
... :param param2: Fails.
... :param param3: Fails.
... '''
...
... def function_3(param1, param2, param3) -> None:
... '''E101,E102,E106,E107.
...
... :param param1: Fails.
... :param param1: Fails.
... :param param2: Fails.
... :param: Fails.
... '''
... """
>>> docsig(string=string)
3
-
def function_1(✓param1, ✓param2, ✓param3) -> ✖int:
"""
:param param1: ✓
:param param2: ✓
:param param3: ✓
:return: ✖
"""
<BLANKLINE>
E105: return missing from docstring (return-missing)
<BLANKLINE>
20
--
def function_3(✓param1, ✖param2, ✖param3, ✖None) -> ✓None:
"""
:param param1: ✓
:param param1: ✖
:param param2: ✖
:param None: ✖
"""
<BLANKLINE>
E102: includes parameters that do not exist (params-do-not-exist)
E106: duplicate parameters found (duplicate-params-found)
E107: parameter appears to be incorrectly documented (param-incorrectly-documented)
<BLANKLINE>
1
Individual rules can also be re-enabled
Module level directives will be evaluated separately to inline directives and providing no rules will disable and enable all rules
>>> string = """
... # docsig: disable
... def function_1(param1, param2, param3) -> int:
... '''E105.
...
... :param param1: Fails.
... :param param2: Fails.
... :param param3: Fails.
... '''
...
... def function_2(param1, param2, param3) -> None: # docsig: enable=E102,E106
... '''E101,E102,E106.
...
... :param param1: Fails.
... :param param1: Fails.
... :param param2: Fails.
... :param param3: Fails.
... '''
...
... def function_3(param1, param2, param3) -> None:
... '''E101,E102,E106,E107.
...
... :param param1: Fails.
... :param param1: Fails.
... :param param2: Fails.
... :param: Fails.
... '''
... """
>>> docsig(string=string)
11
--
def function_2(✓param1, ✖param2, ✖param3, ✖None) -> ✓None:
"""
:param param1: ✓
:param param1: ✖
:param param2: ✖
:param param3: ✖
"""
<BLANKLINE>
E102: includes parameters that do not exist (params-do-not-exist)
E106: duplicate parameters found (duplicate-params-found)
<BLANKLINE>
1
Checking a class docstring is not enabled by default, as there are two mutually exclusive choices to choose from.
This check will either check the documentation of
__init__
, or check
documentation of
__init__
under the class docstring, and not under
__init__
itself
>>> string = """
... class Klass:
... def __init__(self, param1, param2) -> None:
... '''
...
... :param param1: About param1.
... :param param2: About param2.
... :param param3: About param3.
... '''
... """
>>> docsig(string=string, check_class_constructor=True)
3 in Klass
----------
class Klass:
"""
:param param1: ✓
:param param2: ✓
:param param3: ✖
"""
<BLANKLINE>
def __init__(✓param1, ✓param2, ✖None) -> ✓None:
<BLANKLINE>
E102: includes parameters that do not exist (params-do-not-exist)
<BLANKLINE>
1
>>> string = """
... class Klass:
... '''
...
... :param param1: About param1.
... :param param2: About param2.
... :param param3: About param3.
... '''
... def __init__(self, param1, param2) -> None:
... pass
... """
>>> docsig(string=string, check_class=True)
9 in Klass
----------
class Klass:
"""
:param param1: ✓
:param param2: ✓
:param param3: ✖
"""
<BLANKLINE>
def __init__(✓param1, ✓param2, ✖None) -> ✓None:
<BLANKLINE>
E102: includes parameters that do not exist (params-do-not-exist)
<BLANKLINE>
1
Checking class docstrings can be permanently enabled in the pyproject.toml file
[tool.docsig]
check-class-constructor = true
Or
[tool.docsig]
check-class = true
docsig
can be used as a
pre-commit
hook
It can be added to your .pre-commit-config.yaml as follows:
repos:
- repo: https://github.com/jshwi/docsig
rev: v0.44.1
hooks:
- id: docsig
args:
- "--check-class"
- "--check-dunders"
- "--check-overridden"
- "--check-protected"
- "--summary"
If you are entering
Flask
from
django
you'll notice that
Flask
doesn't come with an admin interface.
As per the
django
documentation
for its admin interface:
One of the most powerful parts of Django is the automatic admin interface.
One of the most powerful parts about
Flask
, however, is its lack of these things. Everything is up to you as the developer.
Flask-Admin
can be used to set up an admin interface, so you can manage your database through your browser.
There are 3 main issues, I found, when using
Flassk-Admin
features out-of-the-box
Flask-Login
AssertionError: A blueprint's name collision occurred between <flask.blueprints.Blueprint object at 0x25e5d90> and <flask.blueprints.Blueprint object at 0x21b89d0>. Both share the same name "user". Blueprints that are created on the fly need unique names.
Flask-Admin
, however, is really easy to set up despite this.
$ pip install flask-admin flask-login
For a package to be recognised as an official
Flask
extension it should include the
init_app
method, so that the app does not need to passed directly to the extension upon instantiation.
This enables the extension to be passed around within the app's modules before it even exists. This prevents circular imports, as is the reason for the app factory pattern.
Luckily,
Flask-Admin
does not need to passed around, and therefore works when it is instantiated with the app object.
Your
extensions
module might look like something like this
# app/extensions.py
from flask_login import LoginManager
...
from flask_sqlalchemy import SQLAlchemy
...
login_manager = LoginManager()
...
sqlalchemy = SQLAlchemy()
...
def init_app(app):
...
login_manager.init_app(app)
...
sqlalchemy.init_app(app)
...
To be registered in the app's
__init__.py
file like this:
# app/__init__.py
from flask import Flask
...
from app import extensions
...
def create_app():
app = Flask(__name__)
...
extensions.init_app(app)
...
return app
Create a new app/admin.py module and write up a new
init_app
function
Solution to 1
Flask-Login
comes with the extremely useful
login_required
decorator.
Put this on top of your routes, and the user is required to be logged in to access it.
We can do this with an admin user too.
Add a boolean value to your user model;
admin
# app/models.py
from flask_login import UserMixin
from app.extensions import db
...
class User(UserMixin, db.Model):
...
admin = db.Column(db.Boolean, default=False)
...
...
Now we can decorate the
login_required
functionality with a more restricted,
admin
function.
login_required
, under the hood, returns a
LoginManager.unauthorized
function if the user is not logged in.
This raises, among other things, raises a
401 Unauthorized
error.
How you make your user an admin is pretty simple, but I won't go into it here.
import functools
from flask_login import current_user
from app.extensions import login_manager
def admin_required(func):
"""Handle views that require an admin be signed in.
:param func: View function to wrap.
:return: The wrapped function supplied to this decorator.
"""
@functools.wraps(func)
def _wrapped_view(*args, **kwargs):
if not current_user.admin:
return login_manager.unauthorized()
return func(*args, **kwargs)
return _wrapped_view
Create a new admin.py module.
Flask-Admin
, by default, constructs its index page with the
AdminIndexView
class.
We can override this class and its
index
method to be decorated with our
admin_required
function.
Make sure to continue using
login_required
as a sort of hierarchy of logins, otherwise the
admin_required
decorator will fail. This is because the decorator needs the user to logged in to check for admin.
# app/admin.py
from flask_admin import AdminIndexView, expose
...
from flask_login import login_required
from app.utils.security import admin_required
class MyAdminIndexView(AdminIndexView):
"""Custom index view that handles login / registration."""
@expose("/")
@login_required
@admin_required
def index(self) -> str:
"""Requires user be logged in as admin."""
return super().index()
We can then write our
init_app
function like this.
# app/admin.py
from flask_admin import Admin
...
def init_app(app):
admin = Admin( # noqa
app,
index_view=MyAdminIndexView(), # type: ignore
)
...
Solution to problem 2
We can subclass
flask_admin.contrib.sqla.ModelView
to register their blueprints under a different name.
Now you can use the
/user
,
/post
, etc. prefixes on your user, post etc. routes, and not waste them on the admin routes.
Because the views display, usually, multiple user, post etc. tables, it makes sense to end them as a plural.
It is also worth pointing out that we have blocked access to these routes for non-admins too.
# app/admin.py
from flask_admin import Admin
from flask_admin import AdminIndexView
from flask_admin.contrib import sqla
from flask_login import current_user
from app.extensions import db
from app.utils.models import User, Post, Task, Message, Notification
class MyAdminIndexView(AdminIndexView):
...
class MyModelView(sqla.ModelView):
"""Custom model view that determines accessibility."""
def __init__(self, model: db.Model, session: db.session) -> None:
super().__init__(model, session)
self.endpoint = f"{model.__table__}s" # change the table name here
def is_accessible(self) -> None:
"""Only allow access if user is logged in as admin."""
return current_user.is_authenticated and current_user.admin
...
def init_app(app):
admin = Admin( # noqa
app,
index_view=MyAdminIndexView(), # type: ignore
)
admin.add_view(MyModelView(User, db.session))
admin.add_view(MyModelView(Post, db.session))
admin.add_view(MyModelView(Message, db.session))
admin.add_view(MyModelView(Notification, db.session))
admin.add_view(MyModelView(Task, db.session))
Finally, as a solution to 3, we initialize the app
# app/__init__.py
from flask import Flask
...
from app import extensions
from app.routes import admin
...
def create_app():
app = Flask(__name__)
...
extensions.init_app(app)
admin.init_app(app)
...
return app
SQLAlchemy
has a limited versioning extension that does not support entire transactions.
SQLAlchemy-Continuum
offers a flexible API for implementing a versioning mechanism to your
SQLAlchemy
ORM
database.
If you want to ensure nothing is lost after editing your database I have found this package to be a good place to start. There are drawbacks when it comes to data retention; hashing algorithms and data compression aren't built into the API.
This article assumes you are using
Flask-SQLAlchemy
.
pip install sqlalchemy-continuum
In the module where your
SQLAlchemy
models are defined call
make_versioned()
before their definition and add
__versioned__
to all models you wish to add versioning to.
# app/models.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import configure_mappers
from sqlalchemy_continuum import make_versioned
app = Flask(__name__)
db = SQLAlchemy()
db.init_app(app)
make_versioned(user_cls=None)
class Post(db.Base):
__versioned__ = {}
...
configure_mappers()
Database queries will now have a list-like object called
versions
from app.models import Post, db
id = 1 # first item in the db; we are using `get` and the db starts at 1
revision = 1 # however, with `post.versions` we are working with list indexes
# for a post object with at least 1 entry, and at least 2 versions
post = Post.query.get(id=id)
post.versions[revision].revert()
db.session.commit()
With the above, a third item has been added to
post.versions
, which is basically a duplicate of the first.
We can then craft a route that will restore the previous version:
from app.utils.models import Post, db
from flask_login import login_required
from flask import redirect, url_for, Blueprint
bp = Blueprint("views", __name__)
...
@bp.route("/<int:id>/version/<int:revision>")
@login_required
def version(id, revision):
post = Post.query.get(id=id)
post.versions[revision].revert()
...
db.session.commit()
return redirect(url_for("views.index"))
...
Accessing
/1/version/1
after your URL will restore version 1 from post 1
If you already have an update view, much like the one demonstrated here https://flask.palletsprojects.com/en/2.0.x/tutorial/blog/ then we can also load the previous version with a query-string. With this we don't have to blindly restore versions (and add more duplicate versions along the way).
by default, the revision returned will be the last one i.e.
/1/update
is the same as
/1/update?revision=-1
.
This time we will return the version object - this way the form will be pre-loaded with the revision.
Once the form's submission is validated the current
post
object will be replaced with restored revision.
Once this is committed either a duplicate or edited version will be added to
post.versions
.
from app.utils.models import Post, db
from flask import redirect, Blueprint, request, render_template
from flask_login import login_required
from app.utils.forms import PostForm
bp = Blueprint("views", __name__)
...
@bp.route("/<int:id>/update", methods=["GET", "POST"])
@login_required
def update(id):
# by default, revision returned will be the last one
revision = request.args.get("revision", -1, type=int)
post = Post.query.get(id=id)
# this time we will return the version object
version = post.versions[revision]
# this way the form will be preloaded with the revision
form = PostForm(title=version.title, body=version.body)
# once the form's submit button is pressed the current `post` object
# will be replaced with restored revision
if form.validate_on_submit():
post.title = form.title.data
post.body = form.body.data
...
# once this is committed either a duplicate or edited version
# will be added to `post.versions`
db.session.commit()
return redirect(url_for("views.index"))
return render_template("update.html", post=post, form=form)
...
Check out
The Flask Mega-Tutorial
by Miguel Grinberg. This tutorial highlights the flexibility of the Flask
micro framework
.
With assistance from the above this site includes:
Flask-SqlAlchemy
's database object-relational mapping. This is a good fit for object-oriented programming, and databases can run under SQLite, and PostgreSQL (with psycopg2). Using Flask-SqlAlchemy, after using Python's native sqlite3, it is easy to see why this would be the one to roll with. Those used to working with queries in the SQL console might find a preference for the
str
methodology that sqlite3 incorporates, however, seeing as SQLite is not appropriate as a persistent production-grade database SQLAlchemy is the pick.
current_user
instance.
Flask as a micro framework:
In contrast to Django (these two frameworks being Python's most used web frameworks by far), Flask is un-opinionated, meaning there is no right way to do any one task. Instead, Flask functions through an ecosystem of extensions. Authorization for example can be handled by Flask-Login, Flask-Security, Flask-User, to name a few. This can be a good and a bad thing. The options can be daunting, and some projects have been abandoned (or close to). Flask is ideal for learning application development at a finer level, whereas Django has many aspects of web development included out of the box, such as an admin dashboard interface. An admin dashboard can be achieved with Flask, but only if the user requires it (see Flask-Admin).
See me on Github
commit cad6a56
: feat: Adds support for background tasks
Resource:
The Flask Mega-Tutorial Part XXII: Background Jobs
Introduces:
Redis
.
rq
commit afaeffe
: feat: Adds private messaging feature
Resource:
The Flask Mega-Tutorial Part XXI: User Notifications
commit 655cd08
: add: Adds syntax highlighting
Introduces:
highlight.js
commit 94a0e87
: change: Adds
Flask-Moment
for displaying timestamps
Resource:
The Flask Mega-Tutorial Part XII: Dates and Times
Introduces:
Flask-Moment
(Flask implementation of
moment.js
)
commit 7bc2771
: feat: Adds pagination to posts
Resource:
The Flask Mega-Tutorial Part IX: Pagination
commit 20ea7e0
: add: Adds
Flask-Bootstrap
Resource:
The Flask Mega-Tutorial Part XI: Facelift
Introduces:
Flask-Bootstrap
(Flask implementation of
Bootstrap
)
commit 3861349
: add: Adds support for markdown
Introduces:
Flask-PageDown
,
Flask-Misaka
(Flask implementation of
misaka
)
commit a8a6dd3
: add: Adds followers functionality
Resource:
The Flask Mega-Tutorial Part VIII: Followers
commit edd1135
: add: Adds edit profile page
commit eb393db
: add: Records last login time for user
commit 7bd9dfb
: add: Adds user profiles
commit 88ab80c
: add: Adds user avatars
Resource:
The Flask Mega-Tutorial Part VI: Profile Page and Avatars
commit 4774391
: add: Adds error logging email handler
Resource:
The Flask Mega-Tutorial Part X: Email Support
commit bac8824
: feat: Adds reset password functionality
Resource:
The Flask Mega-Tutorial Part X: Email Support
Introduces:
pyjwt
(Python implementation of
JSON Web Tokens
)
commit c356764
: feat: Adds email verification
Resource:
Handling Email Confirmation During Registration in Flask
Introduces:
itsdangerous
commit 8c9c72f
: change: Adds
Flask-WTF
for handling forms
Resource:
The Flask Mega-Tutorial Part III: Web Forms
Introduces:
Flask-WTF
(Flask implementation of
WTForms
)
commit aeaf4f1
: change: Adds
Flask-Login
for handling logins
Resource:
The Flask Mega-Tutorial Part V: User Logins
Introduces:
Flask-Login
commit 67a9e12
: add: Adds
app.mail
Resource:
The Flask Mega-Tutorial Part X: Email Support
Introduces:
Flask-Mail
commit 70acb5f
: change: moves database over to
SQLAlchemy
Resource:
The Flask Mega-Tutorial Part IV: Database
Introduces:
Flask-Sqlalchemy
,
Flask-Migrate
(Flask implementation of
alembic
)
commit 9c62db7
: Initial commit
Flask's Tutorial: Flaskr
v1.0.0: CHANGELOG.md
Written in Python with
Flask
using
PyCharm
Template Engine:
Jinja
CSS Framework:
Flask-Bootstrap
CSS Dark Mode:
Darkreader
Source:
https://github.com/jshwi/jss
Documentation:
https://jshwi.github.io/jss/
Hosted on Heroku:
https://jshwisolutions.herokuapp.com