ixbrl-viewer 1.4.28__py3-none-any.whl → 1.4.30__py3-none-any.whl

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.

Potentially problematic release.


This version of ixbrl-viewer might be problematic. Click here for more details.

@@ -22,6 +22,9 @@ from .constants import CONFIG_COPY_SCRIPT, CONFIG_FEATURE_PREFIX, CONFIG_LAUNCH_
22
22
  DEFAULT_JS_FILENAME, DEFAULT_VIEWER_PATH, ERROR_MESSAGE_CODE, \
23
23
  EXCEPTION_MESSAGE_CODE, FEATURE_CONFIGS
24
24
  from .iXBRLViewer import IXBRLViewerBuilder, IXBRLViewerBuilderError
25
+ from .plugin import IXBRLViewerPluginData
26
+
27
+ PLUGIN_NAME = 'ixbrl-viewer'
25
28
 
26
29
  #
27
30
  # GUI operation:
@@ -94,10 +97,6 @@ def iXBRLViewerCommandLineOptionExtender(parser, *args, **kwargs):
94
97
  dest="zipViewerOutput",
95
98
  help="Converts the viewer output into a self contained zip")
96
99
 
97
- # Force "keepOpen" to true, so that all models are retained. Needed for
98
- # multi-instance viewers.
99
- parser.set_defaults(keepOpen = True)
100
-
101
100
  featureGroup = OptionGroup(parser, "Viewer Features",
102
101
  "See viewer README for information on enabling/disabling features.")
103
102
  for featureConfig in FEATURE_CONFIGS:
@@ -105,13 +104,30 @@ def iXBRLViewerCommandLineOptionExtender(parser, *args, **kwargs):
105
104
  featureGroup.add_option(arg, arg.lower(), action="store_true", default=False, help=featureConfig.description)
106
105
  parser.add_option_group(featureGroup)
107
106
 
107
+ def pluginData(cntlr: Cntlr):
108
+ pluginData = cntlr.getPluginData(PLUGIN_NAME)
109
+ if pluginData is None:
110
+ pluginData = IXBRLViewerPluginData(PLUGIN_NAME)
111
+ cntlr.setPluginData(pluginData)
112
+ return pluginData
113
+
114
+ def resetPluginData(cntlr: Cntlr):
115
+ pluginData(cntlr).builder = None
116
+
117
+ def processModel(cntlr: Cntlr, modelXbrl: ModelXbrl):
118
+ try:
119
+ pluginData(cntlr).builder.processModel(modelXbrl)
120
+ except IXBRLViewerBuilderError as ex:
121
+ print(ex)
122
+ except Exception as ex:
123
+ tb = traceback.format_tb(sys.exc_info()[2])
124
+ cntlr.addToLog(f"Exception {ex} \nTraceback {tb}", messageCode=EXCEPTION_MESSAGE_CODE)
108
125
 
