Skip to content

Commit 75d4652

Browse files
committed
[BD-21] Sphinx extension and configuration file for setting annotations
Similar to the `featuretoggles` extension, this adds a `settings` Sphinx extension, along with a draft setting annotation configuration file.
1 parent e82dbf0 commit 75d4652

File tree

8 files changed

+227
-42
lines changed

8 files changed

+227
-42
lines changed

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ Change Log
1111

1212
.. There should always be an "Unreleased" section for changes pending release.
1313
14+
[0.8.0] - 2020-09-10
15+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16+
17+
* Add ``settings`` Sphinx extension with setting annotation configuration file
18+
1419
[0.7.0] - 2020-09-07
1520
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1621

code_annotations/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
Extensible tools for parsing annotations in codebases.
33
"""
44

5-
__version__ = '0.7.0'
5+
__version__ = '0.8.0'
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
source_path: ./
2+
report_path: reports
3+
safelist_path: .annotation_safe_list.yml
4+
annotations:
5+
setting:
6+
- ".. setting_name:":
7+
- ".. setting_default:":
8+
- ".. setting_description:":
9+
- ".. setting_warning:":
10+
extensions:
11+
python:
12+
- py
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
Sphinx extensions for generating human-readable documentation from code annotations.
3+
"""
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""
2+
Base utilities for building annotation-based Sphinx extensions.
3+
"""
4+
5+
6+
from code_annotations.base import AnnotationConfig
7+
from code_annotations.find_static import StaticSearch
8+
9+
10+
def find_annotations(source_path, config_path, group_by_key):
11+
"""
12+
Find the feature toggles as defined in the configuration file.
13+
14+
Return:
15+
toggles (dict): feature toggles indexed by name.
16+
"""
17+
config = AnnotationConfig(
18+
config_path, verbosity=0, source_path_override=source_path
19+
)
20+
results = StaticSearch(config).search()
21+
22+
toggles = {}
23+
current_entry = {}
24+
for filename, entries in results.items():
25+
for entry in entries:
26+
key = entry["annotation_token"]
27+
value = entry["annotation_data"]
28+
if key == group_by_key:
29+
toggle_name = value
30+
toggles[toggle_name] = {
31+
"filename": filename,
32+
"line_number": entry["line_number"],
33+
}
34+
current_entry = toggles[toggle_name]
35+
else:
36+
current_entry[key] = value
37+
38+
return toggles
39+
40+
41+
def quote_value(value):
42+
"""
43+
Quote a Python object if it is string-like.
44+
"""
45+
if value in ("True", "False", "None"):
46+
return str(value)
47+
try:
48+
float(value)
49+
return str(value)
50+
except ValueError:
51+
pass
52+
if isinstance(value, str):
53+
return '"{}"'.format(value)
54+
return str(value)

code_annotations/config_and_tools/sphinx/extensions/featuretoggles.py

Lines changed: 3 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55

66
import pkg_resources
77

8-
from code_annotations.base import AnnotationConfig
9-
from code_annotations.find_static import StaticSearch
108
from docutils import nodes
119
from sphinx.util.docutils import SphinxDirective
1210

11+
from .base import find_annotations, quote_value
12+
1313

1414
def find_feature_toggles(source_path):
1515
"""
@@ -22,28 +22,7 @@ def find_feature_toggles(source_path):
2222
"code_annotations",
2323
os.path.join("config_and_tools", "config", "feature_toggle_annotations.yaml"),
2424
)
25-
config = AnnotationConfig(
26-
config_path, verbosity=0, source_path_override=source_path
27-
)
28-
results = StaticSearch(config).search()
29-
30-
toggles = {}
31-
current_entry = {}
32-
for filename, entries in results.items():
33-
for entry in entries:
34-
key = entry["annotation_token"]
35-
value = entry["annotation_data"]
36-
if key == ".. toggle_name:":
37-
toggle_name = value
38-
toggles[toggle_name] = {
39-
"filename": filename,
40-
"line_number": entry["line_number"],
41-
}
42-
current_entry = toggles[toggle_name]
43-
else:
44-
current_entry[key] = value
45-
46-
return toggles
25+
return find_annotations(source_path, config_path, ".. toggle_name:")
4726

4827

4928
class FeatureToggles(SphinxDirective):
@@ -121,22 +100,6 @@ def iter_nodes(self):
121100
)
122101

123102

