arelle-release 2.37.71__py3-none-any.whl → 2.38.0__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.
Files changed (71) hide show
  1. arelle/BetaFeatures.py +0 -21
  2. arelle/Cntlr.py +7 -1
  3. arelle/CntlrCmdLine.py +95 -19
  4. arelle/CntlrWinMain.py +4 -1
  5. arelle/DialogFind.py +1 -1
  6. arelle/ModelDtsObject.py +2 -0
  7. arelle/ModelObject.py +16 -18
  8. arelle/ModelObjectFactory.py +17 -15
  9. arelle/ModelXbrl.py +7 -1
  10. arelle/PluginManager.py +1 -5
  11. arelle/RuntimeOptions.py +1 -0
  12. arelle/UrlUtil.py +11 -0
  13. arelle/Validate.py +3 -3
  14. arelle/ValidateXbrl.py +2 -1
  15. arelle/ValidateXbrlCalcs.py +210 -186
  16. arelle/WebCache.py +2 -8
  17. arelle/XbrlConst.py +2 -0
  18. arelle/XmlUtil.py +16 -21
  19. arelle/XmlValidate.py +4 -6
  20. arelle/_version.py +2 -2
  21. arelle/config/rosettaEntitlements.plist +8 -0
  22. arelle/conformance/CSVTestcaseLoader.py +1 -1
  23. arelle/formula/XPathContext.py +3 -3
  24. arelle/logging/formatters/LogFormatter.py +3 -1
  25. arelle/packages/report/ReportPackage.py +9 -1
  26. arelle/plugin/inlineXbrlDocumentSet.py +1 -3
  27. arelle/plugin/validate/DBA/DisclosureSystems.py +19 -1
  28. arelle/plugin/validate/DBA/resources/config.xml +5 -0
  29. arelle/plugin/validate/DBA/rules/fr.py +19 -2
  30. arelle/plugin/validate/DBA/rules/tc.py +2 -0
  31. arelle/plugin/validate/DBA/rules/th.py +6 -0
  32. arelle/plugin/validate/DBA/rules/tm.py +18 -5
  33. arelle/plugin/validate/DBA/rules/tr.py +11 -5
  34. arelle/plugin/validate/EDINET/ControllerPluginData.py +2 -1
  35. arelle/plugin/validate/EDINET/NamespaceConfig.py +50 -0
  36. arelle/plugin/validate/EDINET/PluginValidationDataExtension.py +33 -78
  37. arelle/plugin/validate/EDINET/TableOfContentsBuilder.py +153 -51
  38. arelle/plugin/validate/EDINET/rules/contexts.py +1 -1
  39. arelle/plugin/validate/EDINET/rules/edinet.py +163 -20
  40. arelle/plugin/validate/EDINET/rules/gfm.py +88 -1
  41. arelle/plugin/validate/EDINET/rules/upload.py +1 -1
  42. arelle/plugin/validate/ESEF/ESEF_2021/ValidateXbrlFinally.py +3 -3
  43. arelle/plugin/validate/ESEF/ESEF_Current/DTS.py +42 -14
  44. arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py +14 -3
  45. arelle/plugin/validate/ESEF/__init__.py +10 -5
  46. arelle/plugin/validate/ESEF/resources/authority-validations.json +10 -5
  47. arelle/plugin/validate/NL/DisclosureSystems.py +22 -0
  48. arelle/plugin/validate/NL/PluginValidationDataExtension.py +20 -0
  49. arelle/plugin/validate/NL/ValidationPluginExtension.py +48 -3
  50. arelle/plugin/validate/NL/resources/config.xml +18 -0
  51. arelle/plugin/validate/NL/rules/br_kvk.py +9 -54
  52. arelle/plugin/validate/NL/rules/fg_nl.py +7 -38
  53. arelle/plugin/validate/NL/rules/fr_kvk.py +7 -42
  54. arelle/plugin/validate/NL/rules/fr_nl.py +25 -140
  55. arelle/plugin/validate/NL/rules/nl_kvk.py +125 -12
  56. arelle/plugin/validate/ROS/rules/ros.py +3 -1
  57. arelle/plugin/validate/UK/__init__.py +70 -14
  58. arelle/utils/EntryPointDetection.py +17 -11
  59. arelle/utils/validate/ESEFImage.py +3 -3
  60. arelle/utils/validate/Validation.py +9 -0
  61. arelle/utils/validate/ValidationPlugin.py +14 -12
  62. {arelle_release-2.37.71.dist-info → arelle_release-2.38.0.dist-info}/METADATA +10 -5
  63. {arelle_release-2.37.71.dist-info → arelle_release-2.38.0.dist-info}/RECORD +67 -69
  64. {arelle_release-2.37.71.dist-info → arelle_release-2.38.0.dist-info}/licenses/LICENSE.md +0 -3
  65. arelle/model/CommentBase.py +0 -9
  66. arelle/model/ElementBase.py +0 -11
  67. arelle/model/PIBase.py +0 -10
  68. arelle/model/__init__.py +0 -15
  69. {arelle_release-2.37.71.dist-info → arelle_release-2.38.0.dist-info}/WHEEL +0 -0
  70. {arelle_release-2.37.71.dist-info → arelle_release-2.38.0.dist-info}/entry_points.txt +0 -0
  71. {arelle_release-2.37.71.dist-info → arelle_release-2.38.0.dist-info}/top_level.txt +0 -0
