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.
Files changed (47) hide show
  1. {code-annotations-1.8.0 → code-annotations-1.8.2}/CHANGELOG.rst +6 -0
  2. {code-annotations-1.8.0 → code-annotations-1.8.2}/PKG-INFO +8 -2
  3. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/__init__.py +1 -1
  4. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/base.py +2 -2
  5. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/cli.py +114 -65
  6. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/find_django.py +169 -102
  7. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/generate_docs.py +1 -1
  8. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations.egg-info/PKG-INFO +8 -2
  9. {code-annotations-1.8.0 → code-annotations-1.8.2}/tests/test_django_generate_safelist.py +2 -2
  10. {code-annotations-1.8.0 → code-annotations-1.8.2}/tests/test_find_django.py +17 -0
  11. {code-annotations-1.8.0 → code-annotations-1.8.2}/LICENSE.txt +0 -0
  12. {code-annotations-1.8.0 → code-annotations-1.8.2}/MANIFEST.in +0 -0
  13. {code-annotations-1.8.0 → code-annotations-1.8.2}/NOTICE.txt +0 -0
  14. {code-annotations-1.8.0 → code-annotations-1.8.2}/README.rst +0 -0
  15. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/annotation_errors.py +0 -0
  16. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/config/__init__.py +0 -0
  17. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/config/feature_toggle_annotations.yaml +0 -0
  18. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/config/openedx_events_annotations.yaml +0 -0
  19. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/config/setting_annotations.yaml +0 -0
  20. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/sphinx/extensions/__init__.py +0 -0
  21. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/sphinx/extensions/base.py +0 -0
  22. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/sphinx/extensions/featuretoggles.py +0 -0
  23. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/sphinx/extensions/openedx_events.py +0 -0
  24. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/contrib/sphinx/extensions/settings.py +0 -0
  25. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/exceptions.py +0 -0
  26. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/extensions/__init__.py +0 -0
  27. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/extensions/base.py +0 -0
  28. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/extensions/javascript.py +0 -0
  29. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/extensions/python.py +0 -0
  30. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/find_static.py +0 -0
  31. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations/helpers.py +0 -0
  32. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations.egg-info/SOURCES.txt +0 -0
  33. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations.egg-info/dependency_links.txt +0 -0
  34. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations.egg-info/entry_points.txt +0 -0
  35. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations.egg-info/not-zip-safe +0 -0
  36. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations.egg-info/requires.txt +1 -1
  37. {code-annotations-1.8.0 → code-annotations-1.8.2}/code_annotations.egg-info/top_level.txt +0 -0
  38. {code-annotations-1.8.0 → code-annotations-1.8.2}/requirements/base.in +0 -0
  39. {code-annotations-1.8.0 → code-annotations-1.8.2}/setup.cfg +0 -0
  40. {code-annotations-1.8.0 → code-annotations-1.8.2}/setup.py +0 -0
  41. {code-annotations-1.8.0 → code-annotations-1.8.2}/tests/test_base.py +0 -0
  42. {code-annotations-1.8.0 → code-annotations-1.8.2}/tests/test_django_coverage.py +0 -0
  43. {code-annotations-1.8.0 → code-annotations-1.8.2}/tests/test_django_list_local_models.py +0 -0
  44. {code-annotations-1.8.0 → code-annotations-1.8.2}/tests/test_find_static.py +0 -0
  45. {code-annotations-1.8.0 → code-annotations-1.8.2}/tests/test_generate_docs.py +0 -0
  46. {code-annotations-1.8.0 → code-annotations-1.8.2}/tests/test_search.py +0 -0
  47. {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.0
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
 
@@ -2,4 +2,4 @@
2
2
  Extensible tools for parsing annotations in codebases.
3
3
  """
4
4
 
5
- __version__ = '1.8.0'
5
+ __version__ = '1.8.2'
@@ -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.now()
619
+ now = datetime.datetime.utcnow()
620
620
  report_filename = os.path.join(self.config.report_path, '{}{}.yaml'.format(
621
- report_prefix, now.strftime('%Y-%d-%m-%H-%M-%S')
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('django_find_annotations')
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
- '--config_file',
27
- default='.annotations',
28
- help='Path to the configuration file',
29
- type=click.Path(exists=True, dir_okay=False, resolve_path=True)
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
- '--seed_safelist/--no_safelist',
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
- '--list_local_models/--no_list_models',
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
- config_file,
51
- seed_safelist,
52
- list_local_models,
53
- app_name,
54
- report_path,
55
- verbosity,
56
- lint,
57
- report,
58
- coverage
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.now()
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("Please add 'coverage_target' to your configuration before running --coverage")
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.now() - start_time
110
- click.echo("Search found {} annotations in {} seconds.".format(
111
- annotation_count, elapsed.total_seconds()
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('static_find_annotations')
156
+ @entry_point.command("static_find_annotations")
120
157
  @click.option(
121
- '--config_file',
122
- default='.annotations',
123
- help='Path to the configuration file',
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
- '--source_path',
128
- help='Location of the source code to search',
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('--report_path', default=None, help='Location to write the report')
132
- @click.option('-v', '--verbosity', count=True, help='Verbosity level (-v through -vvv)')
133
- @click.option('--lint/--no_lint', help='Enable or disable linting checks', default=True, show_default=True)
134
- @click.option('--report/--no_report', help='Enable or disable writing the report file', default=True, show_default=True)
135
- def static_find_annotations(config_file, source_path, report_path, verbosity, lint, report):
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.now()
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.now() - start_time
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
- '--config_file',
180
- default='.annotations',
181
- help='Path to the configuration file',
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('-v', '--verbosity', count=True, help='Verbosity level (-v through -vvv)')
185
- @click.argument("report_files", type=click.File('r'), nargs=-1)
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.now()
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
- 'report_template_dir',
201
- 'rendered_report_dir',
202
- 'rendered_report_file_extension',
203
- 'rendered_report_source_link_prefix'
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("Rendering the following reports: \n{}".format("\n".join([r.name for r in report_files])))
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.now() - start_time
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 = '.annotation_safe_list.yml'
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, needing_annotation = self.get_models_requiring_annotations()
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
- 'total': total,
35
- 'annotated': 0,
36
- 'unannotated': 0,
37
- 'needing_annotation': len(needing_annotation),
38
- 'not_needing_annotation': total - len(needing_annotation),
39
- 'safelisted': 0
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('Local models:\n ' + '\n '.join([str(m) for m in self.local_models]) + '\n')
43
- self.echo.echo_vvv('Non-local models:\n ' + '\n '.join([str(m) for m in self.non_local_models]) + '\n')
44
- self.echo.echo_vv('The following models require annotations:\n ' + '\n '.join(needing_annotation) + '\n')
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'{self.config.safelist_path} already exists, not overwriting.')
66
+ fail(f"{self.config.safelist_path} already exists, not overwriting.")
55
67
 
56
68
  self.echo(
57
- 'Found {} non-local models requiring annotations. Adding them to safelist.'.format(
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 = {self.get_model_id(model): {} for model in self.non_local_models}
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, 'w') as safelist_file:
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(safelist_data, stream=safelist_file, default_flow_style=False)
91
+ yaml.safe_dump(
92
+ safelist_data, stream=safelist_file, default_flow_style=False
93
+ )
77
94
 
78
- self.echo(f'Successfully created safelist file "{self.config.safelist_path}".', fg='red')
79
- self.echo('Now, you need to:', fg='red')
80
- self.echo(' 1) Make sure that any un-annotated models in the safelist are annotated, and', fg='red')
81
- self.echo(' 2) Annotate any LOCAL models (see --list_local_models).', fg='red')
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 need annotations to stdout.
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
- 'Listing {} local models requiring annotations:'.format(len(self.local_models))
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('No local models requiring annotations.')
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('\n', 0, txt.find(inspect.getsource(model_type))) + 1
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('token')
120
- annotation_data = inner_match.group('data')
121
- except IndexError as error:
122
- # pragma: no cover
123
- raise ValueError('{}: Could not find "data" or "token" groups. Found: {}'.format(
124
- self.get_model_id(model_type),
125
- inner_match.groupdict()
126
- )) from error
127
- annotation_token, annotation_data = clean_annotation(annotation_token, annotation_data)
128
- model_annotations.append({
129
- 'found_by': "django",
130
- 'filename': filename,
131
- 'line_number': line,
132
- 'annotation_token': annotation_token,
133
- 'annotation_data': annotation_data,
134
- 'extra': {
135
- 'object_id': model_id,
136
- 'full_comment': model_type.__doc__.strip()
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(self, safelisted_models, model_id, 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
- 'found_by': "safelist",
153
- 'filename': self.config.safelist_path,
154
- 'line_number': 0,
155
- 'annotation_token': annotation.strip(),
156
- 'annotation_data': comment.strip(),
157
- 'extra': {
158
- 'object_id': model_id,
159
- 'full_comment': str(safelisted_models[model_id])
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'Found safelist at {self.config.safelist_path}. Reading.\n')
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('safelisted', len(safelisted_models))
210
+ self._increment_count("safelisted", len(safelisted_models))
175
211
 
176
212
  if safelisted_models:
177
- self.echo.echo_vv(' Safelisted models:\n ' + '\n '.join(safelisted_models))
213
+ self.echo.echo_vv(
214
+ " Safelisted models:\n " + "\n ".join(safelisted_models)
215
+ )
178
216
  else:
179
- self.echo.echo_vv(' No safelisted models found.\n')
217
+ self.echo.echo_vv(" No safelisted models found.\n")
180
218
 
181
219
  return safelisted_models
182
220
  else:
183
- raise Exception('Safelist not found! Generate one with the --seed_safelist command.')
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('Searching models and their parent classes...')
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(' ' + model_id)
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(' ' + DjangoSearch.get_model_id(obj) + ' has annotations.')
213
- self._append_model_annotations(obj, model_id, query, model_annotations)
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(' ' + str(obj) + ' has no annotations.')
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(" {} has {} total annotations".format(model_id, len(model_annotations)))
221
- self._increment_count('annotated')
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(f"{model_id} is annotated, but also in the safelist.")
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('unannotated')
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(f"{model_id} is in the safelist but has no annotations!")
289
+ self._add_error(
290
+ f"{model_id} is in the safelist but has no annotations!"
291
+ )
238
292
  else:
239
- self._increment_count('annotated')
293
+ self._increment_count("annotated")
240
294
 
241
- self._append_safelisted_model_annotations(safelisted_models, model_id, 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 models needing annotations greater than or equal to the configured
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("{needing_annotation} needed annotation, {annotated} were annotated.".format(**self.model_counts))
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['needing_annotation'] > 0:
261
- pct = float(self.model_counts['annotated']) / float(self.model_counts['needing_annotation']) * 100.0
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(len(self.uncovered_model_ids)) +
273
- "\n ".join(displayed_uncovereds)
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, '_meta'):
360
+ if not hasattr(model, "_meta"):
296
361
  return False
297
362
 
298
- return issubclass(model, models.Model) \
299
- and not (model is models.Model) \
300
- and not model._meta.abstract \
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 'dist-packages' in path or 'site-packages' in path:
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'{model._meta.app_label}.{model._meta.object_name}'
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] != '': # pragma: no cover
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
- models_requiring_annotations = []
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
- models_requiring_annotations.append(DjangoSearch.get_model_id(root_model))
456
+ annotation_eligible_models.append(
457
+ DjangoSearch.get_model_id(root_model)
458
+ )
392
459
 
393
- return local_models, non_local_models, total_models, models_requiring_annotations
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.now().isoformat()
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.0
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
- [] # List of model ids that need anntations, irrelevant here
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 = ([], [], 0, [])
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
@@ -1,8 +1,8 @@
1
1
  pyyaml
2
+ Jinja2
2
3
  stevedore
3
4
  python-slugify
4
5
  click
5
- Jinja2
6
6
 
7
7
  [django]
8
8
  Django<2.3,>=2.2