ixbrl-viewer 1.4.29__py3-none-any.whl → 1.4.31__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.

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