18
18
from collections import defaultdict
19
19
import json
20
20
import logging
21
- import os .path
22
- import posixpath
21
+ from pathlib import Path , PosixPath
23
22
24
23
from docutils .utils import get_source_line
25
24
from docutils import nodes
26
25
from sphinx .util import logging as sphinx_logging
27
26
27
+ import matplotlib
28
+
28
29
logger = sphinx_logging .getLogger (__name__ )
29
30
30
31
@@ -42,11 +43,11 @@ def _record_reference(self, record):
42
43
isinstance (getattr (record , 'location' , None ), nodes .Node )):
43
44
return
44
45
45
- if not hasattr (self .app .env , "missing_reference_warnings " ):
46
- self .app .env .missing_reference_warnings = defaultdict (set )
46
+ if not hasattr (self .app .env , "missing_references_warnings " ):
47
+ self .app .env .missing_references_warnings = defaultdict (set )
47
48
48
49
record_missing_reference (self .app ,
49
- self .app .env .missing_reference_warnings ,
50
+ self .app .env .missing_references_warnings ,
50
51
record .location )
51
52
52
53
def filter (self , record ):
@@ -60,9 +61,9 @@ def record_missing_reference(app, record, node):
60
61
target = node ["reftarget" ]
61
62
location = get_location (node , app )
62
63
63
- dtype = "{}:{}" .format (domain , typ )
64
+ domain_type = "{}:{}" .format (domain , typ )
64
65
65
- record [(dtype , target )].add (location )
66
+ record [(domain_type , target )].add (location )
66
67
67
68
68
69
def record_missing_reference_handler (app , env , node , contnode ):
@@ -75,10 +76,10 @@ def record_missing_reference_handler(app, env, node, contnode):
75
76
# no-op when we are disabled.
76
77
return
77
78
78
- if not hasattr (env , "missing_reference_events " ):
79
- env .missing_reference_events = defaultdict (set )
79
+ if not hasattr (env , "missing_references_events " ):
80
+ env .missing_references_events = defaultdict (set )
80
81
81
- record_missing_reference (app , env .missing_reference_events , node )
82
+ record_missing_reference (app , env .missing_references_events , node )
82
83
83
84
84
85
def get_location (node , app ):
@@ -97,56 +98,92 @@ def get_location(node, app):
97
98
98
99
if path :
99
100
100
- basepath = os .path .abspath (os .path .join (app .confdir , ".." ))
101
- path = os .path .relpath (path , start = basepath )
101
+ # We locate references relative to the parent of the doc
102
+ # directory, which for matplotlib, will be the root of the
103
+ # matplotlib repo. When matplotlib is not an editable install
104
+ # wierd things will happen, but we can't totally recover from
105
+ # that.
106
+ basepath = Path (app .srcdir ).parent .resolve ()
107
+
108
+ fullpath = Path (path ).resolve ()
109
+
110
+ try :
111
+ path = fullpath .relative_to (basepath )
112
+ except ValueError :
113
+ # Sometimes docs directly contain e.g. docstrings
114
+ # from installed modules, and we record those as
115
+ # <external> so as to be independent of where the
116
+ # module was installed
117
+ path = Path ("<external>" ) / fullpath .name
102
118
103
- if path .startswith (os .path .pardir ):
104
- path = posixpath .join ("<external>" , os .path .basename (path ))
119
+ # Ensure that all reported paths are POSIX so that docs
120
+ # on windows result in the same warnings in the JSON file.
121
+ path = path .as_posix ()
105
122
106
123
else :
107
124
path = "<unknown>"
108
125
109
- line = str (line ) if line else ""
126
+ if not line :
127
+ line = ""
128
+
110
129
return f"{ path } :{ line } "
111
130
112
131
113
- def save_missing_references_handler (app , exc ):
114
- """
115
- At the end of the sphinx build, either save the missing references to a
116
- JSON file. Also ensure that all lines of the existing JSON file are still
117
- necessary.
118
- """
119
- if not app .config .missing_references_enabled :
120
- # no-op when we are disabled.
132
+ def _warn_unused_missing_references (app ):
133
+ if not app .config .missing_references_warn_unused_ignores :
121
134
return
122
135
123
- json_path = os .path .join (app .confdir ,
124
- app .config .missing_references_filename )
136
+ # We can only warn if we are building from a source install
137
+ # otherwise, we just have to skip this step.
138
+ basepath = Path (matplotlib .__file__ ).parent .parent .parent .resolve ()
139
+ srcpath = Path (app .srcdir ).parent .resolve ()
125
140
126
- references_warnings = getattr (app .env , 'missing_reference_warnings' , {})
141
+ if basepath != srcpath :
142
+ return
127
143
128
- # This is a dictionary of {(dtype, target): locations}
144
+ # This is a dictionary of {(domain_type, target): locations}
129
145
references_ignored = getattr (app .env ,
130
146
'missing_references_ignored_references' , {})
131
- references_events = getattr (app .env , 'missing_reference_events ' , {})
147
+ references_events = getattr (app .env , 'missing_references_events ' , {})
132
148
133
149
# Warn about any reference which is no longer missing.
134
- for (dtype , target ), locations in references_ignored .items ():
150
+ for (domain_type , target ), locations in references_ignored .items ():
135
151
missing_reference_locations = references_events .get (
136
- (dtype , target ), [])
152
+ (domain_type , target ), [])
137
153
138
- # For each ignored reference location, ensure a missing reference was
139
- # observed. If it wasn't observed, issue a warning.
154
+ # For each ignored reference location, ensure a missing reference
155
+ # was observed. If it wasn't observed, issue a warning.
140
156
for ignored_reference_location in locations :
141
- if ignored_reference_location not in missing_reference_locations :
142
- msg = (f"Reference { dtype } { target } for "
143
- f"{ ignored_reference_location } can be removed"
144
- f" from { app .config .missing_references_filename } ."
157
+ if (ignored_reference_location not in
158
+ missing_reference_locations ):
159
+ msg = (f"Reference { domain_type } { target } for "
160
+ f"{ ignored_reference_location } can be removed"
161
+ f" from { app .config .missing_references_filename } ."
145
162
"It is no longer a missing reference in the docs." )
146
163
logger .warning (msg ,
147
164
location = ignored_reference_location ,
148
165
type = 'ref' ,
149
- subtype = dtype )
166
+ subtype = domain_type )
167
+
168
+
169
+ def save_missing_references_handler (app , exc ):
170
+ """
171
+ At the end of the sphinx build, check that all lines of the existing JSON
172
+ file are still necessary.
173
+
174
+ If the configuration value ``missing_references_write_json`` is set
175
+ then write a new JSON file containing missing references.
176
+ """
177
+ if not app .config .missing_references_enabled :
178
+ # no-op when we are disabled.
179
+ return
180
+
181
+ _warn_unused_missing_references (app )
182
+
183
+ json_path = (Path (app .confdir ) /
184
+ app .config .missing_references_filename )
185
+
186
+ references_warnings = getattr (app .env , 'missing_references_warnings' , {})
150
187
151
188
if app .config .missing_references_write_json :
152
189
_write_missing_references_json (references_warnings , json_path )
@@ -156,15 +193,15 @@ def _write_missing_references_json(records, json_path):
156
193
"""
157
194
Convert ignored references to a format which we can write as JSON
158
195
159
- Convert from ``{(dtype , target): locaitons}`` to
160
- ``{dtype : {target: locations}}`` since JSON can't serialize tuples.
196
+ Convert from ``{(domain_type , target): locaitons}`` to
197
+ ``{domain_type : {target: locations}}`` since JSON can't serialize tuples.
161
198
"""
162
199
transformed_records = defaultdict (dict )
163
200
164
- for (dtype , target ), paths in records .items ():
165
- transformed_records [dtype ][target ] = sorted (paths )
201
+ for (domain_type , target ), paths in records .items ():
202
+ transformed_records [domain_type ][target ] = sorted (paths )
166
203
167
- with open (json_path , "w" ) as stream :
204
+ with json_path . open ("w" ) as stream :
168
205
json .dump (transformed_records , stream , indent = 2 )
169
206
170
207
@@ -173,24 +210,24 @@ def _read_missing_references_json(json_path):
173
210
Convert from the JSON file to the form used internally by this
174
211
extension.
175
212
176
- The JSON file is stored as ``{dtype : {target: [locations,]}}`` since JSON
177
- can't store dictionary keys which are tuples. We convert this back to
178
- ``{(dtype, target):[locations]}`` for internal use.
213
+ The JSON file is stored as ``{domain_type : {target: [locations,]}}``
214
+ since JSON can't store dictionary keys which are tuples. We convert
215
+ this back to ``{(domain_type, target):[locations]}`` for internal use.
179
216
180
217
"""
181
- with open (json_path , "r" ) as stream :
218
+ with json_path . open ("r" ) as stream :
182
219
data = json .load (stream )
183
220
184
221
ignored_references = {}
185
- for dtype , targets in data .items ():
222
+ for domain_type , targets in data .items ():
186
223
for target , locations in targets .items ():
187
- ignored_references [(dtype , target )] = locations
224
+ ignored_references [(domain_type , target )] = locations
188
225
return ignored_references
189
226
190
227
191
228
def prepare_missing_references_handler (app ):
192
229
"""
193
- Handler called to initalize this extension once the configuration
230
+ Handler called to initialize this extension once the configuration
194
231
is ready.
195
232
196
233
Reads the missing references file and populates ``nitpick_ignore`` if
@@ -203,8 +240,8 @@ def prepare_missing_references_handler(app):
203
240
sphinx_logger = logging .getLogger ('sphinx' )
204
241
missing_reference_filter = MissingReferenceFilter (app )
205
242
for handler in sphinx_logger .handlers [:]:
206
- if (isinstance (handler , sphinx_logging .WarningStreamHandler ) and
207
- missing_reference_filter not in handler .filters ):
243
+ if (isinstance (handler , sphinx_logging .WarningStreamHandler )
244
+ and missing_reference_filter not in handler .filters ):
208
245
209
246
# This *must* be the first filter, because subsequent filters
210
247
# throw away the node information and then we can't identify
@@ -213,16 +250,16 @@ def prepare_missing_references_handler(app):
213
250
214
251
app .env .missing_references_ignored_references = {}
215
252
216
- json_path = os . path . join ( app .confdir ,
217
- app .config .missing_references_filename )
218
- if not os . path . exists (json_path ):
253
+ json_path = ( Path ( app .confdir ) /
254
+ app .config .missing_references_filename )
255
+ if not json_path . exists ():
219
256
return
220
257
221
258
ignored_references = _read_missing_references_json (json_path )
222
259
223
260
app .env .missing_references_ignored_references = ignored_references
224
261
225
- # If we are going to re-write the JSON file, then don't supress missing
262
+ # If we are going to re-write the JSON file, then don't suppress missing
226
263
# reference warnings. We want to record a full list of missing references
227
264
# for use later. Otherwise, add all known missing references to
228
265
# ``nitpick_ignore```
@@ -233,6 +270,7 @@ def prepare_missing_references_handler(app):
233
270
def setup (app ):
234
271
app .add_config_value ("missing_references_enabled" , True , "env" )
235
272
app .add_config_value ("missing_references_write_json" , False , "env" )
273
+ app .add_config_value ("missing_references_warn_unused_ignores" , True , "env" )
236
274
app .add_config_value ("missing_references_filename" ,
237
275
"missing-references.json" , "env" )
238
276
0 commit comments