ixbrl-viewer 1.4.29__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.29'
16
- __version_tuple__ = version_tuple = (1, 4, 29)
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"]:
@@ -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,111 +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.summationItem11, "calc11")
399
- self.roleMap.getPrefix(XbrlConst.parentChild, "pres")
400
- self.roleMap.getPrefix(XbrlConst.dimensionDefault, "d-d")
401
- self.roleMap.getPrefix(WIDER_NARROWER_ARCROLE, "w-n")
402
-
403
- sourceReportsByFiles = dict()
404
-
405
- if useStubViewer:
406
- iv.addFile(iXBRLViewerFile(DEFAULT_OUTPUT_NAME, self.getStubDocument()))
407
-
408
- for n, report in enumerate(self.reports):
409
- self.footnoteRelationshipSet = ModelRelationshipSet(report, "XBRL-footnotes")
410
- self.currentTargetReport = self.newTargetReport(getattr(report, "ixdsTarget", None))
411
- softwareCredits = set()
412
- for document in report.urlDocs.values():
413
- if document.type not in (Type.INLINEXBRL, Type.INLINEXBRLDOCUMENTSET):
414
- continue
415
- matches = document.creationSoftwareMatches(document.creationSoftwareComment)
416
- softwareCredits.update(matches)
417
- if softwareCredits:
418
- self.currentTargetReport["softwareCredits"] = list(softwareCredits)
419
- for f in report.facts:
420
- self.addFact(report, f)
421
- self.currentTargetReport["rels"] = self.getRelationships(report)
422
-
423
- docSetFiles = None
424
- report.info(INFO_MESSAGE_CODE, "Creating iXBRL viewer (%d of %d)" % (n+1, len(self.reports)))
425
- if report.modelDocument.type == Type.INLINEXBRLDOCUMENTSET:
426
- # Sort by object index to preserve order in which files were specified.
427
- xmlDocsByFilename = {
428
- os.path.basename(self.outputFilename(doc.filepath)): doc.xmlDocument
429
- for doc in sorted(report.modelDocument.referencesDocument.keys(), key=lambda x: x.objectIndex)
430
- if doc.type == Type.INLINEXBRL
431
- }
432
- docSetFiles = list(xmlDocsByFilename.keys())
433
-
434
- for filename, docSetXMLDoc in xmlDocsByFilename.items():
435
- iv.addFile(iXBRLViewerFile(filename, docSetXMLDoc))
436
-
437
- elif useStubViewer:
438
- filename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
439
- docSetFiles = [ filename ]
440
- iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
441
-
442
- else:
443
- srcFilename = self.outputFilename(os.path.basename(report.modelDocument.filepath))
444
- docSetFiles = [ srcFilename ]
445
- if len(self.reports) == 1:
446
- # If there is only a single report, call the output file "xbrlviewer.html"
447
- filename = "xbrlviewer.html"
448
- else:
449
- # Otherwise, preserve filenames
450
- filename = srcFilename
451
- iv.addFile(iXBRLViewerFile(filename, report.modelDocument.xmlDocument))
452
-
453
- docSetKey = frozenset(docSetFiles)
454
- sourceReport = sourceReportsByFiles.get(docSetKey)
455
- if sourceReport is None:
456
- sourceReport = self.addSourceReport()
457
- sourceReportsByFiles[docSetKey] = sourceReport
458
- sourceReport["docSetFiles"] = list(urllib.parse.quote(f) for f in docSetFiles)
459
-
460
- sourceReport["targetReports"].append(self.currentTargetReport)
461
-
462
- localDocs = defaultdict(set)
463
- for path, doc in report.urlDocs.items():
464
- if isHttpUrl(path) or doc.type == Type.INLINEXBRLDOCUMENTSET:
465
- continue
466
- if doc.type == Type.INLINEXBRL:
467
- localDocs[doc.basename].add('inline')
468
- elif doc.type == Type.SCHEMA:
469
- localDocs[doc.basename].add('schema')
470
- elif doc.type == Type.LINKBASE:
471
- linkbaseIdentifed = False
472
- for child in doc.xmlRootElement.iterchildren():
473
- linkbaseLocalDocumentsKey = LINK_QNAME_TO_LOCAL_DOCUMENTS_LINKBASE_TYPE.get(child.qname)
474
- if linkbaseLocalDocumentsKey is not None:
475
- localDocs[doc.basename].add(linkbaseLocalDocumentsKey)
476
- linkbaseIdentifed = True
477
- if not linkbaseIdentifed:
478
- localDocs[doc.basename].add(UNRECOGNIZED_LINKBASE_LOCAL_DOCUMENTS_TYPE)
479
- self.currentTargetReport["localDocs"] = {
480
- localDoc: sorted(docTypes)
481
- for localDoc, docTypes in localDocs.items()
482
- }
483
493
 
484
494
  self.taxonomyData["prefixes"] = self.nsmap.prefixmap
485
495
  self.taxonomyData["roles"] = self.roleMap.prefixmap
@@ -489,32 +499,42 @@ class IXBRLViewerBuilder:
489
499
 
490
500
  if packageDownloadURL is not None:
491
501
  self.taxonomyData["filingDocuments"] = packageDownloadURL
