code-annotations 1.8.0__tar.gz → 1.8.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {code-annotations-1.8.0 → code-annotations-1.8.2}/CHANGELOG.rst +6 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/PKG-INFO +8 -2
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/__init__.py +1 -1
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/base.py +2 -2
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/cli.py +114 -65
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/find_django.py +169 -102
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/generate_docs.py +1 -1
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations.egg-info/PKG-INFO +8 -2
- {code-annotations-1.8.0 → code-annotations-1.8.2}/tests/test_django_generate_safelist.py +2 -2
- {code-annotations-1.8.0 → code-annotations-1.8.2}/tests/test_find_django.py +17 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/LICENSE.txt +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/MANIFEST.in +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/NOTICE.txt +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/README.rst +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/annotation_errors.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/config/__init__.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/config/feature_toggle_annotations.yaml +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/config/openedx_events_annotations.yaml +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/config/setting_annotations.yaml +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/sphinx/extensions/__init__.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/sphinx/extensions/base.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/sphinx/extensions/featuretoggles.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/sphinx/extensions/openedx_events.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/sphinx/extensions/settings.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/exceptions.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/extensions/__init__.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/extensions/base.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/extensions/javascript.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/extensions/python.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/find_static.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/helpers.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations.egg-info/SOURCES.txt +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations.egg-info/dependency_links.txt +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations.egg-info/entry_points.txt +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations.egg-info/not-zip-safe +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations.egg-info/requires.txt +1 -1
- {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations.egg-info/top_level.txt +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/requirements/base.in +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/setup.cfg +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/setup.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/tests/test_base.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/tests/test_django_coverage.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/tests/test_django_list_local_models.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/tests/test_find_static.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/tests/test_generate_docs.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/tests/test_search.py +0 -0
- {code-annotations-1.8.0 → code-annotations-1.8.2}/tests/test_sphinx.py +0 -0
|
@@ -14,6 +14,12 @@ Change Log
|
|
|
14
14
|
Unreleased
|
|
15
15
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
16
16
|
|
|
17
|
+
[1.8.1] - 2024-07-11
|
|
18
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
19
|
+
|
|
20
|
+
* Fix elapsed-time calculations to always use UTC. Other clocks can be altered partway through by Django config settings being loaded while the timer is running, resulting in reporting elapsed time of "-17999.895582 seconds" or similar.
|
|
21
|
+
* Fix report filename to use year-month-day order, not year-day-month. (Also more compact, now.)
|
|
22
|
+
|
|
17
23
|
[1.8.0] - 2024-03-31
|
|
18
24
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
19
25
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: code-annotations
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.2
|
|
4
4
|
Summary: Extensible tools for parsing annotations in codebases
|
|
5
5
|
Home-page: https://github.com/openedx/code-annotations
|
|
6
6
|
Author: edX
|
|
@@ -22,10 +22,10 @@ Description-Content-Type: text/x-rst
|
|
|
22
22
|
License-File: LICENSE.txt
|
|
23
23
|
License-File: NOTICE.txt
|
|
24
24
|
Requires-Dist: pyyaml
|
|
25
|
+
Requires-Dist: Jinja2
|
|
25
26
|
Requires-Dist: stevedore
|
|
26
27
|
Requires-Dist: python-slugify
|
|
27
28
|
Requires-Dist: click
|
|
28
|
-
Requires-Dist: Jinja2
|
|
29
29
|
Provides-Extra: django
|
|
30
30
|
Requires-Dist: Django<2.3,>=2.2; extra == "django"
|
|
31
31
|
|
|
@@ -129,6 +129,12 @@ Change Log
|
|
|
129
129
|
Unreleased
|
|
130
130
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
131
131
|
|
|
132
|
+
[1.8.1] - 2024-07-11
|
|
133
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
134
|
+
|
|
135
|
+
* Fix elapsed-time calculations to always use UTC. Other clocks can be altered partway through by Django config settings being loaded while the timer is running, resulting in reporting elapsed time of "-17999.895582 seconds" or similar.
|
|
136
|
+
* Fix report filename to use year-month-day order, not year-day-month. (Also more compact, now.)
|
|
137
|
+
|
|
132
138
|
[1.8.0] - 2024-03-31
|
|
133
139
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
134
140
|
|
|
@@ -616,9 +616,9 @@ class BaseSearch(metaclass=ABCMeta):
|
|
|
616
616
|
"""
|
|
617
617
|
self.echo.echo_vv(yaml.dump(all_results, default_flow_style=False))
|
|
618
618
|
|
|
619
|
-
now = datetime.datetime.
|
|
619
|
+
now = datetime.datetime.utcnow()
|
|
620
620
|
report_filename = os.path.join(self.config.report_path, '{}{}.yaml'.format(
|
|
621
|
-
report_prefix, now.strftime('%Y
|
|
621
|
+
report_prefix, now.strftime('%Y%m%d-%H%M%S')
|
|
622
622
|
))
|
|
623
623
|
|
|
624
624
|
formatted_results = self._format_results_for_report(all_results)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Command line interface for code annotation tools.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import datetime
|
|
5
6
|
import sys
|
|
6
7
|
import traceback
|
|
@@ -21,53 +22,88 @@ def entry_point():
|
|
|
21
22
|
"""
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
@entry_point.command(
|
|
25
|
+
@entry_point.command("django_find_annotations")
|
|
26
|
+
@click.option(
|
|
27
|
+
"--config_file",
|
|
28
|
+
default=".annotations",
|
|
29
|
+
help="Path to the configuration file",
|
|
30
|
+
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
|
|
31
|
+
)
|
|
32
|
+
@click.option(
|
|
33
|
+
"--seed_safelist/--no_safelist",
|
|
34
|
+
default=False,
|
|
35
|
+
show_default=True,
|
|
36
|
+
help="Generate an initial safelist file based on the current Django environment.",
|
|
37
|
+
)
|
|
38
|
+
@click.option(
|
|
39
|
+
"--list_local_models/--no_list_models",
|
|
40
|
+
default=False,
|
|
41
|
+
show_default=True,
|
|
42
|
+
help="List all locally defined models (in the current repo) that require annotations.",
|
|
43
|
+
)
|
|
44
|
+
@click.option(
|
|
45
|
+
"--app_name",
|
|
46
|
+
default=None,
|
|
47
|
+
help="(Optional) App name for which coverage is generated.",
|
|
48
|
+
)
|
|
49
|
+
@click.option("--report_path", default=None, help="Location to write the report")
|
|
50
|
+
@click.option("-v", "--verbosity", count=True, help="Verbosity level (-v through -vvv)")
|
|
25
51
|
@click.option(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
52
|
+
"--lint/--no_lint",
|
|
53
|
+
help="Enable or disable linting checks",
|
|
54
|
+
default=False,
|
|
55
|
+
show_default=True,
|
|
30
56
|
)
|
|
31
57
|
@click.option(
|
|
32
|
-
|
|
58
|
+
"--report/--no_report",
|
|
59
|
+
help="Enable or disable writing the report",
|
|
33
60
|
default=False,
|
|
34
61
|
show_default=True,
|
|
35
|
-
help='Generate an initial safelist file based on the current Django environment.',
|
|
36
62
|
)
|
|
37
63
|
@click.option(
|
|
38
|
-
|
|
64
|
+
"--coverage/--no_coverage",
|
|
65
|
+
help="Enable or disable coverage checks",
|
|
39
66
|
default=False,
|
|
40
67
|
show_default=True,
|
|
41
|
-
help='List all locally defined models (in the current repo) that require annotations.',
|
|
42
68
|
)
|
|
43
|
-
@click.option('--app_name', default='', help='(Optional) App name for which coverage is generated.')
|
|
44
|
-
@click.option('--report_path', default=None, help='Location to write the report')
|
|
45
|
-
@click.option('-v', '--verbosity', count=True, help='Verbosity level (-v through -vvv)')
|
|
46
|
-
@click.option('--lint/--no_lint', help='Enable or disable linting checks', default=False, show_default=True)
|
|
47
|
-
@click.option('--report/--no_report', help='Enable or disable writing the report', default=False, show_default=True)
|
|
48
|
-
@click.option('--coverage/--no_coverage', help='Enable or disable coverage checks', default=False, show_default=True)
|
|
49
69
|
def django_find_annotations(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
config_file,
|
|
71
|
+
seed_safelist,
|
|
72
|
+
list_local_models,
|
|
73
|
+
app_name,
|
|
74
|
+
report_path,
|
|
75
|
+
verbosity,
|
|
76
|
+
lint,
|
|
77
|
+
report,
|
|
78
|
+
coverage,
|
|
59
79
|
):
|
|
60
80
|
"""
|
|
61
81
|
Subcommand for dealing with annotations in Django models.
|
|
62
82
|
"""
|
|
63
83
|
try:
|
|
64
|
-
start_time = datetime.datetime.
|
|
84
|
+
start_time = datetime.datetime.utcnow()
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
not coverage
|
|
88
|
+
and not seed_safelist
|
|
89
|
+
and not list_local_models
|
|
90
|
+
and not lint
|
|
91
|
+
and not report
|
|
92
|
+
):
|
|
93
|
+
click.echo(
|
|
94
|
+
"No actions specified. Please specify one or more of --seed_safelist, --list_local_models, "
|
|
95
|
+
"--lint, --report, or --coverage"
|
|
96
|
+
)
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
|
|
65
99
|
config = AnnotationConfig(config_file, report_path, verbosity)
|
|
66
|
-
searcher = DjangoSearch(config)
|
|
100
|
+
searcher = DjangoSearch(config, app_name)
|
|
67
101
|
|
|
68
102
|
# Early out if we're trying to do coverage, but a coverage target is not configured
|
|
69
103
|
if coverage and not config.coverage_target:
|
|
70
|
-
raise ConfigurationException(
|
|
104
|
+
raise ConfigurationException(
|
|
105
|
+
"Please add 'coverage_target' to your configuration before running --coverage"
|
|
106
|
+
)
|
|
71
107
|
|
|
72
108
|
if seed_safelist:
|
|
73
109
|
searcher.seed_safelist()
|
|
@@ -106,38 +142,51 @@ def django_find_annotations(
|
|
|
106
142
|
for filename in annotated_models:
|
|
107
143
|
annotation_count += len(annotated_models[filename])
|
|
108
144
|
|
|
109
|
-
elapsed = datetime.datetime.
|
|
110
|
-
click.echo(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
145
|
+
elapsed = datetime.datetime.utcnow() - start_time
|
|
146
|
+
click.echo(
|
|
147
|
+
"Search found {} annotations in {} seconds.".format(
|
|
148
|
+
annotation_count, elapsed.total_seconds()
|
|
149
|
+
)
|
|
150
|
+
)
|
|
114
151
|
except Exception as exc:
|
|
115
152
|
click.echo(traceback.print_exc())
|
|
116
153
|
fail(str(exc))
|
|
117
154
|
|
|
118
155
|
|
|
119
|
-
@entry_point.command(
|
|
156
|
+
@entry_point.command("static_find_annotations")
|
|
120
157
|
@click.option(
|
|
121
|
-
|
|
122
|
-
default=
|
|
123
|
-
help=
|
|
124
|
-
type=click.Path(exists=True, dir_okay=False, resolve_path=True)
|
|
158
|
+
"--config_file",
|
|
159
|
+
default=".annotations",
|
|
160
|
+
help="Path to the configuration file",
|
|
161
|
+
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
|
|
125
162
|
)
|
|
126
163
|
@click.option(
|
|
127
|
-
|
|
128
|
-
help=
|
|
129
|
-
type=click.Path(exists=True, dir_okay=True, resolve_path=True)
|
|
164
|
+
"--source_path",
|
|
165
|
+
help="Location of the source code to search",
|
|
166
|
+
type=click.Path(exists=True, dir_okay=True, resolve_path=True),
|
|
130
167
|
)
|
|
131
|
-
@click.option(
|
|
132
|
-
@click.option(
|
|
133
|
-
@click.option(
|
|
134
|
-
|
|
135
|
-
|
|
168
|
+
@click.option("--report_path", default=None, help="Location to write the report")
|
|
169
|
+
@click.option("-v", "--verbosity", count=True, help="Verbosity level (-v through -vvv)")
|
|
170
|
+
@click.option(
|
|
171
|
+
"--lint/--no_lint",
|
|
172
|
+
help="Enable or disable linting checks",
|
|
173
|
+
default=True,
|
|
174
|
+
show_default=True,
|
|
175
|
+
)
|
|
176
|
+
@click.option(
|
|
177
|
+
"--report/--no_report",
|
|
178
|
+
help="Enable or disable writing the report file",
|
|
179
|
+
default=True,
|
|
180
|
+
show_default=True,
|
|
181
|
+
)
|
|
182
|
+
def static_find_annotations(
|
|
183
|
+
config_file, source_path, report_path, verbosity, lint, report
|
|
184
|
+
):
|
|
136
185
|
"""
|
|
137
186
|
Subcommand to find annotations via static file analysis.
|
|
138
187
|
"""
|
|
139
188
|
try:
|
|
140
|
-
start_time = datetime.datetime.
|
|
189
|
+
start_time = datetime.datetime.utcnow()
|
|
141
190
|
config = AnnotationConfig(config_file, report_path, verbosity, source_path)
|
|
142
191
|
searcher = StaticSearch(config)
|
|
143
192
|
all_results = searcher.search()
|
|
@@ -161,7 +210,7 @@ def static_find_annotations(config_file, source_path, report_path, verbosity, li
|
|
|
161
210
|
report_filename = searcher.report(all_results)
|
|
162
211
|
click.echo(f"Report written to {report_filename}.")
|
|
163
212
|
|
|
164
|
-
elapsed = datetime.datetime.
|
|
213
|
+
elapsed = datetime.datetime.utcnow() - start_time
|
|
165
214
|
annotation_count = 0
|
|
166
215
|
|
|
167
216
|
for filename in all_results:
|
|
@@ -176,41 +225,41 @@ def static_find_annotations(config_file, source_path, report_path, verbosity, li
|
|
|
176
225
|
|
|
177
226
|
@entry_point.command("generate_docs")
|
|
178
227
|
@click.option(
|
|
179
|
-
|
|
180
|
-
default=
|
|
181
|
-
help=
|
|
182
|
-
type=click.Path(exists=True, dir_okay=False)
|
|
228
|
+
"--config_file",
|
|
229
|
+
default=".annotations",
|
|
230
|
+
help="Path to the configuration file",
|
|
231
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
183
232
|
)
|
|
184
|
-
@click.option(
|
|
185
|
-
@click.argument("report_files", type=click.File(
|
|
186
|
-
def generate_docs(
|
|
187
|
-
config_file,
|
|
188
|
-
verbosity,
|
|
189
|
-
report_files
|
|
190
|
-
):
|
|
233
|
+
@click.option("-v", "--verbosity", count=True, help="Verbosity level (-v through -vvv)")
|
|
234
|
+
@click.argument("report_files", type=click.File("r"), nargs=-1)
|
|
235
|
+
def generate_docs(config_file, verbosity, report_files):
|
|
191
236
|
"""
|
|
192
237
|
Generate documentation from a code annotations report.
|
|
193
238
|
"""
|
|
194
|
-
start_time = datetime.datetime.
|
|
239
|
+
start_time = datetime.datetime.utcnow()
|
|
195
240
|
|
|
196
241
|
try:
|
|
197
242
|
config = AnnotationConfig(config_file, verbosity)
|
|
198
243
|
|
|
199
244
|
for key in (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
245
|
+
"report_template_dir",
|
|
246
|
+
"rendered_report_dir",
|
|
247
|
+
"rendered_report_file_extension",
|
|
248
|
+
"rendered_report_source_link_prefix",
|
|
204
249
|
):
|
|
205
250
|
if not getattr(config, key):
|
|
206
251
|
raise ConfigurationException(f"No {key} key in {config_file}")
|
|
207
252
|
|
|
208
|
-
config.echo(
|
|
253
|
+
config.echo(
|
|
254
|
+
"Rendering the following reports: \n{}".format(
|
|
255
|
+
"\n".join([r.name for r in report_files])
|
|
256
|
+
)
|
|
257
|
+
)
|
|
209
258
|
|
|
210
259
|
renderer = ReportRenderer(config, report_files)
|
|
211
260
|
renderer.render()
|
|
212
261
|
|
|
213
|
-
elapsed = datetime.datetime.
|
|
262
|
+
elapsed = datetime.datetime.utcnow() - start_time
|
|
214
263
|
click.echo(f"Report rendered in {elapsed.total_seconds()} seconds.")
|
|
215
264
|
except Exception as exc:
|
|
216
265
|
click.echo(traceback.print_exc())
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Annotation searcher for Django model comment searching Django introspection.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
import inspect
|
|
5
6
|
import os
|
|
6
7
|
import sys
|
|
@@ -13,7 +14,7 @@ from django.db import models
|
|
|
13
14
|
from code_annotations.base import BaseSearch
|
|
14
15
|
from code_annotations.helpers import clean_annotation, fail, get_annotation_regex
|
|
15
16
|
|
|
16
|
-
DEFAULT_SAFELIST_FILE_PATH =
|
|
17
|
+
DEFAULT_SAFELIST_FILE_PATH = ".annotation_safe_list.yml"
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class DjangoSearch(BaseSearch):
|
|
@@ -21,27 +22,38 @@ class DjangoSearch(BaseSearch):
|
|
|
21
22
|
Handles Django model comment searching for annotations.
|
|
22
23
|
"""
|
|
23
24
|
|
|
24
|
-
def __init__(self, config):
|
|
25
|
+
def __init__(self, config, app_name=None):
|
|
25
26
|
"""
|
|
26
27
|
Initialize for DjangoSearch.
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
config: Configuration file path
|
|
30
28
|
"""
|
|
31
29
|
super().__init__(config)
|
|
32
|
-
self.local_models, self.non_local_models, total,
|
|
30
|
+
self.local_models, self.non_local_models, total, annotation_eligible = (
|
|
31
|
+
self.get_models_requiring_annotations(app_name)
|
|
32
|
+
)
|
|
33
33
|
self.model_counts = {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
"total": total,
|
|
35
|
+
"annotated": 0,
|
|
36
|
+
"unannotated": 0,
|
|
37
|
+
"annotation_eligible": len(annotation_eligible),
|
|
38
|
+
"not_annotation_eligible": total - len(annotation_eligible),
|
|
39
|
+
"safelisted": 0,
|
|
40
40
|
}
|
|
41
41
|
self.uncovered_model_ids = set()
|
|
42
|
-
self.echo.echo_vvv(
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
self.echo.echo_vvv(
|
|
43
|
+
"Local models:\n "
|
|
44
|
+
+ "\n ".join([str(m) for m in self.local_models])
|
|
45
|
+
+ "\n"
|
|
46
|
+
)
|
|
47
|
+
self.echo.echo_vvv(
|
|
48
|
+
"Non-local models:\n "
|
|
49
|
+
+ "\n ".join([str(m) for m in self.non_local_models])
|
|
50
|
+
+ "\n"
|
|
51
|
+
)
|
|
52
|
+
self.echo.echo_vv(
|
|
53
|
+
"The following models require annotations:\n "
|
|
54
|
+
+ "\n ".join(annotation_eligible)
|
|
55
|
+
+ "\n"
|
|
56
|
+
)
|
|
45
57
|
|
|
46
58
|
def _increment_count(self, count_type, incr_by=1):
|
|
47
59
|
self.model_counts[count_type] += incr_by
|
|
@@ -51,16 +63,19 @@ class DjangoSearch(BaseSearch):
|
|
|
51
63
|
Seed a new safelist file with all non-local models that need to be vetted.
|
|
52
64
|
"""
|
|
53
65
|
if os.path.exists(self.config.safelist_path):
|
|
54
|
-
fail(f
|
|
66
|
+
fail(f"{self.config.safelist_path} already exists, not overwriting.")
|
|
55
67
|
|
|
56
68
|
self.echo(
|
|
57
|
-
|
|
58
|
-
len(self.non_local_models)
|
|
69
|
+
"Found {} non-local models requiring annotations. Adding them to safelist.".format(
|
|
70
|
+
len(self.non_local_models)
|
|
71
|
+
)
|
|
59
72
|
)
|
|
60
73
|
|
|
61
|
-
safelist_data = {
|
|
74
|
+
safelist_data = {
|
|
75
|
+
self.get_model_id(model): {} for model in self.non_local_models
|
|
76
|
+
}
|
|
62
77
|
|
|
63
|
-
with open(self.config.safelist_path,
|
|
78
|
+
with open(self.config.safelist_path, "w") as safelist_file:
|
|
64
79
|
safelist_comment = """
|
|
65
80
|
# This is a Code Annotations automatically-generated Django model safelist file.
|
|
66
81
|
# These models must be annotated as follows in order to be counted in the coverage report.
|
|
@@ -73,24 +88,37 @@ class DjangoSearch(BaseSearch):
|
|
|
73
88
|
|
|
74
89
|
"""
|
|
75
90
|
safelist_file.write(safelist_comment.lstrip())
|
|
76
|
-
yaml.safe_dump(
|
|
91
|
+
yaml.safe_dump(
|
|
92
|
+
safelist_data, stream=safelist_file, default_flow_style=False
|
|
93
|
+
)
|
|
77
94
|
|
|
78
|
-
self.echo(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
95
|
+
self.echo(
|
|
96
|
+
f'Successfully created safelist file "{self.config.safelist_path}".',
|
|
97
|
+
fg="red",
|
|
98
|
+
)
|
|
99
|
+
self.echo("Now, you need to:", fg="red")
|
|
100
|
+
self.echo(
|
|
101
|
+
" 1) Make sure that any un-annotated models in the safelist are annotated, and",
|
|
102
|
+
fg="red",
|
|
103
|
+
)
|
|
104
|
+
self.echo(" 2) Annotate any LOCAL models (see --list_local_models).", fg="red")
|
|
82
105
|
|
|
83
106
|
def list_local_models(self):
|
|
84
107
|
"""
|
|
85
|
-
Dump a list of models in the local code tree that
|
|
108
|
+
Dump a list of models in the local code tree that are annotation eligible to stdout.
|
|
86
109
|
"""
|
|
87
110
|
if self.local_models:
|
|
88
111
|
self.echo(
|
|
89
|
-
|
|
112
|
+
"Listing {} local models requiring annotations:".format(
|
|
113
|
+
len(self.local_models)
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
self.echo.pprint(
|
|
117
|
+
sorted([self.get_model_id(model) for model in self.local_models]),
|
|
118
|
+
indent=4,
|
|
90
119
|
)
|
|
91
|
-
self.echo.pprint(sorted([self.get_model_id(model) for model in self.local_models]), indent=4)
|
|
92
120
|
else:
|
|
93
|
-
self.echo(
|
|
121
|
+
self.echo("No local models requiring annotations.")
|
|
94
122
|
|
|
95
123
|
def _append_model_annotations(self, model_type, model_id, query, model_annotations):
|
|
96
124
|
"""
|
|
@@ -112,32 +140,38 @@ class DjangoSearch(BaseSearch):
|
|
|
112
140
|
# annotation token itself. We find based on the entire code content of the model
|
|
113
141
|
# as that seems to be the only way to be sure we're getting the correct line number.
|
|
114
142
|
# It is slow and should be replaced if we can find a better way that is accurate.
|
|
115
|
-
line = txt.count(
|
|
143
|
+
line = txt.count("\n", 0, txt.find(inspect.getsource(model_type))) + 1
|
|
116
144
|
|
|
117
145
|
for inner_match in query.finditer(model_type.__doc__):
|
|
118
146
|
try:
|
|
119
|
-
annotation_token = inner_match.group(
|
|
120
|
-
annotation_data = inner_match.group(
|
|
121
|
-
except IndexError as error:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
)
|
|
127
|
-
annotation_token, annotation_data = clean_annotation(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
147
|
+
annotation_token = inner_match.group("token")
|
|
148
|
+
annotation_data = inner_match.group("data")
|
|
149
|
+
except IndexError as error: # pragma: no cover
|
|
150
|
+
raise ValueError(
|
|
151
|
+
'{}: Could not find "data" or "token" groups. Found: {}'.format(
|
|
152
|
+
self.get_model_id(model_type), inner_match.groupdict()
|
|
153
|
+
)
|
|
154
|
+
) from error
|
|
155
|
+
annotation_token, annotation_data = clean_annotation(
|
|
156
|
+
annotation_token, annotation_data
|
|
157
|
+
)
|
|
158
|
+
model_annotations.append(
|
|
159
|
+
{
|
|
160
|
+
"found_by": "django",
|
|
161
|
+
"filename": filename,
|
|
162
|
+
"line_number": line,
|
|
163
|
+
"annotation_token": annotation_token,
|
|
164
|
+
"annotation_data": annotation_data,
|
|
165
|
+
"extra": {
|
|
166
|
+
"object_id": model_id,
|
|
167
|
+
"full_comment": model_type.__doc__.strip(),
|
|
168
|
+
},
|
|
137
169
|
}
|
|
138
|
-
|
|
170
|
+
)
|
|
139
171
|
|
|
140
|
-
def _append_safelisted_model_annotations(
|
|
172
|
+
def _append_safelisted_model_annotations(
|
|
173
|
+
self, safelisted_models, model_id, model_annotations
|
|
174
|
+
):
|
|
141
175
|
"""
|
|
142
176
|
Append the safelisted annotations for the given model id to model_annotations.
|
|
143
177
|
|
|
@@ -148,17 +182,19 @@ class DjangoSearch(BaseSearch):
|
|
|
148
182
|
"""
|
|
149
183
|
for annotation in safelisted_models[model_id]:
|
|
150
184
|
comment = safelisted_models[model_id][annotation]
|
|
151
|
-
model_annotations.append(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
185
|
+
model_annotations.append(
|
|
186
|
+
{
|
|
187
|
+
"found_by": "safelist",
|
|
188
|
+
"filename": self.config.safelist_path,
|
|
189
|
+
"line_number": 0,
|
|
190
|
+
"annotation_token": annotation.strip(),
|
|
191
|
+
"annotation_data": comment.strip(),
|
|
192
|
+
"extra": {
|
|
193
|
+
"object_id": model_id,
|
|
194
|
+
"full_comment": str(safelisted_models[model_id]),
|
|
195
|
+
},
|
|
160
196
|
}
|
|
161
|
-
|
|
197
|
+
)
|
|
162
198
|
|
|
163
199
|
def _read_safelist(self):
|
|
164
200
|
"""
|
|
@@ -168,19 +204,23 @@ class DjangoSearch(BaseSearch):
|
|
|
168
204
|
The Python representation of the safelist
|
|
169
205
|
"""
|
|
170
206
|
if os.path.exists(self.config.safelist_path):
|
|
171
|
-
self.echo(f
|
|
207
|
+
self.echo(f"Found safelist at {self.config.safelist_path}. Reading.\n")
|
|
172
208
|
with open(self.config.safelist_path) as safelist_file:
|
|
173
209
|
safelisted_models = yaml.safe_load(safelist_file)
|
|
174
|
-
self._increment_count(
|
|
210
|
+
self._increment_count("safelisted", len(safelisted_models))
|
|
175
211
|
|
|
176
212
|
if safelisted_models:
|
|
177
|
-
self.echo.echo_vv(
|
|
213
|
+
self.echo.echo_vv(
|
|
214
|
+
" Safelisted models:\n " + "\n ".join(safelisted_models)
|
|
215
|
+
)
|
|
178
216
|
else:
|
|
179
|
-
self.echo.echo_vv(
|
|
217
|
+
self.echo.echo_vv(" No safelisted models found.\n")
|
|
180
218
|
|
|
181
219
|
return safelisted_models
|
|
182
220
|
else:
|
|
183
|
-
raise Exception(
|
|
221
|
+
raise Exception(
|
|
222
|
+
"Safelist not found! Generate one with the --seed_safelist command."
|
|
223
|
+
)
|
|
184
224
|
|
|
185
225
|
def search(self):
|
|
186
226
|
"""
|
|
@@ -196,12 +236,12 @@ class DjangoSearch(BaseSearch):
|
|
|
196
236
|
|
|
197
237
|
annotated_models = {}
|
|
198
238
|
|
|
199
|
-
self.echo.echo_vv(
|
|
239
|
+
self.echo.echo_vv("Searching models and their parent classes...")
|
|
200
240
|
|
|
201
241
|
# Walk all models and their parents looking for annotations
|
|
202
242
|
for model in self.local_models.union(self.non_local_models):
|
|
203
243
|
model_id = self.get_model_id(model)
|
|
204
|
-
self.echo.echo_vv(
|
|
244
|
+
self.echo.echo_vv(" " + model_id)
|
|
205
245
|
hierarchy = inspect.getmro(model)
|
|
206
246
|
model_annotations = []
|
|
207
247
|
|
|
@@ -209,23 +249,35 @@ class DjangoSearch(BaseSearch):
|
|
|
209
249
|
for obj in hierarchy:
|
|
210
250
|
if obj.__doc__ is not None:
|
|
211
251
|
if any(anno in obj.__doc__ for anno in annotation_tokens):
|
|
212
|
-
self.echo.echo_vvv(
|
|
213
|
-
|
|
252
|
+
self.echo.echo_vvv(
|
|
253
|
+
" "
|
|
254
|
+
+ DjangoSearch.get_model_id(obj)
|
|
255
|
+
+ " has annotations."
|
|
256
|
+
)
|
|
257
|
+
self._append_model_annotations(
|
|
258
|
+
obj, model_id, query, model_annotations
|
|
259
|
+
)
|
|
214
260
|
else:
|
|
215
261
|
# Don't use get_model_id here, as this could be a base class below Model
|
|
216
|
-
self.echo.echo_vvv(
|
|
262
|
+
self.echo.echo_vvv(" " + str(obj) + " has no annotations.")
|
|
217
263
|
|
|
218
264
|
# If there are any annotations in the model, format them
|
|
219
265
|
if model_annotations:
|
|
220
|
-
self.echo.echo_vv(
|
|
221
|
-
|
|
266
|
+
self.echo.echo_vv(
|
|
267
|
+
" {} has {} total annotations".format(
|
|
268
|
+
model_id, len(model_annotations)
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
self._increment_count("annotated")
|
|
222
272
|
if model_id in safelisted_models:
|
|
223
|
-
self._add_error(
|
|
273
|
+
self._add_error(
|
|
274
|
+
f"{model_id} is annotated, but also in the safelist."
|
|
275
|
+
)
|
|
224
276
|
self.format_file_results(annotated_models, [model_annotations])
|
|
225
277
|
|
|
226
278
|
# The model is not in the safelist and is not annotated
|
|
227
279
|
elif model_id not in safelisted_models:
|
|
228
|
-
self._increment_count(
|
|
280
|
+
self._increment_count("unannotated")
|
|
229
281
|
self.uncovered_model_ids.add(model_id)
|
|
230
282
|
self.echo.echo_vv(f" {model_id} has no annotations")
|
|
231
283
|
|
|
@@ -234,11 +286,15 @@ class DjangoSearch(BaseSearch):
|
|
|
234
286
|
if not safelisted_models[model_id]:
|
|
235
287
|
self.uncovered_model_ids.add(model_id)
|
|
236
288
|
self.echo.echo_vv(f" {model_id} is in the safelist.")
|
|
237
|
-
self._add_error(
|
|
289
|
+
self._add_error(
|
|
290
|
+
f"{model_id} is in the safelist but has no annotations!"
|
|
291
|
+
)
|
|
238
292
|
else:
|
|
239
|
-
self._increment_count(
|
|
293
|
+
self._increment_count("annotated")
|
|
240
294
|
|
|
241
|
-
self._append_safelisted_model_annotations(
|
|
295
|
+
self._append_safelisted_model_annotations(
|
|
296
|
+
safelisted_models, model_id, model_annotations
|
|
297
|
+
)
|
|
242
298
|
self.format_file_results(annotated_models, [model_annotations])
|
|
243
299
|
|
|
244
300
|
return annotated_models
|
|
@@ -249,16 +305,24 @@ class DjangoSearch(BaseSearch):
|
|
|
249
305
|
|
|
250
306
|
Returns:
|
|
251
307
|
Bool indicating whether or not the number of annotated models covers a percentage
|
|
252
|
-
of total
|
|
308
|
+
of total annotation eligible models greater than or equal to the configured
|
|
253
309
|
coverage_target.
|
|
254
310
|
"""
|
|
255
311
|
self.echo("\nModel coverage report")
|
|
256
312
|
self.echo("-" * 40)
|
|
257
313
|
self.echo("Found {total} total models.".format(**self.model_counts))
|
|
258
|
-
self.echo(
|
|
314
|
+
self.echo(
|
|
315
|
+
"{annotation_eligible} were eligible for annotation, {annotated} were annotated.".format(
|
|
316
|
+
**self.model_counts
|
|
317
|
+
)
|
|
318
|
+
)
|
|
259
319
|
|
|
260
|
-
if self.model_counts[
|
|
261
|
-
pct =
|
|
320
|
+
if self.model_counts["annotation_eligible"] > 0:
|
|
321
|
+
pct = (
|
|
322
|
+
float(self.model_counts["annotated"])
|
|
323
|
+
/ float(self.model_counts["annotation_eligible"])
|
|
324
|
+
* 100.0
|
|
325
|
+
)
|
|
262
326
|
pct = round(pct, 1)
|
|
263
327
|
else:
|
|
264
328
|
pct = 100.0
|
|
@@ -269,15 +333,16 @@ class DjangoSearch(BaseSearch):
|
|
|
269
333
|
displayed_uncovereds = list(self.uncovered_model_ids)
|
|
270
334
|
displayed_uncovereds.sort()
|
|
271
335
|
self.echo(
|
|
272
|
-
"Coverage found {} uncovered models:\n ".format(
|
|
273
|
-
|
|
336
|
+
"Coverage found {} uncovered models:\n ".format(
|
|
337
|
+
len(self.uncovered_model_ids)
|
|
338
|
+
)
|
|
339
|
+
+ "\n ".join(displayed_uncovereds)
|
|
274
340
|
)
|
|
275
341
|
|
|
276
342
|
if pct < float(self.config.coverage_target):
|
|
277
343
|
self.echo(
|
|
278
344
|
"\nCoverage threshold not met! Needed {}, actually {}!".format(
|
|
279
|
-
self.config.coverage_target,
|
|
280
|
-
pct
|
|
345
|
+
self.config.coverage_target, pct
|
|
281
346
|
)
|
|
282
347
|
)
|
|
283
348
|
return False
|
|
@@ -292,13 +357,15 @@ class DjangoSearch(BaseSearch):
|
|
|
292
357
|
# Anything inheriting from django.models.Model will have a ._meta attribute. Our tests
|
|
293
358
|
# inherit from object, which doesn't have it, and will fail below. This is a quick way
|
|
294
359
|
# to early out on both.
|
|
295
|
-
if not hasattr(model,
|
|
360
|
+
if not hasattr(model, "_meta"):
|
|
296
361
|
return False
|
|
297
362
|
|
|
298
|
-
return
|
|
299
|
-
|
|
300
|
-
and not model.
|
|
363
|
+
return (
|
|
364
|
+
issubclass(model, models.Model)
|
|
365
|
+
and not (model is models.Model)
|
|
366
|
+
and not model._meta.abstract
|
|
301
367
|
and not model._meta.proxy
|
|
368
|
+
)
|
|
302
369
|
|
|
303
370
|
@staticmethod
|
|
304
371
|
def is_non_local(model):
|
|
@@ -324,7 +391,7 @@ class DjangoSearch(BaseSearch):
|
|
|
324
391
|
# "site-packages" or "dist-packages".
|
|
325
392
|
non_local_path_prefixes = []
|
|
326
393
|
for path in sys.path:
|
|
327
|
-
if
|
|
394
|
+
if "dist-packages" in path or "site-packages" in path:
|
|
328
395
|
non_local_path_prefixes.append(path)
|
|
329
396
|
model_source_path = inspect.getsourcefile(model)
|
|
330
397
|
return model_source_path.startswith(tuple(non_local_path_prefixes))
|
|
@@ -340,7 +407,7 @@ class DjangoSearch(BaseSearch):
|
|
|
340
407
|
Returns:
|
|
341
408
|
str: identifier string for the given model.
|
|
342
409
|
"""
|
|
343
|
-
return f
|
|
410
|
+
return f"{model._meta.app_label}.{model._meta.object_name}"
|
|
344
411
|
|
|
345
412
|
@staticmethod
|
|
346
413
|
def setup_django():
|
|
@@ -354,12 +421,12 @@ class DjangoSearch(BaseSearch):
|
|
|
354
421
|
|
|
355
422
|
This function is idempotent.
|
|
356
423
|
"""
|
|
357
|
-
if sys.path[0] !=
|
|
358
|
-
sys.path.insert(0,
|
|
424
|
+
if sys.path[0] != "": # pragma: no cover
|
|
425
|
+
sys.path.insert(0, "")
|
|
359
426
|
django.setup()
|
|
360
427
|
|
|
361
428
|
@staticmethod
|
|
362
|
-
def get_models_requiring_annotations():
|
|
429
|
+
def get_models_requiring_annotations(app_name=None):
|
|
363
430
|
"""
|
|
364
431
|
Determine all local and non-local models via django model introspection.
|
|
365
432
|
|
|
@@ -367,19 +434,17 @@ class DjangoSearch(BaseSearch):
|
|
|
367
434
|
edX). This is a compromise in accuracy in order to simplify the generation
|
|
368
435
|
of this list, and also to ease the transition from zero to 100% annotations
|
|
369
436
|
in edX satellite repositories.
|
|
370
|
-
|
|
371
|
-
Returns:
|
|
372
|
-
tuple:
|
|
373
|
-
2-tuple where the first item is a set of local models, and the
|
|
374
|
-
second item is a set of non-local models.
|
|
375
437
|
"""
|
|
376
438
|
DjangoSearch.setup_django()
|
|
377
439
|
local_models = set()
|
|
378
440
|
non_local_models = set()
|
|
379
|
-
|
|
441
|
+
annotation_eligible_models = []
|
|
380
442
|
total_models = 0
|
|
381
443
|
|
|
382
444
|
for app in apps.get_app_configs():
|
|
445
|
+
if app_name and not app.name.endswith(app_name):
|
|
446
|
+
continue
|
|
447
|
+
|
|
383
448
|
for root_model in app.get_models():
|
|
384
449
|
total_models += 1
|
|
385
450
|
if DjangoSearch.requires_annotations(root_model):
|
|
@@ -388,6 +453,8 @@ class DjangoSearch(BaseSearch):
|
|
|
388
453
|
else:
|
|
389
454
|
local_models.add(root_model)
|
|
390
455
|
|
|
391
|
-
|
|
456
|
+
annotation_eligible_models.append(
|
|
457
|
+
DjangoSearch.get_model_id(root_model)
|
|
458
|
+
)
|
|
392
459
|
|
|
393
|
-
return local_models, non_local_models, total_models,
|
|
460
|
+
return local_models, non_local_models, total_models, annotation_eligible_models
|
|
@@ -27,7 +27,7 @@ class ReportRenderer:
|
|
|
27
27
|
self.config = config
|
|
28
28
|
self.echo = self.config.echo
|
|
29
29
|
self.report_files = report_files
|
|
30
|
-
self.create_time = datetime.datetime.
|
|
30
|
+
self.create_time = datetime.datetime.utcnow().isoformat()
|
|
31
31
|
|
|
32
32
|
self.full_report = self._aggregate_reports()
|
|
33
33
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: code-annotations
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.2
|
|
4
4
|
Summary: Extensible tools for parsing annotations in codebases
|
|
5
5
|
Home-page: https://github.com/openedx/code-annotations
|
|
6
6
|
Author: edX
|
|
@@ -22,10 +22,10 @@ Description-Content-Type: text/x-rst
|
|
|
22
22
|
License-File: LICENSE.txt
|
|
23
23
|
License-File: NOTICE.txt
|
|
24
24
|
Requires-Dist: pyyaml
|
|
25
|
+
Requires-Dist: Jinja2
|
|
25
26
|
Requires-Dist: stevedore
|
|
26
27
|
Requires-Dist: python-slugify
|
|
27
28
|
Requires-Dist: click
|
|
28
|
-
Requires-Dist: Jinja2
|
|
29
29
|
Provides-Extra: django
|
|
30
30
|
Requires-Dist: Django<2.3,>=2.2; extra == "django"
|
|
31
31
|
|
|
@@ -129,6 +129,12 @@ Change Log
|
|
|
129
129
|
Unreleased
|
|
130
130
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
131
131
|
|
|
132
|
+
[1.8.1] - 2024-07-11
|
|
133
|
+
~~~~~~~~~~~~~~~~~~~~
|
|
134
|
+
|
|
135
|
+
* Fix elapsed-time calculations to always use UTC. Other clocks can be altered partway through by Django config settings being loaded while the timer is running, resulting in reporting elapsed time of "-17999.895582 seconds" or similar.
|
|
136
|
+
* Fix report filename to use year-month-day order, not year-day-month. (Also more compact, now.)
|
|
137
|
+
|
|
132
138
|
[1.8.0] - 2024-03-31
|
|
133
139
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
134
140
|
|
|
@@ -43,7 +43,7 @@ def test_seeding_safelist(local_models, non_local_models, **kwargs):
|
|
|
43
43
|
local_models,
|
|
44
44
|
non_local_models,
|
|
45
45
|
0, # Number of total models found, irrelevant here
|
|
46
|
-
|
|
46
|
+
set() # List of model ids that are eligible for annotation, irrelevant here
|
|
47
47
|
)
|
|
48
48
|
|
|
49
49
|
def test_safelist_callback():
|
|
@@ -73,7 +73,7 @@ def test_safelist_exists(**kwargs):
|
|
|
73
73
|
Test the success case for seeding the safelist.
|
|
74
74
|
"""
|
|
75
75
|
mock_get_models_requiring_annotations = kwargs['get_models_requiring_annotations']
|
|
76
|
-
mock_get_models_requiring_annotations.return_value = (
|
|
76
|
+
mock_get_models_requiring_annotations.return_value = (set(), set(), 0, [])
|
|
77
77
|
|
|
78
78
|
result = call_script_isolated(
|
|
79
79
|
['django_find_annotations', '--config_file', 'test_config.yml', '--seed_safelist']
|
|
@@ -472,3 +472,20 @@ def test_setup_django(mock_django_setup):
|
|
|
472
472
|
"""
|
|
473
473
|
mock_django_setup.return_value = True
|
|
474
474
|
DjangoSearch.setup_django()
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
@patch.multiple(
|
|
478
|
+
'code_annotations.find_django.DjangoSearch',
|
|
479
|
+
get_models_requiring_annotations=DEFAULT
|
|
480
|
+
)
|
|
481
|
+
def test_find_django_no_action(**kwargs):
|
|
482
|
+
"""
|
|
483
|
+
Test that we fail when there is no action specified.
|
|
484
|
+
"""
|
|
485
|
+
|
|
486
|
+
result = call_script_isolated(
|
|
487
|
+
['django_find_annotations', '--config_file', 'test_config.yml'],
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
assert result.exit_code == EXIT_CODE_FAILURE
|
|
491
|
+
assert 'No actions specified' in result.output
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/config/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/sphinx/extensions/base.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|