arelle/BetaFeatures.py CHANGED
@@ -3,27 +3,6 @@ See COPYRIGHT.md for copyright information.
3
3
  """
4
4
  from __future__ import annotations
5
5
 
6
- BETA_OBJECT_MODEL_FEATURE = "betaObjectModel"
7
6
  # Add camelCaseOptionName
8
7
  BETA_FEATURES_AND_DESCRIPTIONS: dict[str, str] = {
9
- BETA_OBJECT_MODEL_FEATURE: "Replace lxml based object model with a pure Python class hierarchy.",
10
8
  }
11
-
12
-
13
- _NEW_OBJECT_MODEL_STATUS_ACCESSED = False
14
- _USE_NEW_OBJECT_MODEL = False
15
-
16
-
17
- def enableNewObjectModel() -> None:
18
- global _USE_NEW_OBJECT_MODEL
19
- if _USE_NEW_OBJECT_MODEL:
20
- return
21
- if _NEW_OBJECT_MODEL_STATUS_ACCESSED:
22
- raise RuntimeError("Can't change object model transition setting after classes have already been defined.")
23
- _USE_NEW_OBJECT_MODEL = True
24
-
25
-
26
- def newObjectModelEnabled() -> bool:
27
- global _NEW_OBJECT_MODEL_STATUS_ACCESSED
28
- _NEW_OBJECT_MODEL_STATUS_ACCESSED = True
29
- return _USE_NEW_OBJECT_MODEL
arelle/Cntlr.py CHANGED
@@ -35,6 +35,7 @@ from arelle.logging.handlers.StructuredMessageLogHandler import StructuredMessag
35
35
  from arelle.SystemInfo import PlatformOS, getSystemWordSize, hasFileSystem, hasWebServer, isCGI, isGAE
36
36
  from arelle.typing import TypeGetText
37
37
  from arelle.utils.PluginData import PluginData
38
+ from arelle.utils.validate.Validation import Validation
38
39
  from arelle.WebCache import WebCache
39
40
 
40
41
  _: TypeGetText
@@ -166,7 +167,7 @@ class Cntlr:
166
167
  betaFeatures = {}
167
168
  self.betaFeatures = {
168
169
  b: betaFeatures.get(b, False)
169
- for b in BETA_FEATURES_AND_DESCRIPTIONS.keys()
170
+ for b in BETA_FEATURES_AND_DESCRIPTIONS
170
171
  }
171
172
  self.errorManager = None
172
173
  self.hasWin32gui = False
@@ -298,6 +299,11 @@ class Cntlr:
298
299
  self.startLogging(logFileName, logFileMode, logFileEncoding, logFormat)
299
300
  self.errorManager = ErrorManager(self.modelManager, logging._checkLevel("INCONSISTENCY")) # type: ignore[attr-defined]
300
301
 
302
+ def validation(self, val: Validation, fileSource: FileSource | None = None) -> None:
303
+ """Same as `error`, but parameters passed in from Validation object
304
+ """
305
+ self.error(codes=val.codes, msg=val.msg, level=val.level.name, fileSource=fileSource, **val.args)
306
+
301
307
  def error(self, codes: Any, msg: str, level: str = "ERROR", fileSource: FileSource | None = None, **args: Any) -> None:
302
308
  if self.logger is None or self.errorManager is None:
303
309
  self.addToLog(
arelle/CntlrCmdLine.py CHANGED
@@ -11,6 +11,7 @@ import datetime
11
11
  import fnmatch
12
12
  import gettext
13
13
  import glob
14
+ import json
14
15
  import logging
15
16
  import multiprocessing
16
17
  import os
@@ -97,6 +98,38 @@ def parseAndRun(args):
97
98
  return cntlr
98
99
 
99
100
 
101
+ PREPARSE_ARG_CONFIGS = frozenset([
102
+ (re.compile(r'^--plugins?.*$'), 'plugins'),
103
+ (re.compile(r'^--options(F|f)ile.*$'), 'optionsFile'),
104
+ ])
105
+
106
+
107
+ def preparseArgs(args: list[str], parser: OptionParser) -> dict[str, str]:
108
+ """
109
+ Some command line arguments influence the actual parsing of other arguments.
110
+ This function pre-parses those arguments to allow for processing before full
111
+ argument parseing occurs.
112
+ :param args: Command line arguments
113
+ :param parser: OptionParser to report errors
114
+ :return: Dictionary of pre-parsed options
115
+ """
116
+ preparsedArgs = {}
117
+ for i, arg in enumerate(args):
118
+ for pattern, preparsedArg in PREPARSE_ARG_CONFIGS:
119
+ if pattern.fullmatch(arg):
120
+ __, sep, value = arg.partition('=')
121
+ if sep: # --arg=value
122
+ preparsedValue = value
123
+ elif i < len(args) - 1: # --arg value
124
+ preparsedValue = args[i+1]
125
+ else: # --arg
126
+ preparsedValue = ""
127
+ if preparsedArg in preparsedArgs:
128
+ parser.error(_("Multiple '{}' values found during argument preparsing.").format(preparsedArg))
129
+ preparsedArgs[preparsedArg] = preparsedValue
130
+ return preparsedArgs
131
+
132
+
100
133
  def parseArgs(args):
101
134
  """