492
- elif len(self.reports) == 1 and os.path.dirname(self.reports[0].modelDocument.filepath).endswith('.zip'):
493
- filingDocZipPath = os.path.dirname(self.reports[0].modelDocument.filepath)
494
- filingDocZipName = os.path.basename(filingDocZipPath)
495
- iv.addFilingDoc(filingDocZipPath)
502
+ elif self.fromSingleZIP:
503
+ filingDocZipName = os.path.basename(self.filingDocZipPath)
504
+ self.iv.addFilingDoc(self.filingDocZipPath)
496
505
  self.taxonomyData["filingDocuments"] = filingDocZipName
497
506
 
498
- if not self.addViewerData(iv.files[0], scriptUrl):
507
+ if not self.addViewerData(self.iv.files[0], scriptUrl):
499
508
  return None
500
509
 
501
- 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
502
516
 
503
517
 
504
518
  class iXBRLViewerFile:
505
519
 
506
520
  def __init__(self, filename, xmlDocument):
507
521
  self.filename = filename
508
- 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())
509
530
 
510
531
 
511
532
  class iXBRLViewer:
512
533
 
513
- def __init__(self, logger_model: ModelXbrl):
534
+ def __init__(self, cntlr: Cntlr):
514
535
  self.files = []
515
536
  self.filingDocuments = None
516
- # This is an arbitrary ModelXbrl used for logging only
517
- self.logger_model = logger_model
537
+ self.cntlr = cntlr
518
538
  self.filenames = set()
519
539
 
520
540
  def addFile(self, ivf):
@@ -544,15 +564,15 @@ class iXBRLViewer:
544
564
  fileMode = 'w'
545
565
  elif destination.endswith(os.sep):
546
566
  # Looks like a directory, but isn't one
547
- 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)
548
568
  return
549
569
  elif not os.path.isdir(os.path.dirname(os.path.abspath(destination))):
550
570
  # Directory part of filename doesn't exist
551
- 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)
552
572
  return
553
573
  elif not destination.endswith('.zip'):
554
574
  # File extension isn't a zip
555
- 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)
556
576
  return
557
577
  else:
558
578
  file = destination
@@ -560,49 +580,49 @@ class iXBRLViewer:
560
580
 
561
581
  with zipfile.ZipFile(file, fileMode, zipfile.ZIP_DEFLATED, True) as zout:
562
582
  for f in self.files:
563
- 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)
564
584
  with zout.open(f.filename, "w") as fout:
565
585
  writer = XHTMLSerializer(fout)
566
586
  writer.serialize(f.xmlDocument)
567
587
  if self.filingDocuments:
568
588
  filename = os.path.basename(self.filingDocuments)
569
- self.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
589
+ self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
570
590
  zout.write(self.filingDocuments, filename)
571
591
  if copyScriptPath is not None:
572
- 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)
573
593
  zout.write(copyScriptPath, copyScriptPath.name)
574
594
  elif os.path.isdir(destination):
575
595
  # If output is a directory, write each file in the doc set to that
576
596
  # directory using its existing filename
577
597
  for f in self.files:
578
598
  filename = os.path.join(destination, f.filename)
579
- self.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
599
+ self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
580
600
  with open(filename, "wb") as fout:
581
601
  writer = XHTMLSerializer(fout)
582
602
  writer.serialize(f.xmlDocument)
583
603
  if self.filingDocuments:
584
604
  filename = os.path.basename(self.filingDocuments)
585
- self.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
605
+ self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
586
606
  shutil.copy2(self.filingDocuments, os.path.join(destination, filename))
587
607
  if copyScriptPath is not None:
588
608
  self._copyScript(Path(destination), copyScriptPath)
589
609
  else:
590
610
  if len(self.files) > 1:
591
- 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)
592
612
  elif destination.endswith(os.sep):
593
613
  # Looks like a directory, but isn't one
594
- 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)
595
615
  elif not os.path.isdir(os.path.dirname(os.path.abspath(destination))):
596
616
  # Directory part of filename doesn't exist
597
- 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)
598
618
  else:
599
- self.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % destination)
619
+ self.cntlr.addToLog("Writing %s" % destination, messageCode=INFO_MESSAGE_CODE)
600
620
  with open(destination, "wb") as fout:
601
621
  writer = XHTMLSerializer(fout)
602
622
  writer.serialize(self.files[0].xmlDocument)
603
623
  if self.filingDocuments:
604
624
  filename = os.path.basename(self.filingDocuments)
605
- self.logger_model.info(INFO_MESSAGE_CODE, "Writing %s" % filename)
625
+ self.cntlr.addToLog("Writing %s" % filename, messageCode=INFO_MESSAGE_CODE)
606
626
  shutil.copy2(self.filingDocuments, os.path.join(os.path.dirname(destination), filename))
607
627
  if copyScriptPath is not None:
608
628
  outDirectory = Path(destination).parent
@@ -611,5 +631,5 @@ class iXBRLViewer:
611
631
  def _copyScript(self, destDirectory: Path, scriptPath: Path):
612
632
  scriptDest = destDirectory / scriptPath.name
613
633
  if scriptPath != scriptDest:
614
- 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)
615
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