124-
def quote_value(value):
125-
"""
126-
Quote a Python object if it is string-like.
127-
"""
128-
if value in ("True", "False", "None"):
129-
return str(value)
130-
try:
131-
float(value)
132-
return str(value)
133-
except ValueError:
134-
pass
135-
if isinstance(value, str):
136-
return '"{}"'.format(value)
137-
return str(value)
138-
139-
140103
def setup(app):
141104
"""
142105
Declare the Sphinx extension.
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""
2+
Sphinx extension for viewing (non-toggle) setting annotations.
3+
"""
4+
import os
5+
6+
import pkg_resources
7+
8+
from docutils import nodes
9+
from docutils.parsers.rst import directives
10+
from sphinx.util.docutils import SphinxDirective
11+
12+
from .base import find_annotations, quote_value
13+
14+
15+
def find_settings(source_path):
16+
"""
17+
Find the Django settings as defined in the configuration file.
18+
19+
Return:
20+
settings (dict): Django settings indexed by name.
21+
"""
22+
config_path = pkg_resources.resource_filename(
23+
"code_annotations",
24+
os.path.join("config_and_tools", "config", "setting_annotations.yaml"),
25+
)
26+
return find_annotations(source_path, config_path, ".. setting_name:")
27+
28+
29+
class Settings(SphinxDirective):
30+
"""
31+
Sphinx directive to document Django settings in a single documentation page.
32+
33+
Use this directive as follows::
34+
35+
.. settings::
36+
:folder_path: lms/envs/common.py
37+
38+
This directive supports the following configuration parameters:
39+
40+
- ``settings_source_path``: absolute path to the repository file tree. E.g:
41+
42+
settings_source_path = os.path.join(os.path.dirname(__file__), "..", "..")
43+
44+
- ``settings_repo_url``: Github repository where the code is hosted. E.g:
45+
46+
settings_repo_url = "https://github.com/edx/myrepo"
47+
48+
- ``settings_repo_version``: current version of the git repository. E.g:
49+
50+
import git
51+
try:
52+
repo = git.Repo(search_parent_directories=True)
53+
settings_repo_version = repo.head.object.hexsha
54+
except git.InvalidGitRepositoryError:
55+
settings_repo_version = "master"
56+
"""
57+
58+
required_arguments = 0
59+
optional_arguments = 1
60+
option_spec = {"folder_path": directives.unchanged}
61+
62+
def run(self):
63+
"""
64+
Public interface of the Directive class.
65+
66+
Return:
67+
nodes (list): nodes to be appended to the resulting document.
68+
"""
69+
toggle_nodes = list(self.iter_nodes())
70+
return [nodes.section("", *toggle_nodes, ids=["settings"])]
71+
72+
def iter_nodes(self):
73+
"""
74+
Iterate on the docutils nodes generated by this directive.
75+
"""
76+
source_path = os.path.join(
77+
self.env.config.settings_source_path, self.options.get("folder_path", "")
78+
)
79+
settings = find_settings(source_path)
80+
for setting_name in sorted(settings):
81+
setting = settings[setting_name]
82+
yield nodes.title(text=setting_name)
83+
setting_default_value = setting.get(".. setting_default:", "Not defined")
84+
setting_default_node = nodes.literal(
85+
text=quote_value(setting_default_value)
86+
)
87+
yield nodes.paragraph("", "Default: ", setting_default_node)
88+
yield nodes.paragraph(
89+
"",
90+
"Source: ",
91+
nodes.reference(
92+
text="{} (line {})".format(
93+
setting["filename"], setting["line_number"]
94+
),
95+
refuri="{}/blob/{}/{}#L{}".format(
96+
self.env.config.settings_repo_url,
97+
self.env.config.settings_repo_version,
98+
setting["filename"],
99+
setting["line_number"],
100+
),
101+
),
102+
)
103+
yield nodes.paragraph(text=setting.get(".. setting_description:", ""))
104+
if setting.get(".. setting_warning:") not in (None, "None", "n/a", "N/A"):
105+
yield nodes.warning(
106+
"", nodes.paragraph("", setting[".. setting_warning:"])
107+
)
108+
109+
110+
def setup(app):
111+
"""
112+
Declare the Sphinx extension.
113+
"""
114+
app.add_config_value(
115+
"settings_source_path", os.path.abspath(".."), "env",
116+
)
117+
app.add_config_value("settings_repo_url", "", "env")
118+
app.add_config_value("settings_repo_version", "master", "env")
119+
app.add_directive("settings", Settings)
120+
121+
return {
122+
"version": "0.1",
123+
"parallel_read_safe": True,
124+
"parallel_write_safe": True,
125+
}

docs/sphinx_extensions.rst

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
Sphinx extensions
22
-----------------
33

4+
This package can be used to document a couple things in your code case. The following Sphinx extensions will help you generate human-readable docs by parsing the source code, instead of importing it. This allows you to document your code base in any language, without having to install all its dependencies.
5+
6+
.. _sphinx_featuretoggles:
7+
48
``featuretoggles``
59
==================
610

7-
This package can be used to document the feature toggles in your code base. To do so,
11+
Feature toggles are an Open edX mechanism by which features can be individually enabled or disabled. To document these feature toggles,
812
add the following to your ``conf.py``::
913

1014
extensions = ["code_annotations.config_and_tools.sphinx.extensions.featuretoggles"]
@@ -20,3 +24,22 @@ add the following to your ``conf.py``::
2024
Then, in an ``.rst`` file::
2125

2226
.. featuretoggles::
27+
28+
.. _sphinx_settings:
29+
30+
``settings``
31+
============
32+
33+
This package also comes with tooling to annotate Django settings. Settings that should be annotated include non-standard Django settings, and settings that do not correspond to feature toggles (in which case feature toggle annotations should be used instead). Similar to feature toggles, Django setting annotations can also be parsed to generate human-readable documentation. Add the following to your ``conf.py``::
34+
35+
extensions = ["code_annotations.config_and_tools.sphinx.extensions.settings"]
36+
37+
Define the following variables, just like for the :ref:`featuretoggles <sphinx_featuretoggles>` extension::
38+
39+
settings_source_path = ...
40+
settings_repo_url = ...
41+
42+
Then, in an ``.rst`` file::
43+
44+
.. settings::
45+
:folder_path: path/to/settings.py

0 commit comments

Comments
 (0)