102
135
  Parses the command line arguments and generates runtimeOptions and arellePluginModules
@@ -411,24 +444,27 @@ def parseArgs(args):
411
444
  pluginOptionsIndex = len(parser.option_list)
412
445
  pluginOptionsGroupIndex = len(parser.option_groups)
413
446
 
447
+ preparsedArgs = preparseArgs(args, parser)
448
+
449
+ preloadPlugins = []
450
+ optionsFile = preparsedArgs.get('optionsFile')
451
+ optionsFileOptions = {}
452
+ if optionsFile:
453
+ optionsFileOptions = _parseOptionsFile(optionsFile, parser)
454
+ preloadPlugins.extend(optionsFileOptions.get('plugins', '').split('|'))
455
+
456
+ preloadPlugins.extend(preparsedArgs.get('plugins', '').split('|'))
457
+
414
458
  # install any dynamic plugins so their command line options can be parsed if present
415
459
  arellePluginModules = {}
416
- for i, arg in enumerate(args):
417
- if arg.startswith('--plugin'): # allow singular or plural (option must simply be non-ambiguous
418
- if len(arg) > 9 and arg[9] == '=':
419
- preloadPlugins = arg[10:]
420
- elif i < len(args) - 1:
421
- preloadPlugins = args[i+1]
422
- else:
423
- preloadPlugins = ""
424
- for pluginCmd in preloadPlugins.split('|'):
425
- cmd = pluginCmd.strip()
426
- if cmd not in ("show", "temp") and len(cmd) > 0 and cmd[0] not in ('-', '~', '+'):
427
- moduleInfo = PluginManager.addPluginModule(cmd)
428
- if moduleInfo:
429
- arellePluginModules[cmd] = moduleInfo
430
- PluginManager.reset()
431
- break
460
+ for pluginCmd in preloadPlugins:
461
+ cmd = pluginCmd.strip()
462
+ if cmd not in ("show", "temp") and len(cmd) > 0 and cmd[0] not in ('-', '~', '+'):
463
+ moduleInfo = PluginManager.addPluginModule(cmd)
464
+ if moduleInfo:
465
+ arellePluginModules[cmd] = moduleInfo
466
+ PluginManager.reset()
467
+
432
468
  # add plug-in options
433
469
  for optionsExtender in PluginManager.pluginClassMethods("CntlrCmdLine.Options"):
434
470
  optionsExtender(parser)
@@ -439,6 +475,10 @@ def parseArgs(args):
439
475
  help=_("Show product version, copyright, and license."))
440
476
  parser.add_option("--diagnostics", action="store_true", dest="diagnostics",
441
477
  help=_("output system diagnostics information"))
478
+ parser.add_option("--optionsFile", "--optionsfile",
479
+ action="store", dest="optionsFile",
480
+ help=_("Provide a path to a JSON file containing runtime options. "
481
+ "These options will be overridden by any command line options provided."))
442
482
 
443
483
  if not args and isGAE():
444
484
  args = ["--webserver=::gae"]
@@ -517,9 +557,22 @@ def parseArgs(args):
517
557
  for optGroup in parser.option_groups[pluginOptionsGroupIndex:pluginLastOptionsGroupIndex]:
518
558
  for groupOption in optGroup.option_list:
519
559
  pluginOptionDestinations.add(groupOption.dest)
560
+
520
561
  baseOptions = {}
521
- pluginOptions = {}
562
+ # Collect options from options file
563
+ for optionName, optionValue in optionsFileOptions.items():
564
+ if not hasattr(RuntimeOptions, optionName) and optionName not in pluginOptionDestinations:
565
+ parser.error(_("Unexpected name '{}' found in options file.").format(optionName))
566
+ continue
567
+ baseOptions[optionName] = optionValue
568
+ # Collect options from command line
522
569
  for optionName, optionValue in vars(options).items():
570
+ if optionName not in baseOptions or optionValue is not None:
571
+ baseOptions[optionName] = optionValue
572
+
573
+ pluginOptions = {}
574
+ finalOptions = {} # Validated options for RuntimeOptions
575
+ for optionName, optionValue in baseOptions.items():
523
576
  if optionName in pluginOptionDestinations:
