From 2f1a5bd00476bc8af5a7c53348a20fcc0daef393 Mon Sep 17 00:00:00 2001
From: Jack Grahl
Date: Fri, 17 Feb 2023 21:12:33 +0000
Subject: [PATCH 1/7] Use pip to install a not necessarily fully compatible
flask.
---
.github/workflows/minimal-test.yml | 4 ++--
.github/workflows/test.yml | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/minimal-test.yml b/.github/workflows/minimal-test.yml
index 021f871..c582b5a 100644
--- a/.github/workflows/minimal-test.yml
+++ b/.github/workflows/minimal-test.yml
@@ -18,10 +18,10 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install poetry
run: pip install poetry
- - name: Force the chosen flask version
- run: poetry add flask==${{ matrix.flask-version }}
- name: Install requirements
run: poetry install
+ - name: Force the chosen flask version
+ run: pip install flask==${{ matrix.flask-version }}
- name: Test package
run: poetry run test
- name: Test package
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index fda0012..ec2460c 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -18,10 +18,10 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install poetry
run: pip install poetry
- - name: Force the chosen flask version
- run: poetry add flask==${{ matrix.flask-version }}
- name: Install requirements
run: poetry install
+ - name: Force the chosen flask version
+ run: pip install flask==${{ matrix.flask-version }}
- name: Run unit tests
run: poetry run test
- name: Run doctests
From 703507ebb9e1c50e9694b205ffd679489e5f7651 Mon Sep 17 00:00:00 2001
From: Jack Grahl
Date: Fri, 17 Feb 2023 21:13:00 +0000
Subject: [PATCH 2/7] Revert "Use pip to install a not necessarily fully
compatible flask."
This reverts commit 2f1a5bd00476bc8af5a7c53348a20fcc0daef393.
Pushed to main by mistake.
---
.github/workflows/minimal-test.yml | 4 ++--
.github/workflows/test.yml | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/minimal-test.yml b/.github/workflows/minimal-test.yml
index c582b5a..021f871 100644
--- a/.github/workflows/minimal-test.yml
+++ b/.github/workflows/minimal-test.yml
@@ -18,10 +18,10 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install poetry
run: pip install poetry
+ - name: Force the chosen flask version
+ run: poetry add flask==${{ matrix.flask-version }}
- name: Install requirements
run: poetry install
- - name: Force the chosen flask version
- run: pip install flask==${{ matrix.flask-version }}
- name: Test package
run: poetry run test
- name: Test package
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ec2460c..fda0012 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -18,10 +18,10 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install poetry
run: pip install poetry
+ - name: Force the chosen flask version
+ run: poetry add flask==${{ matrix.flask-version }}
- name: Install requirements
run: poetry install
- - name: Force the chosen flask version
- run: pip install flask==${{ matrix.flask-version }}
- name: Run unit tests
run: poetry run test
- name: Run doctests
From c56ad2521f4df8de187bf099c28dd3c75961d14f Mon Sep 17 00:00:00 2001
From: ValentinFrancois <24223132+ValentinFrancois@users.noreply.github.com>
Date: Fri, 5 May 2023 22:59:41 +0200
Subject: [PATCH 3/7] fix Jinja imports
---
flask_selfdoc/autodoc.py | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/flask_selfdoc/autodoc.py b/flask_selfdoc/autodoc.py
index d0fb46f..4568e5f 100644
--- a/flask_selfdoc/autodoc.py
+++ b/flask_selfdoc/autodoc.py
@@ -6,9 +6,23 @@
import inspect
from flask import current_app, render_template, render_template_string, jsonify
-from jinja2 import evalcontextfilter, Markup
from jinja2.exceptions import TemplateAssertionError
+try:
+ # Jinja2 < 3.1 (Flask <= 2.0 and python 3.6)
+ # https://jinja.palletsprojects.com/en/3.0.x/api/#jinja2.evalcontextfilter
+ from jinja2 import evalcontextfilter as pass_eval_context
+except ImportError:
+ # Jinja2 < 3.1 (Flask >= 2.0 and python <= 3.7)
+ from jinja2 import pass_eval_context
+
+try:
+ # Jinja2 < 3.1 (Flask <= 2.0 and python 3.6)
+ from jinja2 import Markup
+except ImportError:
+ # Jinja2 < 3.1 (Flask >= 2.0 and python <= 3.7)
+ from jinja2.utils import markupsafe
+ Markup = markupsafe.Markup
try:
from flask import _app_ctx_stack as stack
@@ -57,7 +71,7 @@ def add_custom_nl2br_filters(self, app):
_paragraph_re = re.compile(r'(?:\r\n|\r|\n){3,}')
@app.template_filter()
- @evalcontextfilter
+ @pass_eval_context
def nl2br(eval_ctx, value):
result = '\n\n'.join('%s' % p.replace('\n', Markup('
\n'))
for p in _paragraph_re.split(value))
From 688d3a08f9dc78250564c0150041c7bc565a3c7f Mon Sep 17 00:00:00 2001
From: ValentinFrancois <24223132+ValentinFrancois@users.noreply.github.com>
Date: Fri, 5 May 2023 23:08:33 +0200
Subject: [PATCH 4/7] fix Flask compatibility
---
flask_selfdoc/autodoc.py | 13 ++++++++++---
pyproject.toml | 6 +++++-
2 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/flask_selfdoc/autodoc.py b/flask_selfdoc/autodoc.py
index 4568e5f..e7c8939 100644
--- a/flask_selfdoc/autodoc.py
+++ b/flask_selfdoc/autodoc.py
@@ -25,9 +25,13 @@
Markup = markupsafe.Markup
try:
- from flask import _app_ctx_stack as stack
+ from flask.globals import _cv_app
except ImportError:
- from flask import _request_ctx_stack as stack
+ _cv_app = None
+ try:
+ from flask import _app_ctx_stack as stack
+ except ImportError:
+ from flask import _request_ctx_stack as stack
if sys.version < '3':
@@ -58,7 +62,10 @@ def init_app(self, app):
self.add_custom_template_filters(app)
def teardown(self, exception):
- ctx = stack.top # noqa: F841
+ if _cv_app is not None:
+ ctx = _cv_app.get(None) # noqa: F841
+ else:
+ ctx = stack.top # noqa: F841
def add_custom_template_filters(self, app):
"""Add custom filters to jinja2 templating engine"""
diff --git a/pyproject.toml b/pyproject.toml
index ccc4250..9a849fe 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -7,7 +7,11 @@ license = "MIT"
[tool.poetry.dependencies]
python = "^3.6"
-Flask = "^1.0"
+Flask = [
+ {python = "~3.6", version = ">=1.0, <2.1"},
+ {python = ">=3.7, <3.10", version = ">=1.0"},
+ {python = "~3.10", version = ">=1.1"}
+]
[tool.poetry.dev-dependencies]
pytest = "^6.2.5"
From baa41e9f8c61dbdeb1ddc5bfb52c67764814961d Mon Sep 17 00:00:00 2001
From: ValentinFrancois <24223132+ValentinFrancois@users.noreply.github.com>
Date: Fri, 5 May 2023 23:10:46 +0200
Subject: [PATCH 5/7] fix decorator line number
---
flask_selfdoc/autodoc.py | 73 +++++++++++++++++++++++++++++++++++++---
tests/config.py | 7 ----
2 files changed, 69 insertions(+), 11 deletions(-)
diff --git a/flask_selfdoc/autodoc.py b/flask_selfdoc/autodoc.py
index e7c8939..fbb55e1 100644
--- a/flask_selfdoc/autodoc.py
+++ b/flask_selfdoc/autodoc.py
@@ -40,6 +40,74 @@
get_function_code = attrgetter('__code__')
+def get_decorator_frame_info(frame) -> dict:
+ """
+ The way that the line number of a decorator is detected changed across
+ python versions:
+ - python <= 3.8:
+ stack()[1].lineno points to the line above the decorated function
+ => points to the closest decorator, not necessarily the one that did the
+ call to stack()
+ - python 3.9 and 3.10:
+ stack()[1].lineno points to the line of the decorated function
+ - python 3.11:
+ stack()[1].lineno points to the exact line of the decorator that did the
+ call to stack()
+
+ Example:
+
+ 1 |def call_stack_and_get_lineno():
+ 2 |
+ 3 | def decorator(func):
+ 4 | calling_frame = stack()[1]
+ 5 | print(calling_frame.lineno)
+ 6 | return func
+ 7 |
+ 8 | return decorator
+ 9 |
+ 10 |
+ 11 |@decorator1
+ 12 |@call_stack_and_get_lineno
+ 13 |@decorator2
+ 14 |def func():
+ 15 | pass
+
+ - python <= 3.8: will print line 13
+ - python 3.9 and 3.10: will print line 14 (desired behaviour)
+ - python 3.11: will print line 12
+
+ We adjust the found line number with some offset (by reading the python
+ source file) if required.
+ """
+ line_number = frame.lineno
+ try:
+ with open(frame.filename, 'r') as python_file:
+ python_lines = python_file.readlines()
+ # current line + next ones
+ context_lines = python_lines[line_number - 1:]
+ except (OSError, FileNotFoundError):
+ print("You're probably using flask_selfdoc with compiled python code "
+ "- prefer uncompiled source files to extract correct filenames "
+ "and line numbers.")
+ # not 100% correct solution, won't work for multiline decorator
+ # or if there are decorators between @autodoc.doc() and the endpoint
+ # function
+ context_lines = frame.code_context
+
+ # if the detected line number doesn't point to a function definition,
+ # we iterate until we find one.
+ for line in context_lines:
+ if not line.strip().startswith('def '):
+ line_number += 1
+ else:
+ break
+
+ return {
+ 'filename': frame.filename,
+ 'line': line_number,
+ }
+
+
class Autodoc(object):
def __init__(self, app=None):
@@ -125,10 +193,7 @@ def decorator(f):
# Set location
if set_location:
caller_frame = inspect.stack()[1]
- self.func_locations[f] = {
- 'filename': caller_frame[1],
- 'line': caller_frame[2],
- }
+ self.func_locations[f] = get_decorator_frame_info(caller_frame)
return f
return decorator
diff --git a/tests/config.py b/tests/config.py
index 94f4ffa..0524c84 100644
--- a/tests/config.py
+++ b/tests/config.py
@@ -1,10 +1,3 @@
import os
-import sys
-
-# The way that the line number of a function is detected changed
-# The old version chooses the location of the first decorator,
-# the new version chooses the location of the 'def' keyword.
-# We detect the version and support both.
-NEW_FN_OFFSETS = sys.version_info >= (3, 8)
IS_WINDOWS = os.name == 'nt'
From e381d32e08e02b6e4b86cd94e856ab017ad7aacb Mon Sep 17 00:00:00 2001
From: ValentinFrancois <24223132+ValentinFrancois@users.noreply.github.com>
Date: Fri, 5 May 2023 23:13:06 +0200
Subject: [PATCH 6/7] Add optional JSON doc formatting options and adapt unit
tests
---
examples/custom/blog.py | 10 +-
examples/simple/blog.py | 7 +-
flask_selfdoc/autodoc.py | 24 ++-
run_tests.py | 1 -
tests/files/builtin.json | 127 +++++++++++++-
tests/files/custom.json | 302 ++++++++++++++++----------------
tests/files/simple.json | 186 +++++++++++++++++++-
tests/files/simple_private.html | 4 +-
tests/test_autodoc.py | 10 +-
tests/test_check_example.py | 33 ++--
10 files changed, 517 insertions(+), 187 deletions(-)
diff --git a/examples/custom/blog.py b/examples/custom/blog.py
index e8d0fd1..c3934be 100644
--- a/examples/custom/blog.py
+++ b/examples/custom/blog.py
@@ -1,7 +1,7 @@
-from os import path
from json import dumps
-from flask import Flask, redirect, request, jsonify
+from flask import Flask, redirect, request
+from flask_selfdoc.autodoc import custom_jsonify
from flask_selfdoc import Autodoc
@@ -58,7 +58,7 @@ def get_post(id):
@app.route('/post', methods=["POST"])
@auto.doc(groups=['posts', 'private'],
- form_data=['title', 'content', 'authorid'])
+ form_data=['title', 'content', 'authorid'])
def post_post():
"""Create a new post."""
authorid = request.form.get('authorid', None)
@@ -84,7 +84,7 @@ def get_user(id):
@app.route('/users', methods=['POST'])
@auto.doc(groups=['users', 'private'],
- form_data=['username'])
+ form_data=['username'])
def post_user(id):
"""Creates a new user."""
User(request.form['username'])
@@ -111,7 +111,7 @@ def private_doc():
@app.route('/doc/json')
def public_doc_json():
- return jsonify(auto.generate())
+ return custom_jsonify(auto.generate(), indent=4, separators=(',', ': '))
if __name__ == '__main__':
diff --git a/examples/simple/blog.py b/examples/simple/blog.py
index 4c44ead..9ff14c8 100644
--- a/examples/simple/blog.py
+++ b/examples/simple/blog.py
@@ -1,6 +1,7 @@
from json import dumps
-from flask import Flask, redirect, request, jsonify
+from flask import Flask, redirect, request
+from flask_selfdoc.autodoc import custom_jsonify
from flask_selfdoc import Autodoc
@@ -128,12 +129,12 @@ def private_doc():
@app.route('/doc/json')
def public_doc_json():
- return jsonify(auto.generate())
+ return custom_jsonify(auto.generate(), indent=4, separators=(',', ': '))
@app.route('/doc/builtin_json')
def public_doc_builtin_json():
- return auto.json()
+ return auto.json(indent=2, separators=(',', ': '))
if __name__ == '__main__':
diff --git a/flask_selfdoc/autodoc.py b/flask_selfdoc/autodoc.py
index fbb55e1..95853ee 100644
--- a/flask_selfdoc/autodoc.py
+++ b/flask_selfdoc/autodoc.py
@@ -1,9 +1,11 @@
+import json
from operator import attrgetter, itemgetter
import os
import re
from collections import defaultdict
import sys
import inspect
+from typing import Optional, Tuple
from flask import current_app, render_template, render_template_string, jsonify
from jinja2.exceptions import TemplateAssertionError
@@ -40,6 +42,19 @@
get_function_code = attrgetter('__code__')
+def custom_jsonify(*args,
+ indent: Optional[int] = None,
+ separators: Optional[Tuple] = (',', ':'),
+ **kwargs):
+ response = jsonify(*args, **kwargs)
+ json_data = json.loads(response.data.decode('utf-8'))
+ json_string = json.dumps(json_data,
+ indent=indent,
+ separators=separators)
+ response.data = json_string.encode('utf-8')
+ return response
+
+
def get_decorator_frame_info(frame) -> dict:
"""
The way that the line number of a decorator is detected changed across
@@ -239,7 +254,7 @@ def generate(self, groups='all', sort=None):
methods=sorted(list(rule.methods)),
rule="%s" % rule,
endpoint=rule.endpoint,
- docstring=func.__doc__,
+ docstring=func.__doc__.strip(' ') if func.__doc__ else None,
args=arguments,
defaults=rule.defaults or dict(),
location=location,
@@ -287,7 +302,10 @@ def html(self, groups='all', template=None, **context):
raise RuntimeError(
"Autodoc was not initialized with the Flask app.")
- def json(self, groups='all'):
+ def json(self,
+ groups='all',
+ indent: Optional[int] = None,
+ separators: Optional[Tuple] = (',', ':')):
"""Return a json object with documentation for all the routes specified
by the doc() method.
@@ -310,7 +328,7 @@ def endpoint_info(doc):
'endpoints':
[endpoint_info(doc) for doc in autodoc]
}
- return jsonify(data)
+ return custom_jsonify(data, indent=indent, separators=separators)
def sort_lexically(links):
diff --git a/run_tests.py b/run_tests.py
index f8f1d2f..6e22b27 100644
--- a/run_tests.py
+++ b/run_tests.py
@@ -1,6 +1,5 @@
import doctest
import logging
-import os
import subprocess
import unittest
diff --git a/tests/files/builtin.json b/tests/files/builtin.json
index eb8ecf5..f1e6a3b 100644
--- a/tests/files/builtin.json
+++ b/tests/files/builtin.json
@@ -1 +1,126 @@
-{"endpoints":[{"args":[],"docstring":"Return all posts.","methods":["GET","HEAD","OPTIONS"],"rule":"/"},{"args":[],"docstring":"Admin interface.","methods":["GET","HEAD","OPTIONS"],"rule":"/admin"},{"args":[["greeting","Hello"],["id",null]],"docstring":"Return the user for the given id.","methods":["GET","HEAD","OPTIONS"],"rule":"/greet//user/"},{"args":[["id",-1]],"docstring":"Return the user for the given id.","methods":["GET","HEAD","OPTIONS"],"rule":"/hello/user/"},{"args":[],"docstring":"Create a new post.\n Form Data: title, content, authorid.\n ","methods":["OPTIONS","POST"],"rule":"/post"},{"args":[["id",null]],"docstring":"Return the post for the given id.","methods":["GET","HEAD","OPTIONS"],"rule":"/post/"},{"args":[],"docstring":"Return all posts.","methods":["GET","HEAD","OPTIONS"],"rule":"/posts"},{"args":[["id",null]],"docstring":"Return the user for the given id.","methods":["GET","HEAD","OPTIONS"],"rule":"/user/"},{"args":[],"docstring":"Return all users.","methods":["GET","HEAD","OPTIONS"],"rule":"/users"},{"args":[],"docstring":"Creates a new user.\n Form Data: username.\n ","methods":["OPTIONS","POST"],"rule":"/users"}]}
+{
+ "endpoints": [
+ {
+ "args": [],
+ "docstring": "Return all posts.",
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/"
+ },
+ {
+ "args": [],
+ "docstring": "Admin interface.",
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/admin"
+ },
+ {
+ "args": [
+ [
+ "greeting",
+ "Hello"
+ ],
+ [
+ "id",
+ null
+ ]
+ ],
+ "docstring": "Return the user for the given id.",
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/greet//user/"
+ },
+ {
+ "args": [
+ [
+ "id",
+ -1
+ ]
+ ],
+ "docstring": "Return the user for the given id.",
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/hello/user/"
+ },
+ {
+ "args": [],
+ "docstring": "Create a new post.\n Form Data: title, content, authorid.\n",
+ "methods": [
+ "OPTIONS",
+ "POST"
+ ],
+ "rule": "/post"
+ },
+ {
+ "args": [
+ [
+ "id",
+ null
+ ]
+ ],
+ "docstring": "Return the post for the given id.",
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/post/"
+ },
+ {
+ "args": [],
+ "docstring": "Return all posts.",
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/posts"
+ },
+ {
+ "args": [
+ [
+ "id",
+ null
+ ]
+ ],
+ "docstring": "Return the user for the given id.",
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/user/"
+ },
+ {
+ "args": [],
+ "docstring": "Return all users.",
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/users"
+ },
+ {
+ "args": [],
+ "docstring": "Creates a new user.\n Form Data: username.\n",
+ "methods": [
+ "OPTIONS",
+ "POST"
+ ],
+ "rule": "/users"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/files/custom.json b/tests/files/custom.json
index fbfd39f..28aae84 100644
--- a/tests/files/custom.json
+++ b/tests/files/custom.json
@@ -1,152 +1,152 @@
[
- {
- "args": [
- "None"
- ],
- "defaults": {},
- "docstring": "Return all posts.",
- "endpoint": "get_posts",
- "location": {
- "filename": "%PATH%/examples/custom/blog.py",
- "line": 47
- },
- "methods": [
- "GET",
- "HEAD",
- "OPTIONS"
- ],
- "rule": "/"
- },
- {
- "args": [
- "None"
- ],
- "defaults": {},
- "docstring": "Admin interface.",
- "endpoint": "admin",
- "location": {
- "filename": "%PATH%/examples/custom/blog.py",
- "line": 96
- },
- "methods": [
- "GET",
- "HEAD",
- "OPTIONS"
- ],
- "rule": "/admin"
- },
- {
- "args": [
- "None"
- ],
- "defaults": {},
- "docstring": "Create a new post.",
- "endpoint": "post_post",
- "form_data": [
- "title",
- "content",
- "authorid"
- ],
- "location": {
- "filename": "%PATH%/examples/custom/blog.py",
- "line": 62
- },
- "methods": [
- "OPTIONS",
- "POST"
- ],
- "rule": "/post"
- },
- {
- "args": [
- "id"
- ],
- "defaults": {},
- "docstring": "Return the post for the given id.",
- "endpoint": "get_post",
- "location": {
- "filename": "%PATH%/examples/custom/blog.py",
- "line": 54
- },
- "methods": [
- "GET",
- "HEAD",
- "OPTIONS"
- ],
- "rule": "/post/"
- },
- {
- "args": [
- "None"
- ],
- "defaults": {},
- "docstring": "Return all posts.",
- "endpoint": "get_posts",
- "location": {
- "filename": "%PATH%/examples/custom/blog.py",
- "line": 47
- },
- "methods": [
- "GET",
- "HEAD",
- "OPTIONS"
- ],
- "rule": "/posts"
- },
- {
- "args": [
- "id"
- ],
- "defaults": {},
- "docstring": "Return the user for the given id.",
- "endpoint": "get_user",
- "location": {
- "filename": "%PATH%/examples/custom/blog.py",
- "line": 80
- },
- "methods": [
- "GET",
- "HEAD",
- "OPTIONS"
- ],
- "rule": "/user/"
- },
- {
- "args": [
- "None"
- ],
- "defaults": {},
- "docstring": "Return all users.",
- "endpoint": "get_users",
- "location": {
- "filename": "%PATH%/examples/custom/blog.py",
- "line": 73
- },
- "methods": [
- "GET",
- "HEAD",
- "OPTIONS"
- ],
- "rule": "/users"
- },
- {
- "args": [
- "None"
- ],
- "defaults": {},
- "docstring": "Creates a new user.",
- "endpoint": "post_user",
- "form_data": [
- "username"
- ],
- "location": {
- "filename": "%PATH%/examples/custom/blog.py",
- "line": 88
- },
- "methods": [
- "OPTIONS",
- "POST"
- ],
- "rule": "/users"
- }
-]
+ {
+ "args": [
+ "None"
+ ],
+ "defaults": {},
+ "docstring": "Return all posts.",
+ "endpoint": "get_posts",
+ "location": {
+ "filename": "%PATH%/examples/custom/blog.py",
+ "line": 47
+ },
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/"
+ },
+ {
+ "args": [
+ "None"
+ ],
+ "defaults": {},
+ "docstring": "Admin interface.",
+ "endpoint": "admin",
+ "location": {
+ "filename": "%PATH%/examples/custom/blog.py",
+ "line": 96
+ },
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/admin"
+ },
+ {
+ "args": [
+ "None"
+ ],
+ "defaults": {},
+ "docstring": "Create a new post.",
+ "endpoint": "post_post",
+ "form_data": [
+ "title",
+ "content",
+ "authorid"
+ ],
+ "location": {
+ "filename": "%PATH%/examples/custom/blog.py",
+ "line": 62
+ },
+ "methods": [
+ "OPTIONS",
+ "POST"
+ ],
+ "rule": "/post"
+ },
+ {
+ "args": [
+ "id"
+ ],
+ "defaults": {},
+ "docstring": "Return the post for the given id.",
+ "endpoint": "get_post",
+ "location": {
+ "filename": "%PATH%/examples/custom/blog.py",
+ "line": 54
+ },
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/post/"
+ },
+ {
+ "args": [
+ "None"
+ ],
+ "defaults": {},
+ "docstring": "Return all posts.",
+ "endpoint": "get_posts",
+ "location": {
+ "filename": "%PATH%/examples/custom/blog.py",
+ "line": 47
+ },
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/posts"
+ },
+ {
+ "args": [
+ "id"
+ ],
+ "defaults": {},
+ "docstring": "Return the user for the given id.",
+ "endpoint": "get_user",
+ "location": {
+ "filename": "%PATH%/examples/custom/blog.py",
+ "line": 80
+ },
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/user/"
+ },
+ {
+ "args": [
+ "None"
+ ],
+ "defaults": {},
+ "docstring": "Return all users.",
+ "endpoint": "get_users",
+ "location": {
+ "filename": "%PATH%/examples/custom/blog.py",
+ "line": 73
+ },
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/users"
+ },
+ {
+ "args": [
+ "None"
+ ],
+ "defaults": {},
+ "docstring": "Creates a new user.",
+ "endpoint": "post_user",
+ "form_data": [
+ "username"
+ ],
+ "location": {
+ "filename": "%PATH%/examples/custom/blog.py",
+ "line": 88
+ },
+ "methods": [
+ "OPTIONS",
+ "POST"
+ ],
+ "rule": "/users"
+ }
+]
\ No newline at end of file
diff --git a/tests/files/simple.json b/tests/files/simple.json
index b8bc216..70ad4cb 100644
--- a/tests/files/simple.json
+++ b/tests/files/simple.json
@@ -1 +1,185 @@
-[{"args":["None"],"defaults":{},"docstring":"Return all posts.","endpoint":"get_posts","location":{"filename":"%PATH%/examples/simple/blog.py","line":45},"methods":["GET","HEAD","OPTIONS"],"rule":"/"},{"args":["None"],"defaults":{},"docstring":"Admin interface.","endpoint":"admin","location":{"filename":"%PATH%/examples/simple/blog.py","line":113},"methods":["GET","HEAD","OPTIONS"],"rule":"/admin"},{"args":["greeting","id"],"defaults":{"greeting":"Hello"},"docstring":"Return the user for the given id.","endpoint":"greet_user","location":{"filename":"%PATH%/examples/simple/blog.py","line":96},"methods":["GET","HEAD","OPTIONS"],"rule":"/greet//user/"},{"args":["id"],"defaults":{"id":-1},"docstring":"Return the user for the given id.","endpoint":"hello_to_user","location":{"filename":"%PATH%/examples/simple/blog.py","line":86},"methods":["GET","HEAD","OPTIONS"],"rule":"/hello/user/"},{"args":["None"],"defaults":{},"docstring":"Create a new post.\n Form Data: title, content, authorid.\n ","endpoint":"post_post","location":{"filename":"%PATH%/examples/simple/blog.py","line":59},"methods":["OPTIONS","POST"],"rule":"/post"},{"args":["id"],"defaults":{},"docstring":"Return the post for the given id.","endpoint":"get_post","location":{"filename":"%PATH%/examples/simple/blog.py","line":52},"methods":["GET","HEAD","OPTIONS"],"rule":"/post/"},{"args":["None"],"defaults":{},"docstring":"Return all posts.","endpoint":"get_posts","location":{"filename":"%PATH%/examples/simple/blog.py","line":45},"methods":["GET","HEAD","OPTIONS"],"rule":"/posts"},{"args":["id"],"defaults":{},"docstring":"Return the user for the given id.","endpoint":"get_user","location":{"filename":"%PATH%/examples/simple/blog.py","line":79},"methods":["GET","HEAD","OPTIONS"],"rule":"/user/"},{"args":["None"],"defaults":{},"docstring":"Return all users.","endpoint":"get_users","location":{"filename":"%PATH%/examples/simple/blog.py","line":72},"methods":["GET","HEAD","OPTIONS"],"rule":"/users"},{"args":["None"],"defaults":{},"docstring":"Creates a new user.\n Form Data: username.\n ","endpoint":"post_user","location":{"filename":"%PATH%/examples/simple/blog.py","line":103},"methods":["OPTIONS","POST"],"rule":"/users"}]
+[
+ {
+ "args": [
+ "None"
+ ],
+ "defaults": {},
+ "docstring": "Return all posts.",
+ "endpoint": "get_posts",
+ "location": {
+ "filename": "%PATH%/examples/simple/blog.py",
+ "line": 46
+ },
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/"
+ },
+ {
+ "args": [
+ "None"
+ ],
+ "defaults": {},
+ "docstring": "Admin interface.",
+ "endpoint": "admin",
+ "location": {
+ "filename": "%PATH%/examples/simple/blog.py",
+ "line": 114
+ },
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/admin"
+ },
+ {
+ "args": [
+ "greeting",
+ "id"
+ ],
+ "defaults": {
+ "greeting": "Hello"
+ },
+ "docstring": "Return the user for the given id.",
+ "endpoint": "greet_user",
+ "location": {
+ "filename": "%PATH%/examples/simple/blog.py",
+ "line": 97
+ },
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/greet//user/"
+ },
+ {
+ "args": [
+ "id"
+ ],
+ "defaults": {
+ "id": -1
+ },
+ "docstring": "Return the user for the given id.",
+ "endpoint": "hello_to_user",
+ "location": {
+ "filename": "%PATH%/examples/simple/blog.py",
+ "line": 87
+ },
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/hello/user/"
+ },
+ {
+ "args": [
+ "None"
+ ],
+ "defaults": {},
+ "docstring": "Create a new post.\n Form Data: title, content, authorid.\n",
+ "endpoint": "post_post",
+ "location": {
+ "filename": "%PATH%/examples/simple/blog.py",
+ "line": 60
+ },
+ "methods": [
+ "OPTIONS",
+ "POST"
+ ],
+ "rule": "/post"
+ },
+ {
+ "args": [
+ "id"
+ ],
+ "defaults": {},
+ "docstring": "Return the post for the given id.",
+ "endpoint": "get_post",
+ "location": {
+ "filename": "%PATH%/examples/simple/blog.py",
+ "line": 53
+ },
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/post/"
+ },
+ {
+ "args": [
+ "None"
+ ],
+ "defaults": {},
+ "docstring": "Return all posts.",
+ "endpoint": "get_posts",
+ "location": {
+ "filename": "%PATH%/examples/simple/blog.py",
+ "line": 46
+ },
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/posts"
+ },
+ {
+ "args": [
+ "id"
+ ],
+ "defaults": {},
+ "docstring": "Return the user for the given id.",
+ "endpoint": "get_user",
+ "location": {
+ "filename": "%PATH%/examples/simple/blog.py",
+ "line": 80
+ },
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/user/"
+ },
+ {
+ "args": [
+ "None"
+ ],
+ "defaults": {},
+ "docstring": "Return all users.",
+ "endpoint": "get_users",
+ "location": {
+ "filename": "%PATH%/examples/simple/blog.py",
+ "line": 73
+ },
+ "methods": [
+ "GET",
+ "HEAD",
+ "OPTIONS"
+ ],
+ "rule": "/users"
+ },
+ {
+ "args": [
+ "None"
+ ],
+ "defaults": {},
+ "docstring": "Creates a new user.\n Form Data: username.\n",
+ "endpoint": "post_user",
+ "location": {
+ "filename": "%PATH%/examples/simple/blog.py",
+ "line": 104
+ },
+ "methods": [
+ "OPTIONS",
+ "POST"
+ ],
+ "rule": "/users"
+ }
+]
\ No newline at end of file
diff --git a/tests/files/simple_private.html b/tests/files/simple_private.html
index 9629dd9..18d5bc4 100644
--- a/tests/files/simple_private.html
+++ b/tests/files/simple_private.html
@@ -176,7 +176,7 @@
Create a new post.
Form Data: title, content, authorid.
-
+
@@ -304,7 +304,7 @@
Creates a new user.
Form Data: username.
-
+