Skip to content

Commit fe2075b

Browse files
authored
Merge pull request #8848 from webknjaz/features/8783--sphinx-ext-admonitions
2 parents ccc84a6 + 13962cd commit fe2075b

File tree

6 files changed

+198
-3
lines changed

6 files changed

+198
-3
lines changed

MANIFEST.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ exclude noxfile.py
2222

2323
recursive-include src/pip/_vendor *.pem
2424
recursive-include src/pip/_vendor py.typed
25-
recursive-include docs Makefile *.rst *.py *.bat
25+
recursive-include docs *.css *.rst *.py
2626

2727
exclude src/pip/_vendor/six
2828
exclude src/pip/_vendor/six/moves

docs/docs_feedback_sphinxext.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
"""A sphinx extension for collecting per doc feedback."""
2+
3+
from __future__ import annotations
4+
5+
from itertools import chain
6+
from typing import TYPE_CHECKING
7+
8+
if TYPE_CHECKING:
9+
from typing import Dict, List, Union
10+
11+
from sphinx.application import Sphinx
12+
13+
14+
DEFAULT_DOC_LINES_THRESHOLD = 250
15+
RST_INDENT = 4
16+
EMAIL_INDENT = 6
17+
18+
19+
def _modify_rst_document_source_on_read(
20+
app: Sphinx,
21+
docname: str,
22+
source: List[str],
23+
) -> None:
24+
"""Add info block to top and bottom of each document source.
25+
26+
This function modifies RST source in-place by adding an admonition
27+
block at the top and the bottom of each document right after it's
28+
been read from disk preserving :orphan: at top, if present.
29+
"""
30+
admonition_type = app.config.docs_feedback_admonition_type
31+
big_doc_lines = app.config.docs_feedback_big_doc_lines
32+
escaped_email = app.config.docs_feedback_email.replace(' ', r'\ ')
33+
excluded_documents = set(app.config.docs_feedback_excluded_documents)
34+
questions_list = app.config.docs_feedback_questions_list
35+
36+
valid_admonitions = {
37+
'attention', 'caution', 'danger', 'error', 'hint',
38+
'important', 'note', 'tip', 'warning', 'admonition',
39+
}
40+
41+
if admonition_type not in valid_admonitions:
42+
raise ValueError(
43+
'Expected `docs_feedback_admonition_type` to be one of '
44+
f'{valid_admonitions} but got {admonition_type}.'
45+
)
46+
47+
if not questions_list:
48+
raise ValueError(
49+
'Expected `docs_feedback_questions_list` to list questions '
50+
'but got none.'
51+
)
52+
53+
if docname in excluded_documents:
54+
# NOTE: Completely ignore any document
55+
# NOTE: listed in 'docs_feedback_excluded_documents'.
56+
return
57+
58+
is_doc_big = source[0].count('\n') >= big_doc_lines
59+
60+
questions_list_rst = '\n'.join(
61+
f'{" " * RST_INDENT}{number!s}. {question}'
62+
for number, question in enumerate(questions_list, 1)
63+
)
64+
questions_list_urlencoded = (
65+
'\n'.join(
66+
f'\n{" " * RST_INDENT}{number!s}. {question} '
67+
for number, question in enumerate(
68+
chain(
69+
(f'Document: {docname}. Page URL: https://', ),
70+
questions_list,
71+
),
72+
)
73+
).
74+
rstrip('\r\n\t ').
75+
replace('\r', '%0D').
76+
replace('\n', '%0A').
77+
replace(' ', '%20')
78+
)
79+
80+
admonition_msg = rf"""
81+
**Did this article help?**
82+
83+
We are currently doing research to improve pip's documentation
84+
and would love your feedback.
85+
Please `email us`_ and let us know{{let_us_know_ending}}
86+
87+
{{questions_list_rst}}
88+
89+
.. _email us:
90+
mailto:{escaped_email}\
91+
?subject=[Doc:\ {docname}]\ Pip\ docs\ feedback\ \
92+
(URL\:\ https\://)\
93+
&body={questions_list_urlencoded}
94+
"""
95+
let_us_know_ending = ':'
96+
97+
info_block_bottom = (
98+
f'.. {admonition_type}::\n\t\t{admonition_msg.format_map(locals())}\n'
99+
)
100+
101+
questions_list_rst = ''
102+
let_us_know_ending = (
103+
' why you came to this page and what on it helped '
104+
'you and what did not. '
105+
'(:issue:`Read more about this research <8517>`)'
106+
)
107+
info_block_top = '' if is_doc_big else (
108+
f'.. {admonition_type}::\n\t\t{admonition_msg.format_map(locals())}\n'
109+
)
110+
111+
orphan_mark = ':orphan:'
112+
is_orphan = orphan_mark in source[0]
113+
if is_orphan:
114+
source[0].replace(orphan_mark, '')
115+
else:
116+
orphan_mark = ''
117+
118+
source[0] = '\n\n'.join((
119+
orphan_mark, info_block_top, source[0], info_block_bottom,
120+
))
121+
122+
123+
def setup(app: Sphinx) -> Dict[str, Union[bool, str]]:
124+
"""Initialize the Sphinx extension.
125+
126+
This function adds a callback for modifying the document sources
127+
in-place on read.
128+
129+
It also declares the extension settings changable via :file:`conf.py`.
130+
"""
131+
rebuild_trigger = 'html' # rebuild full html on settings change
132+
app.add_config_value(
133+
'docs_feedback_admonition_type',
134+
default='important',
135+
rebuild=rebuild_trigger,
136+
)
137+
app.add_config_value(
138+
'docs_feedback_big_doc_lines',
139+
default=DEFAULT_DOC_LINES_THRESHOLD,
140+
rebuild=rebuild_trigger,
141+
)
142+
app.add_config_value(
143+
'docs_feedback_email',
144+
default='Docs UX Team <docs-feedback+ux/[email protected]>',
145+
rebuild=rebuild_trigger,
146+
)
147+
app.add_config_value(
148+
'docs_feedback_excluded_documents',
149+
default=set(),
150+
rebuild=rebuild_trigger,
151+
)
152+
app.add_config_value(
153+
'docs_feedback_questions_list',
154+
default=(),
155+
rebuild=rebuild_trigger,
156+
)
157+
158+
app.add_css_file('important-admonition.css')
159+
app.connect('source-read', _modify_rst_document_source_on_read)
160+
161+
return {
162+
'parallel_read_safe': True,
163+
'parallel_write_safe': True,
164+
'version': 'builtin',
165+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.admonition.important {
2+
background-color: rgb(219, 250, 244);
3+
border: 1px solid rgb(26, 188, 156);
4+
}
5+
6+
.admonition.important>.admonition-title {
7+
color: rgb(26, 188, 156);
8+
}

docs/html/conf.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,14 @@
3131
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
3232
# extensions = ['sphinx.ext.autodoc']
3333
extensions = [
34+
# native:
3435
'sphinx.ext.extlinks',
35-
'pip_sphinxext',
3636
'sphinx.ext.intersphinx',
37+
# third-party:
3738
'sphinx_tabs.tabs',
39+
# in-tree:
40+
'docs_feedback_sphinxext',
41+
'pip_sphinxext',
3842
]
3943

4044
# intersphinx
@@ -177,7 +181,7 @@
177181
# Add any paths that contain custom static files (such as style sheets) here,
178182
# relative to this directory. They are copied after the builtin static files,
179183
# so a file named "default.css" will overwrite the builtin "default.css".
180-
html_static_path = []
184+
html_static_path = ['_static']
181185

182186
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
183187
# using the given strftime format.
@@ -308,3 +312,19 @@ def to_document_name(path, base_dir):
308312
)
309313

310314
man_pages.append((fname_base, outname, description, u'pip developers', 1))
315+
316+
# -- Options for docs_feedback_sphinxext --------------------------------------
317+
318+
# NOTE: Must be one of 'attention', 'caution', 'danger', 'error', 'hint',
319+
# NOTE: 'important', 'note', 'tip', 'warning' or 'admonition'.
320+
docs_feedback_admonition_type = 'important'
321+
docs_feedback_big_doc_lines = 50 # bigger docs will have a banner on top
322+
docs_feedback_email = 'Docs UX Team <docs-feedback+ux/[email protected]>'
323+
docs_feedback_excluded_documents = { # these won't have any banners
324+
'news',
325+
}
326+
docs_feedback_questions_list = (
327+
'What problem were you trying to solve when you came to this page?',
328+
'What content was useful?',
329+
'What content was not useful?',
330+
)

news/8783.doc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added initial UX feedback widgets to docs.

news/8848.doc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
8783.doc

0 commit comments

Comments
 (0)