524
577
  pluginOptions[optionName] = optionValue
525
578
  else:
@@ -531,9 +584,10 @@ def parseArgs(args):
531
584
  parser.error(_("--testcaseExpectedErrors must be in the format '--testcaseExpectedErrors=testcase-index.xml:v-1|errorCode1,errorCode2,...'"))
532
585
  expectedErrors[expectedErrorSplit[0]] = expectedErrorSplit[1].split(',')
533
586
  optionValue = expectedErrors
534
- baseOptions[optionName] = optionValue
587
+ if optionValue is not None or optionName not in finalOptions:
588
+ finalOptions[optionName] = optionValue
535
589
  try:
536
- runtimeOptions = RuntimeOptions(pluginOptions=pluginOptions, **baseOptions)
590
+ runtimeOptions = RuntimeOptions(pluginOptions=pluginOptions, **finalOptions)
537
591
  except RuntimeOptionsException as e:
538
592
  parser.error(f"{e}, please try\n python CntlrCmdLine.py --help")
539
593
  if (
@@ -626,6 +680,28 @@ def _pluginHasCliOptions(moduleInfo):
626
680
  return False
627
681
 
628
682
 
683
+ def _parseOptionsFile(optionsFile: str, parser: OptionParser) -> dict:
684
+ """
685
+ Parse the JSON options within the provided filepath.
686
+ :param optionsFile: The path to the JSON options file.
687
+ :param parser: The parser to log an error to if needed.
688
+ :return: The parsed options as a dictionary.
689
+ """
690
+ try:
691
+ with open(optionsFile) as f:
692
+ jsonOptions = json.load(f)
693
+ except OSError:
694
+ parser.error(_("Options file path does not exist: {}").format(optionsFile))
695
+ return {}
696
+ except Exception as e:
697
+ parser.error(_("Unable to parse options JSON file: {}").format(e))
698
+ return {}
699
+ if not isinstance(jsonOptions, dict):
700
+ parser.error(_("Options JSON file must contain a JSON object at its root."))
701
+ return {}
702
+ return jsonOptions
703
+
704
+
629
705
  class CntlrCmdLine(Cntlr.Cntlr):
630
706
  """
631
707
  .. class:: CntlrCmdLin()
arelle/CntlrWinMain.py CHANGED
@@ -17,7 +17,7 @@ from typing import Any
17
17
 
18
18
  import regex as re
19
19
 
20
- from arelle import ValidateDuplicateFacts
20
+ from arelle import UrlUtil, ValidateDuplicateFacts
21
21
  from arelle.ValidateFileSource import ValidateFileSource
22
22
  from arelle.logging.formatters.LogFormatter import logRefsFileLines
23
23
  from arelle.utils.EntryPointDetection import parseEntrypointFileInput
@@ -868,6 +868,9 @@ class CntlrWinMain (Cntlr.Cntlr):
868
868
  entrypointFiles = entrypointParseResult.entrypointFiles
869
869
  # check for archive files
870
870
  if filesource.isArchive:
871
+ filenameWithoutFakeIxdsPrefix = UrlUtil.stripIxdsSurrogatePrefix(filename)
872
+ if all(e.get("file") == filenameWithoutFakeIxdsPrefix for e in entrypointFiles):
873
+ entrypointFiles = []
871
874
  if (
872
875
  len(entrypointFiles) == 0 and
873
876
  not filesource.selection and
arelle/DialogFind.py CHANGED
@@ -186,7 +186,7 @@ class DialogFind(Toplevel):
186
186
  else:
187
187
  if not self.modelManager.modelXbrl or not docType in (
188
188
  ModelDocument.Type.SCHEMA, ModelDocument.Type.LINKBASE, ModelDocument.Type.INSTANCE, ModelDocument.Type.INLINEXBRL,
189
- ModelDocument.Type.RSSFEED):
189
+ ModelDocument.Type.RSSFEED, ModelDocument.Type.INLINEXBRLDOCUMENTSET):
190
190
  messagebox.showerror(_("Find cannot be completed"),
191
191
  _("Find requires an opened DTS or RSS Feed"), parent=self.parent)
192
192
  return
arelle/ModelDtsObject.py CHANGED
@@ -2015,6 +2015,7 @@ class ModelRelationship(ModelObject):
2015
2015
  @property
2016
2016
  def equivalenceHash(self): # not exact, use equivalenceKey if hashes are the same
2017
2017
  return hash((self.qname,
2018
+ self.arcrole,
2018
2019
  self.linkQname,
2019
2020
  self.linkrole, # needed when linkrole=None merges multiple links
2020
2021
  self.fromModelObject.objectIndex if isinstance(self.fromModelObject, ModelObject) else -1,
@@ -2028,6 +2029,7 @@ class ModelRelationship(ModelObject):
2028
2029
  """(tuple) -- Key to determine relationship equivalence per 2.1 spec"""
2029
2030
  # cannot be cached because this is unique per relationship
2030
2031
  return (self.qname,
2032
+ self.arcrole,
2031
2033
  self.linkQname,
2032
2034
  self.linkrole, # needed when linkrole=None merges multiple links
2033
2035
  self.fromModelObject.objectIndex if isinstance(self.fromModelObject, ModelObject) else -1,
arelle/ModelObject.py CHANGED
@@ -8,7 +8,6 @@ from lxml import etree
8
8
  from arelle import Locale
9
9
  from arelle import ModelValue
10
10
  from arelle.XmlValidateConst import VALID_NO_CONTENT
11
- from arelle.model import CommentBase, ElementBase, PIBase
12
11
 
13
12
  if TYPE_CHECKING:
14
13
  from arelle.ModelDocument import ModelDocument
@@ -21,7 +20,7 @@ if TYPE_CHECKING:
21
20
  from arelle.ModelInstanceObject import ModelInlineFootnote
22
21
  from arelle.ModelInstanceObject import ModelInlineFact
23
22
  from arelle.ModelInstanceObject import ModelDimensionValue
24
- from arelle.ModelValue import qname, qnameEltPfxName, QName, TypeSValue, TypeXValue
23
+ from arelle.ModelValue import QName, TypeSValue, TypeXValue
25
24
 
26
25
  XmlUtil: Any = None
27
26
 
@@ -32,7 +31,7 @@ def init() -> None: # init globals
32
31
  if XmlUtil is None:
33
32
  from arelle import XmlUtil
34
33
 
35
- class ModelObject(ElementBase):
34
+ class ModelObject(etree.ElementBase):
36
35
  """ModelObjects represent the XML elements within a document, and are implemented as custom
37
36
  lxml proxy objects. Each modelDocument has a parser with the parser objects in ModelObjectFactory.py,
38
37
  to determine the type of model object to correspond to a proxied lxml XML element.
@@ -117,6 +116,7 @@ class ModelObject(ElementBase):
117
116
  xValueError: Exception | None
118
117
  xValid: int
119
118
  xlinkLabel: str
119
+ tag: str
120
120
  targetModelXbrl: ModelXbrl
121
121
 
122
122
  def _init(self) -> None:
@@ -125,9 +125,9 @@ class ModelObject(ElementBase):
125
125
  if parent is not None and hasattr(parent, "modelDocument"):
126
126
  self.init(parent.modelDocument)
127
127
 
128
- def clear(self) -> None:
128
+ def clear(self, keep_tail: bool = False) -> None:
129
129
  self.__dict__.clear() # delete local attributes
130
- super(ModelObject, self).clear() # delete children
130
+ super().clear(keep_tail) # delete children
131
131
 
132
132
  def init(self, modelDocument: ModelDocument) -> None:
133
133
  self.modelDocument = modelDocument
@@ -160,9 +160,10 @@ class ModelObject(ElementBase):
160
160
  return emptySet
161
161
 
162
162
  def setNamespaceLocalName(self) -> None:
163
- ns, sep, self._localName = self.tag.rpartition("}")
163
+ tag = self.tag
164
+ ns, sep, self._localName = tag.rpartition("}")
164
165
  if sep:
165
- self._namespaceURI = ns[1:]
166
+ self._namespaceURI: str | None = ns[1:]
166
167
  else:
167
168
  self._namespaceURI = None
168
169
  if self.prefix:
@@ -257,7 +258,7 @@ class ModelObject(ElementBase):
257
258
  return self._parentQname
258
259
  except AttributeError:
259
260
  parentObj = self.getparent()
260
- self._parentQname = parentObj.elementQname if parentObj is not None else None # type: ignore[attr-defined]
261
+ self._parentQname = parentObj.elementQname if parentObj is not None else None
261
262
  return self._parentQname
262
263
 
263
264
 
@@ -267,18 +268,18 @@ class ModelObject(ElementBase):
267
268
 
268
269
  @property
269
270
  def stringValue(self) -> str: # "string value" of node, text of all Element descendants
270
- return ''.join(self._textNodes(recurse=True)) # return text of Element descendants
271
+ return ''.join(self.textNodes(recurse=True)) # return text of Element descendants
271
272
 
272
273
  @property
273
274
  def textValue(self) -> str: # xml axis text() differs from string value, no descendant element text
274
- return ''.join(self._textNodes()) # no text nodes returns ''
275
+ return ''.join(self.textNodes()) # no text nodes returns ''
275
276
 
276
- def _textNodes(self, recurse:bool = False) -> Generator[str | Any, None, None]:
277
+ def textNodes(self, recurse:bool = False) -> Generator[str | Any, None, None]:
277
278
  if self.text and getattr(self,"xValid", 0) != VALID_NO_CONTENT: # skip tuple whitespaces
278
279
  yield self.text
279
280
  for c in self.iterchildren():
280
281
  if recurse and isinstance(c, ModelObject):
281
- for nestedText in c._textNodes(recurse):
282
+ for nestedText in c.textNodes(recurse):
282
283
  yield nestedText
283
284
  if c.tail and getattr(self,"xValid", 0) != VALID_NO_CONTENT: # skip tuple whitespaces
284
285
  yield c.tail # get tail of nested element, comment or processor nodes
@@ -305,10 +306,7 @@ class ModelObject(ElementBase):
305
306
 
306
307
  @property
307
308
  def elementAttributesStr(self) -> str:
308
- # Note 2022-09-09:
309
- # Mypy raises the following error. Not sure why this is the case, this returns a str not binary data?
310
- # On Python 3 formatting "b'abc'" with "{}" produces "b'abc'", not "abc"; use "{!r}" if this is desired behavior
311
- return ', '.join(["{0}='{1}'".format(name, value) for name, value in self.items()]) # type: ignore[str-bytes-safe]
309
+ return ', '.join(["{0}='{1}'".format(name, value) for name, value in self.items()])
312
310
 
313
311
  def resolveUri(
314
312
  self,
@@ -398,7 +396,7 @@ class ModelObject(ElementBase):
398
396
  def __repr__(self) -> str:
399
397
  return ("{0}[{1}, {2} line {3})".format(type(self).__name__, self.objectIndex, self.modelDocument.basename, self.sourceline))
400
398
 
401
- class ModelComment(CommentBase): # type: ignore[misc]
399
+ class ModelComment(etree.CommentBase):
402
400
  """ModelConcept is a custom proxy objects for etree.
403
401
  """
404
402
  def _init(self) -> None:
@@ -410,7 +408,7 @@ class ModelComment(CommentBase): # type: ignore[misc]
410
408
  def init(self, modelDocument: ModelDocument) -> None:
411
409
  self.modelDocument = modelDocument
412
410
 
413
- class ModelProcessingInstruction(PIBase): # type: ignore[misc]
411
+ class ModelProcessingInstruction(etree.PIBase):
414
412
  """ModelProcessingInstruction is a custom proxy object for etree.
415
413
  """
416
414
  def _init(self) -> None:
@@ -4,7 +4,7 @@ See COPYRIGHT.md for copyright information.
4
4
  from __future__ import annotations
5
5
 
6
6
  from arelle.ModelObject import ModelObject, init as moduleObject_init
7
- from typing import Any, Optional, TYPE_CHECKING, Type
7
+ from typing import Any, cast, Optional, TYPE_CHECKING, Type
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from arelle.ModelValue import QName
@@ -45,18 +45,18 @@ def parser(
45
45
  modelXbrl: ModelXbrl,
46
46
  baseUrl: str | None,
47
47
  target: None = None
48
- ) -> tuple[etree.XMLParser, KnownNamespacesModelObjectClassLookup, DiscoveringClassLookup]:
48
+ ) -> tuple[etree.XMLParser[etree._Element], KnownNamespacesModelObjectClassLookup, DiscoveringClassLookup]:
49
49
  moduleObject_init() # init ModelObject globals
50
- _parser = etree.XMLParser(recover=True, huge_tree=True, target=target,
51
- resolve_entities=False)
50
+ _parser = etree.XMLParser(recover=True, huge_tree=True, target=target, # type: ignore[call-overload]
51
+ resolve_entities=False)
52
52
  return setParserElementClassLookup(_parser, modelXbrl, baseUrl)
53
53
 
54
54
 
55
55
  def setParserElementClassLookup(
56
- _parser: etree.XMLParser,
56
+ _parser: etree.XMLParser[etree._Element],
57
57
  modelXbrl: ModelXbrl,
58
58
  baseUrl: str | None = None,
59
- ) -> tuple[etree.XMLParser, KnownNamespacesModelObjectClassLookup, DiscoveringClassLookup]:
59
+ ) -> tuple[etree.XMLParser[etree._Element], KnownNamespacesModelObjectClassLookup, DiscoveringClassLookup]:
60
60
  classLookup = DiscoveringClassLookup(modelXbrl, baseUrl)
