Skip to content

Commit 90dbf1c

Browse files
committed
[BD-21] Add featuretoggle configuration and sphinx extension
We introduce a new Sphinx extension for generating human-readable documentation of feature toggle annotations. This extension introduces a `.. featuretoggles::` directive that will list feature toggles for the configured code base. Annotations are collected on-the-fly using the feature toggle annotation configuration file.
1 parent 4b95c8d commit 90dbf1c

File tree

6 files changed

+206
-3
lines changed

6 files changed

+206
-3
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ Change Log
1111

1212
.. There should always be an "Unreleased" section for changes pending release.
1313
14-
Unreleased
15-
~~~~~~~~~~
14+
[0.5.0] - 2020-08-06
15+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1616

17+
* Add ``featuretoggles`` Sphinx extension
1718
* Include ``config_and_tools`` folder in pip-installable package
1819
* Add ADR 0001-config-and-tools.rst for adding a placing in this repository for shared annotation configs and supporting tools.
1920

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.4.0'
5+
__version__ = '0.5.0'
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# This code-annotations configuration file supports OEP-17, Feature Toggles.
2+
3+
source_path: ./
4+
report_path: reports
5+
safelist_path: .annotation_safe_list.yml
6+
coverage_target: 50.0
7+
annotations:
8+
documented_elsewhere:
9+
- ".. documented_elsewhere:":
10+
- ".. documented_elsewhere_name:":
11+
feature_toggle:
12+
- ".. toggle_name:":
13+
- ".. toggle_implementation:":
14+
choices: [ExperimentWaffleFlag, WaffleFlag, WaffleSample, WaffleSwitch, CourseWaffleFlag, ConfigurationModel, DjangoSetting]
15+
- ".. toggle_default:":
16+
- ".. toggle_description:":
17+
- ".. toggle_category:":
18+
- ".. toggle_use_cases:":
19+
choices: [incremental_release, launch_date, monitored_rollout, graceful_degradation, beta_testing, vip, opt_out, opt_in, open_edx]
20+
- ".. toggle_creation_date:":
21+
- ".. toggle_expiration_date:":
22+
- ".. toggle_warnings:":
23+
- ".. toggle_tickets:":
24+
- ".. toggle_status:":
25+
extensions:
26+
python:
27+
- py
28+
rst_template: doc.rst.j2
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""
2+
Sphinx extension for viewing feature toggle annotations.
3+
"""
4+
import os
5+
6+
import pkg_resources
7+
8+
from code_annotations.base import AnnotationConfig
9+
from code_annotations.find_static import StaticSearch
10+
from docutils import nodes
11+
from sphinx.util.docutils import SphinxDirective
12+
13+
14+
def find_feature_toggles(source_path):
15+
"""
16+
Find the feature toggles as defined in the configuration file.
17+
18+
Return:
19+
toggles (dict): feature toggles indexed by name.
20+
"""
21+
config_path = pkg_resources.resource_filename(
22+
"code_annotations",
23+
os.path.join("config_and_tools", "config", "feature_toggle_annotations.yaml"),
24+
)
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
47+
48+
49+
class FeatureToggles(SphinxDirective):
50+
"""
51+
Sphinx directive to list the feature toggles in a single documentation page.
52+
53+
Use this directive as follows::
54+
55+
.. featuretoggles::
56+
57+
This directive supports the following configuration parameters:
58+
59+
- ``featuretoggles_source_path``: absolute path to the repository file tree. E.g:
60+
61+
featuretoggles_source_path = os.path.join(os.path.dirname(__file__), "..", "..")
62+
63+
- ``featuretoggles_repo_url``: Github repository where the code is hosted. E.g:
64+
65+
featuretoggles_repo_url = "https://github.com/edx/myrepo"
66+
67+
- ``featuretoggles_repo_version``: current version of the git repository. E.g:
68+
69+
import git
70+
try:
71+
repo = git.Repo(search_parent_directories=True)
72+
featuretoggles_repo_version = repo.head.object.hexsha
73+
except git.InvalidGitRepositoryError:
74+
featuretoggles_repo_version = "master"
75+
"""
76+
77+
required_arguments = 0
78+
optional_arguments = 0
79+
option_spec = {}
80+
81+
def run(self):
82+
"""
83+
Public interface of the Directive class.
84+
85+
Return:
86+
nodes (list): nodes to be appended to the resulting document.
87+
"""
88+
toggle_nodes = list(self.iter_nodes())
89+
return [nodes.section("", *toggle_nodes, ids=["featuretoggles"])]
90+
91+
def iter_nodes(self):
92+
"""
93+
Iterate on the docutils nodes generated by this directive.
94+
"""
95+
toggles = find_feature_toggles(self.env.config.featuretoggles_source_path)
96+
for toggle_name in sorted(toggles):
97+
toggle = toggles[toggle_name]
98+
yield nodes.title(text=toggle_name)
99+
toggle_default_value = toggle.get(".. toggle_default:", "Not defined")
100+
toggle_default_node = nodes.literal(text=quote_value(toggle_default_value))
101+
yield nodes.paragraph("", "Default: ", toggle_default_node)
102+
yield nodes.paragraph(
103+
"",
104+
"Source: ",
105+
nodes.reference(
106+
text="{} (line {})".format(
107+
toggle["filename"], toggle["line_number"]
108+
),
109+
refuri="{}/blob/{}/{}#L{}".format(
110+
self.env.config.featuretoggles_repo_url,
111+
self.env.config.featuretoggles_repo_version,
112+
toggle["filename"],
113+
toggle["line_number"],
114+
),
115+
),
116+
)
117+
yield nodes.paragraph(text=toggle.get(".. toggle_description:", ""))
118+
119+
120+
def quote_value(value):
121+
"""
122+
Quote a Python object if it is string-like.
123+
"""
124+
if value in ("True", "False", "None"):
125+
return str(value)
126+
try:
127+
float(value)
128+
return str(value)
129+
except ValueError:
130+
pass
131+
if isinstance(value, str):
132+
return '"{}"'.format(value)
133+
return str(value)
134+
135+
136+
def setup(app):
137+
"""
138+
Declare the Sphinx extension.
139+
"""
140+
app.add_config_value(
141+
"featuretoggles_source_path", os.path.abspath(".."), "env",
142+
)
143+
app.add_config_value("featuretoggles_repo_url", "", "env")
144+
app.add_config_value("featuretoggles_repo_version", "master", "env")
145+
app.add_directive("featuretoggles", FeatureToggles)
146+
147+
return {
148+
"version": "0.1",
149+
"parallel_read_safe": True,
150+
"parallel_write_safe": True,
151+
}

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Contents:
1818
static_search
1919
django_search
2020
configuration
21+
sphinx_extensions
2122
extensions
2223
testing
2324
modules

docs/sphinx_extensions.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Sphinx extensions
2+
-----------------
3+
4+
``featuretoggles``
5+
==================
6+
7+
This package can be used to document the feature toggles in your code base. To do so,
8+
add the following to your ``conf.py``::
9+
10+
extensions = ["code_annotations.config_and_tools.sphinx.extensions.featuretoggles"]
11+
featuretoggles_source_path = os.path.abspath(
12+
os.path.join(os.path.dirname(__file__), "..")
13+
)
14+
featuretoggles_repo_url = "https://github.com/edx/yourrepo"
15+
try:
16+
featuretoggles_repo_version = git.Repo(search_parent_directories=True).head.object.hexsha
17+
except git.InvalidGitRepositoryError:
18+
pass
19+
20+
Then, in an ``.rst`` file::
21+
22+
.. featuretoggles::

0 commit comments

Comments
 (0)