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 1/5] 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(' Create a new post.
\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 2/5] 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 3/5] 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 4/5] 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/
Form Data: title, content, authorid.
-
Creates a new user.
Form Data: username.
-