61
61
  nsNameLookup = KnownNamespacesModelObjectClassLookup(modelXbrl, fallback=classLookup)
62
62
  _parser.set_element_class_lookup(nsNameLookup)
@@ -91,9 +91,10 @@ class KnownNamespacesModelObjectClassLookup(etree.CustomElementClassLookup):
91
91
  self.modelXbrl = modelXbrl
92
92
  self.type: int | None = None
93
93
 
94
- def lookup(self, node_type: str, document: etree._Document, ns: str | None, ln: str) -> Type[etree.ElementBase] | None:
94
+ def lookup(self, node_type: str, document: object, ns: str | None, ln: str | None) -> type[etree._Element] | None:
95
95
  # node_type is "element", "comment", "PI", or "entity"
96
96
  if node_type == "element":
97
+ assert ln is not None, "element nodes must have a local name"
97
98
  if ns == XbrlConst.xsd:
98
99
  if self.type is None:
99
100
  self.type = SCHEMA
@@ -160,14 +161,14 @@ class KnownNamespacesModelObjectClassLookup(etree.CustomElementClassLookup):
160
161
 
161
162
  return ModelComment
162
163
  elif node_type == "PI":
163
- return etree.PIBase # type: ignore[no-any-return]
164
+ return etree.PIBase
164
165
  elif node_type == "entity":