109
126
  def generateViewer(
110
127
  cntlr: Cntlr,
111
128
  saveViewerDest: io.BytesIO | str | None,
112
129
  viewerURL: str | None = None,
113
130
  showValidationMessages: bool = False,
114
- useStubViewer: bool = False,
115
131
  zipViewerOutput: bool = False,
116
132
  features: list[str] | None = None,
117
133
  packageDownloadURL: str | None = None,
@@ -138,13 +154,10 @@ def generateViewer(
138
154
 
139
155
  viewerURL = viewerURL or DEFAULT_VIEWER_PATH
140
156
 
141
- if (cntlr.modelManager is None
142
- or len(cntlr.modelManager.loadedModelXbrls) == 0
143
- or any(not mx.modelDocument for mx in cntlr.modelManager.loadedModelXbrls)):
144
- cntlr.addToLog(f"No taxonomy loaded. {abortGenerationMsg}", messageCode=ERROR_MESSAGE_CODE)
145
- return
146
- if cntlr.modelManager.modelXbrl.modelDocument.type not in (Type.INLINEXBRL, Type.INLINEXBRLDOCUMENTSET):
147
- cntlr.addToLog(f"No inline XBRL document loaded. {abortGenerationMsg}", messageCode=ERROR_MESSAGE_CODE)
157
+ bldr = pluginData(cntlr).builder
158
+
159
+ if bldr.reportCount == 0:
160
+ cntlr.addToLog(f"No inline XBRL documents loaded. {abortGenerationMsg}", messageCode=ERROR_MESSAGE_CODE)
148
161
  return
149
162
 
150
163
  copyScriptPath = None
@@ -171,11 +184,10 @@ def generateViewer(
171
184
  return
172
185
 
173
186
  try:
174
- viewerBuilder = IXBRLViewerBuilder(cntlr.modelManager.loadedModelXbrls)
175
187
  if features:
176
188
  for feature in features:
177
- viewerBuilder.enableFeature(feature)
178
- iv = viewerBuilder.createViewer(scriptUrl=viewerURL, showValidations=showValidationMessages, useStubViewer=useStubViewer, packageDownloadURL=packageDownloadURL)
189
+ bldr.enableFeature(feature)
190
+ iv = bldr.createViewer(scriptUrl=viewerURL, showValidations=showValidationMessages, packageDownloadURL=packageDownloadURL)
179
191
  if iv is not None:
180
192
  iv.save(saveViewerDest, zipOutput=zipViewerOutput, copyScriptPath=copyScriptPath)
181
193
  except IXBRLViewerBuilderError as ex:
@@ -183,6 +195,7 @@ def generateViewer(
183
195
  except Exception as ex:
184
196
  tb = traceback.format_tb(sys.exc_info()[2])
185
197
  cntlr.addToLog(f"Exception {ex} \nTraceback {tb}", messageCode=EXCEPTION_MESSAGE_CODE)
198
+ resetPluginData(cntlr)
186
199
 
187
200
 
188
201
  def getFeaturesFromOptions(options: argparse.Namespace | OptionParser):
@@ -192,15 +205,19 @@ def getFeaturesFromOptions(options: argparse.Namespace | OptionParser):
192
205
  if getattr(options, f'viewer_feature_{featureConfig.key}') or getattr(options, f'viewer_feature_{featureConfig.key.lower()}')
193
206
  ]
194
207
 
208
+ def iXBRLViewerCommandLineXbrlRun(cntlr, options, modelXbrl, *args, **kwargs):
209
+ pd = pluginData(cntlr)
210
+ if pd.builder is None:
211
+ pd.builder = IXBRLViewerBuilder(cntlr, useStubViewer = options.useStubViewer)
212
+ processModel(cntlr, modelXbrl)
195
213
 
196
- def iXBRLViewerCommandLineXbrlRun(cntlr, options, *args, **kwargs):
214
+ def iXBRLViewerCommandLineFilingEnd(cntlr, options, *args, **kwargs):
197
215
  generateViewer(
198
216
  cntlr=cntlr,
199
217
  saveViewerDest=options.saveViewerDest or kwargs.get("responseZipStream"),
200
218
  viewerURL=options.viewerURL,
201
219
  copyScript=not options.viewerNoCopyScript,
202
220
  showValidationMessages=options.validationMessages,
203
- useStubViewer=options.useStubViewer,
204
221
  zipViewerOutput=options.zipViewerOutput,
205
222
  features=getFeaturesFromOptions(options),
206
223
  packageDownloadURL=options.packageDownloadURL,
@@ -259,9 +276,11 @@ def commandLineOptionExtender(*args, **kwargs):
259
276
  iXBRLViewerCommandLineOptionExtender(*args, **kwargs)
260
277
 
261
278
 
262
- def commandLineRun(*args, **kwargs):
263
- iXBRLViewerCommandLineXbrlRun(*args, **kwargs)
279
+ def commandLineRun(cntlr, options, modelXbrl, *args, **kwargs):
280
+ iXBRLViewerCommandLineXbrlRun(cntlr, options, modelXbrl, *args, **kwargs)
264
281
 
282
+ def commandLineFilingEnd(*args, **kwargs):
283
+ iXBRLViewerCommandLineFilingEnd(*args, **kwargs)
265
284
 
266
285
  class iXBRLViewerLocalViewer(LocalViewer):
267
286
  # plugin-specific local file handler
@@ -300,12 +319,13 @@ def guiRun(cntlr, modelXbrl, attach, *args, **kwargs):
300
319
  for c in FEATURE_CONFIGS
301
320
  if cntlr.config.setdefault(f'{CONFIG_FEATURE_PREFIX}{c.key}', False)
302
321
  ]
322
+ pluginData(cntlr).builder = IXBRLViewerBuilder(cntlr, useStubViewer = True)
323
+ processModel(cntlr, modelXbrl)
303
324
  generateViewer(
304
325
  cntlr=cntlr,
305
326
  saveViewerDest=tempViewer.name,
306
327
  viewerURL=cntlr.config.get(CONFIG_SCRIPT_URL),
307
328
  copyScript=cntlr.config.get(CONFIG_COPY_SCRIPT, DEFAULT_COPY_SCRIPT),
308
- useStubViewer=True,
309
329
  features=features,
310
330
  )
311
331
  if Path(tempViewer.name, viewer_file_name).exists():
@@ -325,7 +345,7 @@ def load_plugin_url():
325
345
 
326
346
 
327
347
  __pluginInfo__ = {
328
- 'name': 'ixbrl-viewer',
348
+ 'name': PLUGIN_NAME,
329
349
  'aliases': [
330
350
  'iXBRLViewerPlugin',
331
351
  ],
@@ -335,7 +355,8 @@ __pluginInfo__ = {
335
355
  'author': 'Paul Warren',
336
356
  'copyright': 'Copyright :: Workiva Inc. :: 2019',
337
357
  'CntlrCmdLine.Options': commandLineOptionExtender,
338
- 'CntlrCmdLine.Filing.End': commandLineRun,
358
+ 'CntlrCmdLine.Xbrl.Run': commandLineRun,
359
+ 'CntlrCmdLine.Filing.End': commandLineFilingEnd,
339
360
  'CntlrWinMain.Menu.Tools': toolsMenuExtender,
340
361
  'CntlrWinMain.Xbrl.Loaded': guiRun,
341
362
  }
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.4.28'
16
- __version_tuple__ = version_tuple = (1, 4, 28)
15
+ __version__ = version = '1.4.30'
16
+ __version_tuple__ = version_tuple = (1, 4, 30)
@@ -26,7 +26,6 @@ from lxml import etree
26
26
  from .constants import DEFAULT_JS_FILENAME, DEFAULT_OUTPUT_NAME, ERROR_MESSAGE_CODE, FEATURE_CONFIGS, INFO_MESSAGE_CODE
27
27
  from .xhtmlserialize import XHTMLSerializer
28
28
 
29
-
30
29
  UNRECOGNIZED_LINKBASE_LOCAL_DOCUMENTS_TYPE = 'unrecognizedLinkbase'
31
30
  LINK_QNAME_TO_LOCAL_DOCUMENTS_LINKBASE_TYPE = {
32
31
  XbrlConst.qnLinkCalculationLink: 'calcLinkbase',
@@ -82,18 +81,38 @@ class IXBRLViewerBuilderError(Exception):
82
81
 
83
82
  class IXBRLViewerBuilder:
84
83
 
85
- def __init__(self, reports: list[ModelXbrl], basenameSuffix: str = ''):
84
+ def __init__(self,
85
+ cntlr: Cntlr,
86
+ basenameSuffix: str = '',
87
+ useStubViewer: bool = False,
88
+ ):
86
89
  self.nsmap = NamespaceMap()
87
90
  self.roleMap = NamespaceMap()
88
- self.reports = reports
89
- # Arbitrary ModelXbrl used for logging
90
- self.logger_model = reports[0]
91
91
  self.taxonomyData = {
92
92
  "sourceReports": [],
93
93
  "features": [],
94
94
  }
95
95
  self.basenameSuffix = basenameSuffix
96
96
  self.currentTargetReport = None
97
+ self.useStubViewer = useStubViewer
98
+ self.cntlr = cntlr
99
+
100
+ self.idGen = 0
101
+ self.roleMap.getPrefix(XbrlConst.standardLabel, "std")
102
+ self.roleMap.getPrefix(XbrlConst.documentationLabel, "doc")
103
+ self.roleMap.getPrefix(XbrlConst.summationItem, "calc")
104
+ self.roleMap.getPrefix(XbrlConst.summationItem, "calc11")
105
+ self.roleMap.getPrefix(XbrlConst.parentChild, "pres")
106
+ self.roleMap.getPrefix(XbrlConst.dimensionDefault, "d-d")
107
+ self.roleMap.getPrefix(WIDER_NARROWER_ARCROLE, "w-n")
108
+
109
+ self.sourceReportsByFiles = dict()
110
+ self.iv = iXBRLViewer(cntlr)
111
+ if self.useStubViewer:
112
+ self.iv.addFile(iXBRLViewerFile(DEFAULT_OUTPUT_NAME, self.getStubDocument()))
113
+
114
+ self.fromSingleZIP = None
115
+ self.reportCount = 0
97
116
 
98
117
  def enableFeature(self, featureName: str):
99
118
  if featureName in self.taxonomyData["features"]:
@@ -190,7 +209,7 @@ class IXBRLViewerBuilder:
190
209
 
191
210
  for baseSetKey, baseSetModelLinks in report.baseSets.items():
192
211
  arcrole, ELR, linkqname, arcqname = baseSetKey
193
- if arcrole in (XbrlConst.summationItem, WIDER_NARROWER_ARCROLE, XbrlConst.parentChild, XbrlConst.dimensionDefault) and ELR is not None:
212
+ if arcrole in (XbrlConst.summationItem, XbrlConst.summationItem11, WIDER_NARROWER_ARCROLE, XbrlConst.parentChild, XbrlConst.dimensionDefault) and ELR is not None:
194
213
  self.addELR(report, ELR)
195
214
  rr = dict()
196
215
  relSet = report.relationshipSet(arcrole, ELR)
@@ -210,7 +229,7 @@ class IXBRLViewerBuilder:
210
229
  return rels
211
230
 
212
231
  def validationErrors(self):
213
- logHandler = self.logger_model.modelManager.cntlr.logHandler
232
+ logHandler = self.cntlr.logHandler
214
233
  if getattr(logHandler, "logRecordBuffer") is None:
215
234
  raise IXBRLViewerBuilderError("Logging is not configured to use a buffer. Unable to retrieve validation messages")
216
235
 
@@ -326,14 +345,13 @@ class IXBRLViewerBuilder:
326
345
  return numeratorsString
327
346
 
328
347
  def addViewerData(self, viewerFile, scriptUrl):
329
- viewerFile.xmlDocument = deepcopy(viewerFile.xmlDocument)
330
348
  taxonomyDataJSON = self.escapeJSONForScriptTag(json.dumps(self.taxonomyData, indent=1, allow_nan=False))
331
349
 
332
350
  for child in viewerFile.xmlDocument.getroot():
333
351
  if child.tag == '{http://www.w3.org/1999/xhtml}body':
334
352
  for body_child in child:
335
353
  if body_child.tag == '{http://www.w3.org/1999/xhtml}script' and body_child.get('type','') == 'application/x.ixbrl-viewer+json':
336
- self.logger_model.error(ERROR_MESSAGE_CODE, "File already contains iXBRL viewer")
354
+ self.cntlr.addToLog("File already contains iXBRL viewer", messageCode="error")
337
355
  return False
338
356
 
339
357
  child.append(etree.Comment("BEGIN IXBRL VIEWER EXTENSIONS"))
@@ -375,110 +393,103 @@ class IXBRLViewerBuilder:
375
393
  self.taxonomyData["sourceReports"].append(sourceReport)
376
394
  return sourceReport
377
395
 
396
+ def processModel(
397
+ self,
398
+ report: ModelXbrl
399
+ ):
400
+
401
+ self.footnoteRelationshipSet = ModelRelationshipSet(report, "XBRL-footnotes")
402
+ self.currentTargetReport = self.newTargetReport(getattr(report, "ixdsTarget", None))
403
+ softwareCredits = set()
404
+ for document in report.urlDocs.values():
405
+ if document.type not in (Type.INLINEXBRL, Type.INLINEXBRLDOCUMENTSET):
406
+ continue
407
+ matches = document.creationSoftwareMatches(document.creationSoftwareComment)
408
+ softwareCredits.update(matches)
409
+ if softwareCredits:
410
+ self.currentTargetReport["softwareCredits"] = list(softwareCredits)
411
+ for f in report.facts:
412
+ self.addFact(report, f)
413
+ self.currentTargetReport["rels"] = self.getRelationships(report)
414
+
415
+ docSetFiles = None
416
+ self.reportCount += 1
417
+ report.info(INFO_MESSAGE_CODE, "Creating iXBRL viewer (%d) [%s]" % (self.reportCount, self.currentTargetReport["target"]))
418
+ if report.modelDocument.type == Type.INLINEXBRLDOCUMENTSET:
419
+ # Sort by object index to preserve order in which files were specified.
420
+ xmlDocsByFilename = {
421
+ os.path.basename(self.outputFilename(doc.filepath)): doc.xmlDocument
422
+ for doc in sorted(report.modelDocument.referencesDocument.keys(), key=lambda x: x.objectIndex)
423
+ if doc.type == Type.INLINEXBRL
424
+ }
425
+ docSetFiles = list(xmlDocsByFilename.keys())
426
+
427
+ for filename, docSetXMLDoc in xmlDocsByFilename.items():
428
+ self.iv.addFile(iXBRLViewerFile(filename, docSetXMLDoc))
429
+
430
+ elif self.useStubViewer:
431
+ filename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
432
+ docSetFiles = [ filename ]
433
+ self.iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
434
+
435
+ else:
436
+ srcFilename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
437
+ docSetFiles = [ srcFilename ]
438
+ filename = srcFilename
439
+ self.iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
440
+
441
+ docSetKey = frozenset(docSetFiles)
442
+ sourceReport = self.sourceReportsByFiles.get(docSetKey)
443
+ if sourceReport is None:
444
+ sourceReport = self.addSourceReport()
445
+ self.sourceReportsByFiles[docSetKey] = sourceReport
446
+ sourceReport["docSetFiles"] = list(urllib.parse.quote(f) for f in docSetFiles)
447
+
448
+ sourceReport["targetReports"].append(self.currentTargetReport)
449
+
450
+ localDocs = defaultdict(set)
451
+ for path, doc in report.urlDocs.items():
452
+ if isHttpUrl(path) or doc.type == Type.INLINEXBRLDOCUMENTSET:
453
+ continue
454
+ if doc.type == Type.INLINEXBRL:
455
+ localDocs[doc.basename].add('inline')
456
+ elif doc.type == Type.SCHEMA:
457
+ localDocs[doc.basename].add('schema')
458
+ elif doc.type == Type.LINKBASE:
459
+ linkbaseIdentifed = False
460
+ for child in doc.xmlRootElement.iterchildren():
461
+ linkbaseLocalDocumentsKey = LINK_QNAME_TO_LOCAL_DOCUMENTS_LINKBASE_TYPE.get(child.qname)
462
+ if linkbaseLocalDocumentsKey is not None:
463
+ localDocs[doc.basename].add(linkbaseLocalDocumentsKey)
464
+ linkbaseIdentifed = True
465
+ if not linkbaseIdentifed:
466
+ localDocs[doc.basename].add(UNRECOGNIZED_LINKBASE_LOCAL_DOCUMENTS_TYPE)
467
+ self.currentTargetReport["localDocs"] = {
468
+ localDoc: sorted(docTypes)
469
+ for localDoc, docTypes in localDocs.items()
470
+ }
471
+
472
+ # If we only process a single ZIP, add a download link to it as the
473
+ # "filing documents" on the viewer menu.
474
+ if self.fromSingleZIP is None:
475
+ self.fromSingleZIP = report.modelDocument.filepath.endswith(".zip")
476
+ if self.fromSingleZIP:
477
+ self.filingDocZipPath = os.path.dirname(report.modelDocument.filepath)
478
+ else:
479
+ self.fromSingleZIP = False
480
+
378
481
  def createViewer(
379
482
  self,
380
483
  scriptUrl: str = DEFAULT_JS_FILENAME,
381
- useStubViewer: bool = False,
382
484
  showValidations: bool = True,
383
485
  packageDownloadURL: str | None = None,
384
486
  ) -> iXBRLViewer | None:
385
487
  """
386
488
  Create an iXBRL file with XBRL data as a JSON blob, and script tags added.
387
489
  :param scriptUrl: The `src` value of the script tag that loads the viewer script.
388
- :param useStubViewer: True if stub document should be included in output.
389
490
  :param showValidations: True if validation errors should be included in output taxonomy data.
390
491
  :return: An iXBRLViewer instance that is ready to be saved.
391
492
  """
392
- # This "dts" is only used for logging
393
- iv = iXBRLViewer(self.reports[0])
394
- self.idGen = 0
395
- self.roleMap.getPrefix(XbrlConst.standardLabel, "std")
396
- self.roleMap.getPrefix(XbrlConst.documentationLabel, "doc")
397
- self.roleMap.getPrefix(XbrlConst.summationItem, "calc")
398
- self.roleMap.getPrefix(XbrlConst.parentChild, "pres")
399
- self.roleMap.getPrefix(XbrlConst.dimensionDefault, "d-d")
400
- self.roleMap.getPrefix(WIDER_NARROWER_ARCROLE, "w-n")
401
-
402
- sourceReportsByFiles = dict()
403
-
404
- if useStubViewer:
405
- iv.addFile(iXBRLViewerFile(DEFAULT_OUTPUT_NAME, self.getStubDocument()))
406
-
407
- for n, report in enumerate(self.reports):
408
- self.footnoteRelationshipSet = ModelRelationshipSet(report, "XBRL-footnotes")
409
- self.currentTargetReport = self.newTargetReport(getattr(report, "ixdsTarget", None))
410
- softwareCredits = set()
411
- for document in report.urlDocs.values():
412
- if document.type not in (Type.INLINEXBRL, Type.INLINEXBRLDOCUMENTSET):
413
- continue
414
- matches = document.creationSoftwareMatches(document.creationSoftwareComment)
415
- softwareCredits.update(matches)
416
- if softwareCredits:
417
- self.currentTargetReport["softwareCredits"] = list(softwareCredits)
418
- for f in report.facts:
419
- self.addFact(report, f)
420
- self.currentTargetReport["rels"] = self.getRelationships(report)
421
-
422
- docSetFiles = None
423
- report.info(INFO_MESSAGE_CODE, "Creating iXBRL viewer (%d of %d)" % (n+1, len(self.reports)))
424
- if report.modelDocument.type == Type.INLINEXBRLDOCUMENTSET:
425
- # Sort by object index to preserve order in which files were specified.
426
- xmlDocsByFilename = {
427
- os.path.basename(self.outputFilename(doc.filepath)): doc.xmlDocument
428
- for doc in sorted(report.modelDocument.referencesDocument.keys(), key=lambda x: x.objectIndex)
429
- if doc.type == Type.INLINEXBRL
430
- }
431
- docSetFiles = list(xmlDocsByFilename.keys())
432
-
433
- for filename, docSetXMLDoc in xmlDocsByFilename.items():
434
- iv.addFile(iXBRLViewerFile(filename, docSetXMLDoc))
435
-
436
- elif useStubViewer:
437
- filename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
438
- docSetFiles = [ filename ]
439
- iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
440
-
441
- else:
442
- srcFilename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
443
- docSetFiles = [ srcFilename ]
444
- if len(self.reports) == 1:
445
- # If there is only a single report, call the output file "xbrlviewer.html"
446
- filename = "xbrlviewer.html"
447
- else:
448
- # Otherwise, preserve filenames
449
- filename = srcFilename
450
- iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
451
-
452
- docSetKey = frozenset(docSetFiles)
453
- sourceReport = sourceReportsByFiles.get(docSetKey)
454
- if sourceReport is None:
455
- sourceReport = self.addSourceReport()
456
- sourceReportsByFiles[docSetKey] = sourceReport
457
- sourceReport["docSetFiles"] = list(urllib.parse.quote(f) for f in docSetFiles)
458
-
459
- sourceReport["targetReports"].append(self.currentTargetReport)
460
-
461
- localDocs = defaultdict(set)
462
- for path, doc in report.urlDocs.items():
463
- if isHttpUrl(path) or doc.type == Type.INLINEXBRLDOCUMENTSET:
464
- continue
465
- if doc.type == Type.INLINEXBRL:
466
- localDocs[doc.basename].add('inline')
467
- elif doc.type == Type.SCHEMA:
468
- localDocs[doc.basename].add('schema')
469
- elif doc.type == Type.LINKBASE:
470
- linkbaseIdentifed = False
471
- for child in doc.xmlRootElement.iterchildren():
472
- linkbaseLocalDocumentsKey = LINK_QNAME_TO_LOCAL_DOCUMENTS_LINKBASE_TYPE.get(child.qname)
473
- if linkbaseLocalDocumentsKey is not None:
474
- localDocs[doc.basename].add(linkbaseLocalDocumentsKey)
475
- linkbaseIdentifed = True
476
- if not linkbaseIdentifed:
477
- localDocs[doc.basename].add(UNRECOGNIZED_LINKBASE_LOCAL_DOCUMENTS_TYPE)
478
- self.currentTargetReport["localDocs"] = {
479
- localDoc: sorted(docTypes)
480
- for localDoc, docTypes in localDocs.items()
481
- }
482
493
 
483
494
  self.taxonomyData["prefixes"] = self.nsmap.prefixmap
484
495
  self.taxonomyData["roles"] = self.roleMap.prefixmap
@@ -488,32 +499,42 @@ class IXBRLViewerBuilder:
488
499
 
489
500
  if packageDownloadURL is not None:
490
501
  self.taxonomyData["filingDocuments"] = packageDownloadURL
491
- elif len(self.reports) == 1 and os.path.dirname(self.reports[0].modelDocument.filepath).endswith('.zip'):
492
- filingDocZipPath = os.path.dirname(self.reports[0].modelDocument.filepath)
493
- filingDocZipName = os.path.basename(filingDocZipPath)
494
- iv.addFilingDoc(filingDocZipPath)
502
+ elif self.fromSingleZIP:
503
+ filingDocZipName = os.path.basename(self.filingDocZipPath)
504
+ self.iv.addFilingDoc(self.filingDocZipPath)
495
505
  self.taxonomyData["filingDocuments"] = filingDocZipName
496
506
 
497
- if not self.addViewerData(iv.files[0], scriptUrl):
507
+ if not self.addViewerData(self.iv.files[0], scriptUrl):
498
508
  return None
499
509
 
500
- return iv
510
+ if len(self.iv.files) == 1:
511
+ # If there is only a single report, call the output file "xbrlviewer.html"
512
+ # We should probably preserve the source file extension here.
513
+ self.iv.files[0].filename = 'xbrlviewer.html'
514
+
515
+ return self.iv
501
516
 
502
517
 
503
518
  class iXBRLViewerFile:
504
519
 
505
520
  def __init__(self, filename, xmlDocument):
506
521
  self.filename = filename
507
- self.xmlDocument = xmlDocument
522
+ self.xmlDocument = deepcopy(xmlDocument)
523
+ # deepcopy does not retain the Python proxies, so iterating the node
524
+ # tree during serialization will create new ones. However, the original
525
+ # ModelObjectFactory is still referenced, and that references a
526
+ # ModelXbrl that will potentially be closed by the time we serialize.
527
+ # Serialization only requires standard XML features, so the default
528
+ # lxml.etree classes (and thus lookup) are fine.
529
+ self.xmlDocument.parser.set_element_class_lookup(etree.ElementDefaultClassLookup())
508
530
 
509
531
 
510
532
  class iXBRLViewer:
511
533
 
512
- def __init__(self, logger_model: ModelXbrl):
534
+ def __init__(self, cntlr: Cntlr):
513
535
  self.files = []
514
536
  self.filingDocuments = None
515
- # This is an arbitrary ModelXbrl used for logging only
516
- self.logger_model = logger_model
537
+ self.cntlr = cntlr
517
538
  self.filenames = set()
518
539
 
519
540
  def addFile(self, ivf):
@@ -543,15 +564,15 @@ class iXBRLViewer:
543
564
  fileMode = 'w'
544
565
  elif destination.endswith(os.sep):
545
566
  # Looks like a directory, but isn't one
546
- self.logger_model.error(ERROR_MESSAGE_CODE, "Directory %s does not exist" % destination)
567
+ self.cntlr.addToLog("Directory %s does not exist" % destination, messageCode=ERROR_MESSAGE_CODE)
547
568
  return
548
569
  elif not os.path.isdir(os.path.dirname(os.path.abspath(destination))):
549
570
  # Directory part of filename doesn't exist
550
- self.logger_model.error(ERROR_MESSAGE_CODE, "Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)))
571
+ self.cntlr.addToLog("Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)), messageCode=ERROR_MESSAGE_CODE)
551
572
  return
552
573
  elif not destination.endswith('.zip'):
553
574
  # File extension isn't a zip
554
- self.logger_model.error(ERROR_MESSAGE_CODE, "File extension %s is not a zip" % os.path.splitext(destination)[0])
575
+ self.cntlr.addToLog("File extension %s is not a zip" % os.path.splitext(destination)[0], messageCode=ERROR_MESSAGE_CODE)
555
576
  return
556
577
  else:
557
578
  file = destination
@@ -559,49 +580,49 @@ class iXBRLViewer:
559
580
 
560
581
  with zipfile.ZipFile(file, fileMode, zipfile.ZIP_DEFLATED, True) as zout:
561
582
  for f in self.files:
562
- self.logger_model.info(INFO_MESSAGE_CODE, "Saving in output zip %s" % f.filename)
583
+ self.cntlr.addToLog("Saving in output zip %s" % f.filename, messageCode=INFO_MESSAGE_CODE)
563
584
  with zout.open(f.filename, "w") as fout:
564
585
  writer = XHTMLSerializer(fout)
565
586
  writer.serialize(f.xmlDocument)
566
587
  if self.filingDocuments:
567
588
  filename = os.path.basename(self.filingDocuments)
568
- self.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
589
+ self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
569
590
  zout.write(self.filingDocuments, filename)
570
591
  if copyScriptPath is not None:
571
- self.logger_model.info(INFO_MESSAGE_CODE, f"Writing script from {copyScriptPath}")
592
+ self.cntlr.addToLog(f"Writing script from {copyScriptPath}", messageCode=INFO_MESSAGE_CODE)
572
593
  zout.write(copyScriptPath, copyScriptPath.name)
573
594
  elif os.path.isdir(destination):
574
595
  # If output is a directory, write each file in the doc set to that
575
596
  # directory using its existing filename
576
597
  for f in self.files:
577
598
  filename = os.path.join(destination, f.filename)
578
- self.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
599
+ self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
579
600
  with open(filename, "wb") as fout:
580
601
  writer = XHTMLSerializer(fout)
581
602
  writer.serialize(f.xmlDocument)
582
603
  if self.filingDocuments:
583
604
  filename = os.path.basename(self.filingDocuments)
584
- self.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
605
+ self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
585
606
  shutil.copy2(self.filingDocuments, os.path.join(destination, filename))
586
607
  if copyScriptPath is not None:
587
608
  self._copyScript(Path(destination), copyScriptPath)
588
609
  else:
589
610
  if len(self.files) > 1:
590
- self.logger_model.error(ERROR_MESSAGE_CODE, "More than one file in input, but output is not a directory")
611
+ self.cntlr.addToLog("More than one file in input, but output is not a directory", messageCode=ERROR_MESSAGE_CODE)
591
612
  elif destination.endswith(os.sep):
592
613
  # Looks like a directory, but isn't one
593
- self.logger_model.error(ERROR_MESSAGE_CODE, "Directory %s does not exist" % destination)
614
+ self.cntlr.addToLog("Directory %s does not exist" % destination, messageCode=ERROR_MESSAGE_CODE)
594
615
  elif not os.path.isdir(os.path.dirname(os.path.abspath(destination))):
595
616
  # Directory part of filename doesn't exist
596
- self.logger_model.error(ERROR_MESSAGE_CODE, "Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)))
617
+ self.cntlr.addToLog("Directory %s does not exist" % os.path.dirname(os.path.abspath(destination)), messageCode=ERROR_MESSAGE_CODE)
597
618
  else:
598
- self.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % destination)
619
+ self.cntlr.addToLog("Writing %s" % destination, messageCode=INFO_MESSAGE_CODE)
599
620
  with open(destination, "wb") as fout:
600
621
  writer = XHTMLSerializer(fout)
601
622
  writer.serialize(self.files[0].xmlDocument)
602
623
  if self.filingDocuments:
603
624
  filename = os.path.basename(self.filingDocuments)
604
- self.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
625
+ self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
605
626
  shutil.copy2(self.filingDocuments, os.path.join(os.path.dirname(destination), filename))
606
627
  if copyScriptPath is not None:
607
628
  outDirectory = Path(destination).parent
@@ -610,5 +631,5 @@ class iXBRLViewer:
610
631
  def _copyScript(self, destDirectory: Path, scriptPath: Path):
611
632
  scriptDest = destDirectory / scriptPath.name
612
633
  if scriptPath != scriptDest:
613
- self.logger_model.info(INFO_MESSAGE_CODE, f"Copying script from {scriptDest} to {scriptDest}.")
634
+ self.cntlr.addToLog(f"Copying script from {scriptPath} to {scriptDest}.", messageCode=INFO_MESSAGE_CODE)
614
635
  shutil.copy2(scriptPath, scriptDest)
@@ -0,0 +1,7 @@
1
+ from arelle.utils.PluginData import PluginData
2
+ from .iXBRLViewer import IXBRLViewerBuilder
3
+ from typing import Optional
4
+
5
+ class IXBRLViewerPluginData(PluginData): # type: ignore[misc]
6
+
7
+ builder: Optional[IXBRLViewerBuilder] = None