165
- return etree.EntityBase # type: ignore[no-any-return]
166
+ return etree.EntityBase
166
167
  # returning None delegates to fallback lookup classes
167
168
  return None
168
169
 
169
170
 
170
- class DiscoveringClassLookup(etree.PythonElementClassLookup): # type: ignore[misc]
171
+ class DiscoveringClassLookup(etree.PythonElementClassLookup):
171
172
  def __init__(self, modelXbrl: ModelXbrl, baseUrl: str | None, fallback: etree.ElementClassLookup | None = None) -> None:
172
173
  super(DiscoveringClassLookup, self).__init__(fallback)
173
174
  self.modelXbrl = modelXbrl
@@ -180,10 +181,11 @@ class DiscoveringClassLookup(etree.PythonElementClassLookup): # type: ignore[mi
180
181
  if self.streamingOrSkipDTS and ModelFact is None:
181
182
  from arelle.ModelInstanceObject import ModelFact
182
183
 
183
- def lookup(self, document: etree._Document, proxyElement: etree._Element) -> Type[ModelObject]:
184
+ def lookup(self, document: object, proxyElement: etree._Element) -> type[etree._Element] | None:
184
185
  # check if proxyElement's namespace is not known
185
186
  ns: str | None
186
- ns, sep, ln = proxyElement.tag.partition("}")
187
+ tag = cast(str, proxyElement.tag)
188
+ ns, sep, ln = tag.partition("}")
187
189
  if sep:
188
190
  ns = ns[1:]
189
191
  else:
@@ -208,9 +210,9 @@ class DiscoveringClassLookup(etree.PythonElementClassLookup): # type: ignore[mi
208
210
  # self.makeelementParentModelObject is set in streamingExtensions.py and ModelXbrl.createFact
209
211
  ancestor = proxyElement.getparent() or getattr(self.modelXbrl, "makeelementParentModelObject", None)
210
212
  while ancestor is not None:
211
- tag = ancestor.tag # not a modelObject yet, just parser prototype
212
- if tag.startswith("{http://www.xbrl.org/2003/instance}") or tag.startswith("{http://www.xbrl.org/2003/linkbase}"):
213
- if tag == "{http://www.xbrl.org/2003/instance}xbrl":
213
+ ancestorTag = cast(str, ancestor.tag) # not a modelObject yet, just parser prototype
214
+ if ancestorTag.startswith("{http://www.xbrl.org/2003/instance}") or ancestorTag.startswith("{http://www.xbrl.org/2003/linkbase}"):
215
+ if ancestorTag == "{http://www.xbrl.org/2003/instance}xbrl":
214
216
  # element not parented by context or footnoteLink
215
217
  return ModelFact # type: ignore[no-any-return]
216
218
  else:
arelle/ModelXbrl.py CHANGED
@@ -26,6 +26,7 @@ from arelle.UrlUtil import isHttpUrl
26
26
  from arelle.ValidateXbrlDimensions import isFactDimensionallyValid
27
27
  from arelle.XbrlConst import standardLabel
28
28
  from arelle.XbrlUtil import sEqual
29
+ from arelle.utils.validate.Validation import Validation
29
30
 
30
31
  if TYPE_CHECKING:
31
32
  from datetime import date, datetime
@@ -603,7 +604,7 @@ class ModelXbrl:
603
604
  all([cDim.isEqualTo(dims[cDimQn]) for cDimQn, cDim in c.qnameDims.items()]))) and
604
605
  # OCCs match for either dimensional or non-dimensional modle
605
606
  all(
606
- all([sEqual(self, cOCCs[i], mOCCs[i]) for i in range(len(mOCCs))]) # type: ignore[arg-type]
607
+ all([sEqual(self, cOCCs[i], mOCCs[i]) for i in range(len(mOCCs))])
607
608
  if len(cOCCs) == len(mOCCs) else False
608
609
  for cOCCs,mOCCs in ((c.nonDimValues(segAspect),segOCCs),
609
610
  (c.nonDimValues(scenAspect),scenOCCs)))
@@ -1043,6 +1044,11 @@ class ModelXbrl:
1043
1044
  """@messageCatalog=[]"""
1044
1045
  self.log('WARNING', codes, msg, **args)
1045
1046
 
1047
+ def validation(self, val: Validation) -> None:
1048
+ """Same as log, but parameters passed in from Validation object
1049
+ """
1050
+ self.log(level=val.level.name, codes=val.codes, msg=val.msg, **val.args)
1051
+
1046
1052
  def log(self, level: str, codes: Any, msg: str, **args: Any) -> None:
1047
1053
  """Same as error(), but level passed in as argument
1048
1054
  """
arelle/PluginManager.py CHANGED
@@ -856,11 +856,7 @@ class EntryPointRef:
856
856
  Retrieve all installed plugin entry points.
857
857
  :return: List of all discovered entry points.
858
858
  """
859
- entryPoints: list[EntryPoint]
860
- if sys.version_info < (3, 10):
861
- entryPoints = [e for e in entry_points().get('arelle.plugin', [])]
862
- else:
863
- entryPoints = list(entry_points(group='arelle.plugin'))
859
+ entryPoints = list(entry_points(group='arelle.plugin'))
864
860
  entryPointRefs = []
865
861
  for entryPoint in entryPoints:
866
862
  entryPointRef = EntryPointRef.fromEntryPoint(entryPoint)
arelle/RuntimeOptions.py CHANGED
@@ -106,6 +106,7 @@ class RuntimeOptions:
106
106
  logXmlMaxAttributeLength: Optional[int] = None
107
107
  monitorParentProcess: Optional[bool] = None
108
108
  noCertificateCheck: Optional[bool] = None
109
+ optionsFile: Optional[str] = None
109
110
  outputAttribution: Optional[str] = None
110
111
  packageManifestName: Optional[str] = None
111
112
  packages: Optional[list[str]] = None
arelle/UrlUtil.py CHANGED
@@ -12,6 +12,17 @@ from email.utils import parsedate
12
12
  from datetime import datetime
13
13
  from typing import overload
14
14
 
15
+ IXDS_DOC_SEPARATOR = "#?#" # the files of the document set follow the "surrogate" with these separators
16
+ IXDS_SURROGATE = f"_IXDS{IXDS_DOC_SEPARATOR}" # surrogate (fake) file name for inline XBRL doc set (IXDS)
17
+
18
+ def stripIxdsSurrogatePrefix(path: str) -> str:
19
+ """If path contains IXDS surrogate prefix, strip it and return the rest."""
20
+ if path:
21
+ _, found, after = path.partition(IXDS_SURROGATE)
22
+ if found:
23
+ return after
24
+ return path
25
+
15
26
  def authority(url: str, includeScheme: bool=True) -> str:
16
27
  if url:
17
28
  authSep = url.find(':')
arelle/Validate.py CHANGED
@@ -136,7 +136,7 @@ class Validate:
136
136
  instance=self.modelXbrl.modelDocument.basename if hasattr(self.modelXbrl, "modelDocument") and hasattr(self.modelXbrl.modelDocument, "basename") else "(closed)",
137
137
  error=err,
138
138
  # traceback=traceback.format_tb(sys.exc_info()[2]),
139
- exc_info=(type(err) is not AssertionError))
139
+ exc_info=True)
140
140
  self.close()
141
141
 
142
142
  def validateRssFeed(self):
@@ -489,7 +489,7 @@ class Validate:
489
489
  except Exception as err:
490
490
  model.error("exception:" + type(err).__name__,
491
491
  _("Testcase variation validation exception: %(error)s, instance: %(instance)s"),
492
- modelXbrl=model, instance=model.modelDocument.basename, error=err, exc_info=(type(err) is not AssertionError))
492
+ modelXbrl=model, instance=model.modelDocument.basename, error=err, exc_info=True)
493
493
  model.hasFormulae = _hasFormulae
494
494
  for pluginXbrlMethod in pluginClassMethods("Validate.Complete"):
495
495
  pluginXbrlMethod(self.modelXbrl.modelManager.cntlr, filesource)
@@ -575,7 +575,7 @@ class Validate:
575
575
  except Exception as err:
576
576
  modelXbrl.error("exception:" + type(err).__name__,
577
577
  _("Testcase formula variation validation exception: %(error)s, instance: %(instance)s"),
578
- modelXbrl=modelXbrl, instance=modelXbrl.modelDocument.basename, error=err, exc_info=(type(err) is not AssertionError))
578
+ modelXbrl=modelXbrl, instance=modelXbrl.modelDocument.basename, error=err, exc_info=True)
579
579
  if modelTestcaseVariation.resultIsInfoset and self.modelXbrl.modelManager.validateInfoset:
580
580
  for pluginXbrlMethod in pluginClassMethods("Validate.Infoset"):
581
581
  pluginXbrlMethod(modelXbrl, modelTestcaseVariation.resultInfosetUri)
arelle/ValidateXbrl.py CHANGED
@@ -428,7 +428,8 @@ class ValidateXbrl:
428
428
 
429
429
  if self.validateCalcs:
430
430
  modelXbrl.modelManager.showStatus(_("Validating instance calculations"))
431
- ValidateXbrlCalcs.validate(modelXbrl, self.validateCalcs)
431
+ for val in ValidateXbrlCalcs.validate(modelXbrl, self.validateCalcs):
432
+ modelXbrl.validation(val)
432
433
  modelXbrl.profileStat(_("validateCalculations"))
433
434
 
434
435
  if self.validateUTR: