arelle-release 2.37.46__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 (204) hide show
  1. arelle/BetaFeatures.py +0 -21
  2. arelle/Cntlr.py +15 -8
  3. arelle/CntlrCmdLine.py +121 -56
  4. arelle/CntlrWinMain.py +143 -70
  5. arelle/DialogFind.py +1 -1
  6. arelle/DialogPluginManager.py +6 -4
  7. arelle/DisclosureSystem.py +7 -0
  8. arelle/ErrorManager.py +21 -6
  9. arelle/FileSource.py +11 -4
  10. arelle/FunctionIxt.py +16 -11
  11. arelle/HtmlUtil.py +5 -4
  12. arelle/LeiUtil.py +63 -43
  13. arelle/ModelDocument.py +20 -15
  14. arelle/ModelDtsObject.py +8 -0
  15. arelle/ModelInstanceObject.py +1 -1
  16. arelle/ModelObject.py +16 -18
  17. arelle/ModelObjectFactory.py +35 -17
  18. arelle/ModelXbrl.py +28 -11
  19. arelle/PluginManager.py +130 -105
  20. arelle/RuntimeOptions.py +1 -0
  21. arelle/UrlUtil.py +14 -0
  22. arelle/Validate.py +17 -12
  23. arelle/ValidateDuplicateFacts.py +3 -1
  24. arelle/ValidateFileSource.py +38 -0
  25. arelle/ValidateFilingText.py +3 -3
  26. arelle/ValidateXbrl.py +5 -2
  27. arelle/ValidateXbrlCalcs.py +210 -186
  28. arelle/ValidateXbrlDTS.py +1 -1
  29. arelle/ViewFile.py +1 -0
  30. arelle/ViewFileFactTable.py +2 -2
  31. arelle/ViewWinDTS.py +4 -1
  32. arelle/WebCache.py +28 -24
  33. arelle/XbrlConst.py +22 -0
  34. arelle/XmlUtil.py +16 -21
  35. arelle/XmlValidate.py +6 -9
  36. arelle/_version.py +16 -3
  37. arelle/api/Session.py +11 -2
  38. arelle/config/disclosuresystems.xsd +2 -0
  39. arelle/config/rosettaEntitlements.plist +8 -0
  40. arelle/conformance/CSVTestcaseLoader.py +1 -1
  41. arelle/formula/XPathContext.py +3 -3
  42. arelle/logging/formatters/LogFormatter.py +3 -1
  43. arelle/packages/report/ReportPackage.py +26 -13
  44. arelle/packages/report/ReportPackageConst.py +0 -1
  45. arelle/plugin/inlineXbrlDocumentSet.py +19 -5
  46. arelle/plugin/validate/DBA/DisclosureSystems.py +19 -1
  47. arelle/plugin/validate/DBA/PluginValidationDataExtension.py +2 -4
  48. arelle/plugin/validate/DBA/ValidationPluginExtension.py +2 -1
  49. arelle/plugin/validate/DBA/resources/config.xml +5 -0
  50. arelle/plugin/validate/DBA/rules/__init__.py +2 -2
  51. arelle/plugin/validate/DBA/rules/fr.py +19 -2
  52. arelle/plugin/validate/DBA/rules/tc.py +2 -0
  53. arelle/plugin/validate/DBA/rules/th.py +6 -0
  54. arelle/plugin/validate/DBA/rules/tm.py +18 -5
  55. arelle/plugin/validate/DBA/rules/tr.py +11 -5
  56. arelle/plugin/validate/EDINET/Constants.py +193 -9
  57. arelle/plugin/validate/EDINET/ContextRequirement.py +58 -0
  58. arelle/plugin/validate/EDINET/ControllerPluginData.py +220 -1
  59. arelle/plugin/validate/EDINET/CoverItemRequirements.py +42 -0
  60. arelle/plugin/validate/EDINET/DeiRequirements.py +118 -0
  61. arelle/plugin/validate/EDINET/FilingFormat.py +275 -0
  62. arelle/plugin/validate/EDINET/FormType.py +134 -0
  63. arelle/plugin/validate/EDINET/ManifestInstance.py +72 -5
  64. arelle/plugin/validate/EDINET/NamespaceConfig.py +50 -0
  65. arelle/plugin/validate/EDINET/PluginValidationDataExtension.py +493 -132
  66. arelle/plugin/validate/EDINET/{InstanceType.py → ReportFolderType.py} +72 -15
  67. arelle/plugin/validate/EDINET/Statement.py +139 -0
  68. arelle/plugin/validate/EDINET/TableOfContentsBuilder.py +595 -0
  69. arelle/plugin/validate/EDINET/UploadContents.py +48 -0
  70. arelle/plugin/validate/EDINET/ValidationPluginExtension.py +20 -2
  71. arelle/plugin/validate/EDINET/__init__.py +31 -6
  72. arelle/plugin/validate/EDINET/resources/config.xml +8 -1
  73. arelle/plugin/validate/EDINET/resources/cover-item-requirements.json +793 -0
  74. arelle/plugin/validate/EDINET/resources/dei-requirements.csv +27 -0
  75. arelle/plugin/validate/EDINET/resources/edinet-taxonomies.xml +2 -0
  76. arelle/plugin/validate/EDINET/rules/contexts.py +375 -14
  77. arelle/plugin/validate/EDINET/rules/edinet.py +1934 -45
  78. arelle/plugin/validate/EDINET/rules/frta.py +122 -3
  79. arelle/plugin/validate/EDINET/rules/gfm.py +1907 -11
  80. arelle/plugin/validate/EDINET/rules/upload.py +989 -141
  81. arelle/plugin/validate/ESEF/Const.py +3 -1
  82. arelle/plugin/validate/ESEF/ESEF_2021/DTS.py +5 -0
  83. arelle/plugin/validate/ESEF/ESEF_2021/Image.py +2 -2
  84. arelle/plugin/validate/ESEF/ESEF_2021/ValidateXbrlFinally.py +23 -20
  85. arelle/plugin/validate/ESEF/ESEF_Current/DTS.py +47 -14
  86. arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py +100 -25
  87. arelle/plugin/validate/ESEF/__init__.py +20 -6
  88. arelle/plugin/validate/ESEF/resources/authority-validations.json +76 -9
  89. arelle/plugin/validate/ESEF/resources/config.xml +20 -0
  90. arelle/plugin/validate/NL/DisclosureSystems.py +22 -0
  91. arelle/plugin/validate/NL/PluginValidationDataExtension.py +27 -9
  92. arelle/plugin/validate/NL/ValidationPluginExtension.py +51 -7
  93. arelle/plugin/validate/NL/resources/config.xml +18 -0
  94. arelle/plugin/validate/NL/rules/br_kvk.py +17 -61
  95. arelle/plugin/validate/NL/rules/fg_nl.py +7 -38
  96. arelle/plugin/validate/NL/rules/fr_kvk.py +7 -42
  97. arelle/plugin/validate/NL/rules/fr_nl.py +31 -147
  98. arelle/plugin/validate/NL/rules/nl_kvk.py +142 -28
  99. arelle/plugin/validate/ROS/PluginValidationDataExtension.py +2 -0
  100. arelle/plugin/validate/ROS/ValidationPluginExtension.py +4 -1
  101. arelle/plugin/validate/ROS/rules/ros.py +41 -9
  102. arelle/plugin/validate/UK/ValidateUK.py +130 -66
  103. arelle/plugin/validate/UK/__init__.py +89 -103
  104. arelle/utils/EntryPointDetection.py +79 -13
  105. arelle/utils/PluginHooks.py +125 -0
  106. arelle/utils/validate/ESEFImage.py +6 -6
  107. arelle/utils/validate/Validation.py +18 -0
  108. arelle/utils/validate/ValidationPlugin.py +76 -11
  109. arelle/utils/validate/ValidationUtil.py +35 -3
  110. {arelle_release-2.37.46.dist-info → arelle_release-2.38.0.dist-info}/METADATA +30 -20
  111. {arelle_release-2.37.46.dist-info → arelle_release-2.38.0.dist-info}/RECORD +115 -191
  112. {arelle_release-2.37.46.dist-info → arelle_release-2.38.0.dist-info}/licenses/LICENSE.md +0 -3
  113. arelle/archive/CustomLogger.py +0 -43
  114. arelle/archive/LoadEFMvalidate.py +0 -32
  115. arelle/archive/LoadSavePreLbCsv.py +0 -26
  116. arelle/archive/LoadValidate.cs +0 -31
  117. arelle/archive/LoadValidate.py +0 -36
  118. arelle/archive/LoadValidateCmdLine.java +0 -69
  119. arelle/archive/LoadValidatePostedZip.java +0 -57
  120. arelle/archive/LoadValidateWebService.java +0 -34
  121. arelle/archive/SaveTableToExelle.py +0 -140
  122. arelle/archive/TR3toTR4.py +0 -88
  123. arelle/archive/plugin/ESEF_2022/__init__.py +0 -47
  124. arelle/archive/plugin/bigInstance.py +0 -394
  125. arelle/archive/plugin/cmdWebServerExtension.py +0 -43
  126. arelle/archive/plugin/crashTest.py +0 -38
  127. arelle/archive/plugin/functionsXmlCreation.py +0 -106
  128. arelle/archive/plugin/hello_i18n.pot +0 -26
  129. arelle/archive/plugin/hello_i18n.py +0 -32
  130. arelle/archive/plugin/importTestChild1.py +0 -21
  131. arelle/archive/plugin/importTestChild2.py +0 -22
  132. arelle/archive/plugin/importTestGrandchild1.py +0 -21
  133. arelle/archive/plugin/importTestGrandchild2.py +0 -21
  134. arelle/archive/plugin/importTestImported1.py +0 -23
  135. arelle/archive/plugin/importTestImported11.py +0 -22
  136. arelle/archive/plugin/importTestParent.py +0 -48
  137. arelle/archive/plugin/instanceInfo.py +0 -306
  138. arelle/archive/plugin/loadFromOIM-2018.py +0 -1282
  139. arelle/archive/plugin/locale/fr/LC_MESSAGES/hello_i18n.po +0 -25
  140. arelle/archive/plugin/objectmaker.py +0 -285
  141. arelle/archive/plugin/packagedImportTest/__init__.py +0 -47
  142. arelle/archive/plugin/packagedImportTest/importTestChild1.py +0 -21
  143. arelle/archive/plugin/packagedImportTest/importTestChild2.py +0 -22
  144. arelle/archive/plugin/packagedImportTest/importTestGrandchild1.py +0 -21
  145. arelle/archive/plugin/packagedImportTest/importTestGrandchild2.py +0 -21
  146. arelle/archive/plugin/packagedImportTest/importTestImported1.py +0 -24
  147. arelle/archive/plugin/packagedImportTest/importTestImported11.py +0 -21
  148. arelle/archive/plugin/packagedImportTest/subdir/importTestImported111.py +0 -21
  149. arelle/archive/plugin/packagedImportTest/subdir/subsubdir/importTestImported1111.py +0 -21
  150. arelle/archive/plugin/sakaCalendar.py +0 -215
  151. arelle/archive/plugin/saveInstanceInfoset.py +0 -121
  152. arelle/archive/plugin/sphinx/FormulaGenerator.py +0 -823
  153. arelle/archive/plugin/sphinx/SphinxContext.py +0 -404
  154. arelle/archive/plugin/sphinx/SphinxEvaluator.py +0 -783
  155. arelle/archive/plugin/sphinx/SphinxMethods.py +0 -1287
  156. arelle/archive/plugin/sphinx/SphinxParser.py +0 -1093
  157. arelle/archive/plugin/sphinx/SphinxValidator.py +0 -163
  158. arelle/archive/plugin/sphinx/US-GAAP Ratios Example.xsr +0 -52
  159. arelle/archive/plugin/sphinx/__init__.py +0 -285
  160. arelle/archive/plugin/streamingExtensions.py +0 -335
  161. arelle/archive/plugin/updateTableLB.py +0 -242
  162. arelle/archive/plugin/validate/SBRnl/CustomLoader.py +0 -19
  163. arelle/archive/plugin/validate/SBRnl/DTS.py +0 -305
  164. arelle/archive/plugin/validate/SBRnl/Dimensions.py +0 -357
  165. arelle/archive/plugin/validate/SBRnl/Document.py +0 -799
  166. arelle/archive/plugin/validate/SBRnl/Filing.py +0 -467
  167. arelle/archive/plugin/validate/SBRnl/__init__.py +0 -75
  168. arelle/archive/plugin/validate/SBRnl/config.xml +0 -26
  169. arelle/archive/plugin/validate/SBRnl/sbr-nl-taxonomies.xml +0 -754
  170. arelle/archive/plugin/validate/USBestPractices.py +0 -570
  171. arelle/archive/plugin/validate/USCorpAction.py +0 -557
  172. arelle/archive/plugin/validate/USSecTagging.py +0 -337
  173. arelle/archive/plugin/validate/XDC/__init__.py +0 -77
  174. arelle/archive/plugin/validate/XDC/config.xml +0 -20
  175. arelle/archive/plugin/validate/XFsyntax/__init__.py +0 -64
  176. arelle/archive/plugin/validate/XFsyntax/xf.py +0 -2227
  177. arelle/archive/plugin/validate/calc2.py +0 -536
  178. arelle/archive/plugin/validateSchemaLxml.py +0 -156
  179. arelle/archive/plugin/validateTableInfoset.py +0 -52
  180. arelle/archive/us-gaap-dei-docType-extraction-frm.xml +0 -90
  181. arelle/archive/us-gaap-dei-ratio-cash-frm.xml +0 -150
  182. arelle/examples/plugin/formulaSuiteConverter.py +0 -212
  183. arelle/examples/plugin/functionsCustom.py +0 -59
  184. arelle/examples/plugin/hello_dolly.py +0 -64
  185. arelle/examples/plugin/multi.py +0 -58
  186. arelle/examples/plugin/rssSaveOim.py +0 -96
  187. arelle/examples/plugin/validate/XYZ/DisclosureSystems.py +0 -2
  188. arelle/examples/plugin/validate/XYZ/PluginValidationDataExtension.py +0 -10
  189. arelle/examples/plugin/validate/XYZ/ValidationPluginExtension.py +0 -49
  190. arelle/examples/plugin/validate/XYZ/__init__.py +0 -75
  191. arelle/examples/plugin/validate/XYZ/resources/config.xml +0 -16
  192. arelle/examples/plugin/validate/XYZ/rules/__init__.py +0 -0
  193. arelle/examples/plugin/validate/XYZ/rules/rules01.py +0 -110
  194. arelle/examples/plugin/validate/XYZ/rules/rules02.py +0 -59
  195. arelle/model/CommentBase.py +0 -9
  196. arelle/model/ElementBase.py +0 -11
  197. arelle/model/PIBase.py +0 -10
  198. arelle/model/__init__.py +0 -15
  199. arelle/scripts-macOS/startWebServer.command +0 -3
  200. arelle/scripts-unix/startWebServer.sh +0 -1
  201. arelle/scripts-windows/startWebServer.bat +0 -5
  202. {arelle_release-2.37.46.dist-info → arelle_release-2.38.0.dist-info}/WHEEL +0 -0
  203. {arelle_release-2.37.46.dist-info → arelle_release-2.38.0.dist-info}/entry_points.txt +0 -0
  204. {arelle_release-2.37.46.dist-info → arelle_release-2.38.0.dist-info}/top_level.txt +0 -0
@@ -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)
@@ -68,6 +68,22 @@ LINKBASE = 2
68
68
  VERSIONINGREPORT = 3
69
69
  RSSFEED = 4
70
70
 
71
+ LINK_LOCALNAME_TO_MODEL_CLASS = {
72
+ 'loc': ModelLocator,
73
+ 'label': ModelResource,
74
+ 'reference': ModelResource,
75
+ 'roleType': ModelRoleType,
76
+ 'arcroleType': ModelRoleType,
77
+ } | {
78
+ q.localName: ModelObject
79
+ for q in [
80
+ XbrlConst.qnLinkCalculationArc,
81
+ XbrlConst.qnLinkDefinitionArc,
82
+ XbrlConst.qnLinkPresentationArc,
83
+ XbrlConst.qnLinkReferenceArc,
84
+ ]
85
+ }
86
+
71
87
 
72
88
  class KnownNamespacesModelObjectClassLookup(etree.CustomElementClassLookup):
73
89
  def __init__(self, modelXbrl: ModelXbrl, fallback: etree.ElementClassLookup | None = None) -> None:
@@ -75,9 +91,10 @@ class KnownNamespacesModelObjectClassLookup(etree.CustomElementClassLookup):
75
91
  self.modelXbrl = modelXbrl
76
92
  self.type: int | None = None
77
93
 
78
- 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:
79
95
  # node_type is "element", "comment", "PI", or "entity"
80
96
  if node_type == "element":
97
+ assert ln is not None, "element nodes must have a local name"
81
98
  if ns == XbrlConst.xsd:
82
99
  if self.type is None:
83
100
  self.type = SCHEMA
@@ -106,8 +123,8 @@ class KnownNamespacesModelObjectClassLookup(etree.CustomElementClassLookup):
106
123
  elif ns == XbrlConst.link:
107
124
  if self.type is None:
108
125
  self.type = LINKBASE
109
- if ln == "roleType" or ln == "arcroleType":
110
- return ModelRoleType
126
+ if modelObjectClass := LINK_LOCALNAME_TO_MODEL_CLASS.get(ln):
127
+ return modelObjectClass
111
128
  elif ns == "http://edgar/2009/conformance":
112
129
  # don't force loading of test schema
113
130
  if ln == "variation":
@@ -144,14 +161,14 @@ class KnownNamespacesModelObjectClassLookup(etree.CustomElementClassLookup):
144
161
 
145
162
  return ModelComment
146
163
  elif node_type == "PI":
147
- return etree.PIBase # type: ignore[no-any-return]
164
+ return etree.PIBase
148
165
  elif node_type == "entity":
149
- return etree.EntityBase # type: ignore[no-any-return]
166
+ return etree.EntityBase
150
167
  # returning None delegates to fallback lookup classes
151
168
  return None
152
169
 
153
170
 
154
- class DiscoveringClassLookup(etree.PythonElementClassLookup): # type: ignore[misc]
171
+ class DiscoveringClassLookup(etree.PythonElementClassLookup):
155
172
  def __init__(self, modelXbrl: ModelXbrl, baseUrl: str | None, fallback: etree.ElementClassLookup | None = None) -> None:
156
173
  super(DiscoveringClassLookup, self).__init__(fallback)
157
174
  self.modelXbrl = modelXbrl
@@ -164,10 +181,11 @@ class DiscoveringClassLookup(etree.PythonElementClassLookup): # type: ignore[mi
164
181
  if self.streamingOrSkipDTS and ModelFact is None:
165
182
  from arelle.ModelInstanceObject import ModelFact
166
183
 
167
- def lookup(self, document: etree._Document, proxyElement: etree._Element) -> Type[ModelObject]:
184
+ def lookup(self, document: object, proxyElement: etree._Element) -> type[etree._Element] | None:
168
185
  # check if proxyElement's namespace is not known
169
186
  ns: str | None
170
- ns, sep, ln = proxyElement.tag.partition("}")
187
+ tag = cast(str, proxyElement.tag)
188
+ ns, sep, ln = tag.partition("}")
171
189
  if sep:
172
190
  ns = ns[1:]
173
191
  else:
@@ -192,9 +210,9 @@ class DiscoveringClassLookup(etree.PythonElementClassLookup): # type: ignore[mi
192
210
  # self.makeelementParentModelObject is set in streamingExtensions.py and ModelXbrl.createFact
193
211
  ancestor = proxyElement.getparent() or getattr(self.modelXbrl, "makeelementParentModelObject", None)
194
212
  while ancestor is not None:
195
- tag = ancestor.tag # not a modelObject yet, just parser prototype
196
- if tag.startswith("{http://www.xbrl.org/2003/instance}") or tag.startswith("{http://www.xbrl.org/2003/linkbase}"):
197
- 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":
198
216
  # element not parented by context or footnoteLink
199
217
  return ModelFact # type: ignore[no-any-return]
200
218
  else:
arelle/ModelXbrl.py CHANGED
@@ -12,7 +12,7 @@ from collections import defaultdict
12
12
  from typing import TYPE_CHECKING, Any, TypeVar, Union, cast, Optional
13
13
 
14
14
  import regex as re
15
- from collections.abc import Iterable
15
+ from collections.abc import Iterable, Iterator
16
16
 
17
17
  import arelle
18
18
  from arelle import FileSource, ModelRelationshipSet, XmlUtil, ModelValue, XbrlConst, XmlValidate
@@ -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
@@ -335,8 +336,12 @@ class ModelXbrl:
335
336
  self.facts: list[ModelFact] = []
336
337
  self.factsInInstance: set[ModelFact] = set()
337
338
  self.undefinedFacts: list[ModelFact] = [] # elements presumed to be facts but not defined
338
- self.contexts: dict[str, ModelDocumentClass.xmlRootElement] = {}
339
+ self.contexts: dict[str, ModelContext] = {}
340
+ self.ixdsUnmappedContexts: dict[str, ModelContext] = {}
341
+ self._contextsInUseMarked = False
339
342
  self.units: dict[str, ModelUnit] = {}
343
+ self.ixdsUnmappedUnits: dict[str, ModelUnit] = {}
344
+ self._unitsInUseMarked = False
340
345
  self.modelObjects: list[ModelObject] = []
341
346
  self.qnameParameters: dict[QName, Any] = {}
342
347
  self.modelVariableSets: set[ModelVariableSet] = set()
@@ -599,12 +604,12 @@ class ModelXbrl:
599
604
  all([cDim.isEqualTo(dims[cDimQn]) for cDimQn, cDim in c.qnameDims.items()]))) and
600
605
  # OCCs match for either dimensional or non-dimensional modle
601
606
  all(
602
- 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))])
603
608
  if len(cOCCs) == len(mOCCs) else False
604
609
  for cOCCs,mOCCs in ((c.nonDimValues(segAspect),segOCCs),
605
610
  (c.nonDimValues(scenAspect),scenOCCs)))
606
611
  ):
607
- return cast('ModelContext', c)
612
+ return c
608
613
  return None
609
614
 
610
615
  def createContext(
@@ -868,17 +873,24 @@ class ModelXbrl:
868
873
  return fbdq[memQname]
869
874
 
870
875
  @property
871
- def contextsInUse(self) -> Any:
872
- try:
873
- if self._contextsInUseMarked:
874
- return (cntx for cntx in self.contexts.values() if getattr(cntx, "_inUse", False))
875
- except AttributeError:
876
+ def contextsInUse(self) -> Iterator[ModelContext]:
877
+ if not self._contextsInUseMarked:
876
878
  for fact in self.factsInInstance:
877
879
  cntx = fact.context
878
880
  if cntx is not None:
879
881
  cntx._inUse = True
880
- self._contextsInUseMarked: bool = True
881
- return self.contextsInUse
882
+ self._contextsInUseMarked = True
883
+ return (cntx for cntx in self.contexts.values() if getattr(cntx, "_inUse", False))
884
+
885
+ @property
886
+ def unitsInUse(self) -> Iterator[ModelUnit]:
887
+ if not self._unitsInUseMarked:
888
+ for fact in self.factsInInstance:
889
+ unit = fact.unit
890
+ if unit is not None:
891
+ unit._inUse = True
892
+ self._unitsInUseMarked = True
893
+ return (unit for unit in self.units.values() if getattr(unit, "_inUse", False))
882
894
 
883
895
  @property
884
896
  def dimensionsInUse(self) -> set[Any]:
@@ -1032,6 +1044,11 @@ class ModelXbrl:
1032
1044
  """@messageCatalog=[]"""
1033
1045
  self.log('WARNING', codes, msg, **args)
1034
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
+
1035
1052
  def log(self, level: str, codes: Any, msg: str, **args: Any) -> None:
1036
1053
  """Same as error(), but level passed in as argument
1037
1054
  """
arelle/PluginManager.py CHANGED
@@ -1,31 +1,41 @@
1
1
  '''
2
2
  See COPYRIGHT.md for copyright information.
3
-
4
- based on pull request 4
5
-
6
3
  '''
7
4
  from __future__ import annotations
8
- import os, sys, types, time, ast, importlib, io, json, gettext, traceback
9
- from dataclasses import dataclass
10
- from importlib.metadata import entry_points, EntryPoint
5
+
6
+ import ast
7
+ import gettext
8
+ from glob import glob
11
9
  import importlib.util
10
+ import json
12
11
  import logging
13
-
12
+ import os
13
+ import sys
14
+ import time
15
+ import traceback
16
+ import types
17
+ from collections import defaultdict
18
+ from collections.abc import Callable, Iterator
19
+ from dataclasses import dataclass
20
+ from importlib.metadata import EntryPoint, entry_points
21
+ from numbers import Number
22
+ from pathlib import Path
14
23
  from types import ModuleType
15
24
  from typing import TYPE_CHECKING, Any, cast
16
- from arelle.Locale import getLanguageCodes
25
+
17
26
  import arelle.FileSource
27
+ from arelle.Locale import getLanguageCodes
18
28
  from arelle.PythonUtil import isLegacyAbs
29
+ from arelle.typing import TypeGetText
19
30
  from arelle.UrlUtil import isAbsolute
20
- from pathlib import Path
21
- from collections import OrderedDict, defaultdict
22
- from collections.abc import Callable, Iterator
23
-
24
31
 
25
32
  if TYPE_CHECKING:
26
33
  # Prevent potential circular import error
27
34
  from .Cntlr import Cntlr
28
35
 
36
+
37
+ _: TypeGetText
38
+
29
39
  PLUGIN_TRACE_FILE = None
30
40
  # PLUGIN_TRACE_FILE = "c:/temp/pluginerr.txt"
31
41
  PLUGIN_TRACE_LEVEL = logging.WARNING
@@ -58,7 +68,7 @@ def init(cntlr: Cntlr, loadPluginConfig: bool = True) -> None:
58
68
  if loadPluginConfig:
59
69
  try:
60
70
  pluginJsonFile = cntlr.userAppDir + os.sep + "plugins.json"
61
- with io.open(pluginJsonFile, 'rt', encoding='utf-8') as f:
71
+ with open(pluginJsonFile, encoding='utf-8') as f:
62
72
  pluginConfig = json.load(f)
63
73
  freshenModuleInfos()
64
74
  except Exception:
@@ -79,29 +89,45 @@ def reset() -> None: # force reloading modules and plugin infos
79
89
  pluginMethodsForClasses.clear() # dict by class of list of ordered callable function objects
80
90
 
81
91
  def orderedPluginConfig():
82
- return OrderedDict(
83
- (('modules',OrderedDict((moduleName,
84
- OrderedDict(sorted(moduleInfo.items(),
85
- key=lambda k: {'name': '01',
86
- 'status': '02',
87
- 'version': '03',
88
- 'fileDate': '04', 'version': '05',
89
- 'description': '05',
90
- 'moduleURL': '06',
91
- 'localeURL': '07',
92
- 'localeDomain': '08',
93
- 'license': '09',
94
- 'author': '10',
95
- 'copyright': '11',
96
- 'classMethods': '12'}.get(k[0],k[0]))))
97
- for moduleName, moduleInfo in sorted(pluginConfig['modules'].items()))),
98
- ('classes',OrderedDict(sorted(pluginConfig['classes'].items())))))
92
+ fieldOrder = [
93
+ 'name',
94
+ 'status',
95
+ 'fileDate',
96
+ 'version',
97
+ 'description',
98
+ 'moduleURL',
99
+ 'localeURL',
100
+ 'localeDomain',
101
+ 'license',
102
+ 'author',
103
+ 'copyright',
104
+ 'classMethods',
105
+ ]
106
+ priorityIndex = {k: i for i, k in enumerate(fieldOrder)}
107
+
108
+ def sortModuleInfo(moduleInfo):
109
+ # Prioritize known fields by the index in fieldOrder; sort others alphabetically
110
+ orderedKeys = sorted(
111
+ moduleInfo.keys(),
112
+ key=lambda k: (priorityIndex.get(k, len(priorityIndex)), k)
113
+ )
114
+ return {k: moduleInfo[k] for k in orderedKeys}
115
+
116
+ orderedModules = {
117
+ moduleName: sortModuleInfo(pluginConfig['modules'][moduleName])
118
+ for moduleName in sorted(pluginConfig['modules'].keys())
119
+ }
120
+
121
+ return {
122
+ 'modules': orderedModules,
123
+ 'classes': dict(sorted(pluginConfig['classes'].items()))
124
+ }
99
125
 
100
126
  def save(cntlr: Cntlr) -> None:
101
127
  global pluginConfigChanged
102
128
  if pluginConfigChanged and cntlr.hasFileSystem and not cntlr.disablePersistentConfig:
103
129
  pluginJsonFile = cntlr.userAppDir + os.sep + "plugins.json"
104
- with io.open(pluginJsonFile, 'wt', encoding='utf-8') as f:
130
+ with open(pluginJsonFile, 'w', encoding='utf-8') as f:
105
131
  jsonStr = str(json.dumps(orderedPluginConfig(), ensure_ascii=False, indent=2)) # might not be unicode in 2.7
106
132
  f.write(jsonStr)
107
133
  pluginConfigChanged = False
@@ -155,7 +181,6 @@ moduleInfo = {
155
181
 
156
182
  '''
157
183
 
158
-
159
184
  def logPluginTrace(message: str, level: Number) -> None:
160
185
  """
161
186
  If plugin trace file logging is configured, logs `message` to it.
@@ -274,6 +299,14 @@ def getModuleFilename(moduleURL: str, reload: bool, normalize: bool, base: str |
274
299
  if moduleFilename:
275
300
  # `moduleFilename` normalized to an existing script
276
301
  return moduleFilename, None
302
+ if base and not _isAbsoluteModuleURL(moduleURL):
303
+ # Search for a matching plugin deeper in the plugin directory tree.
304
+ # Handles cases where a plugin exists in a nested structure, such as
305
+ # when a developer clones an entire repository into the plugin directory.
306
+ # Example: arelle/plugin/xule/plugin/xule/__init__.py
307
+ for path in glob("**/" + moduleURL.replace('\\', '/'), recursive=True):
308
+ if normalizedPath := normalizeModuleFilename(path):
309
+ return normalizedPath, None
277
310
  # `moduleFilename` did not map to a local filepath or did not normalize to a script
278
311
  # Try using `moduleURL` to search for pip-installed entry point
279
312
  entryPointRef = EntryPointRef.get(moduleURL)
@@ -416,7 +449,7 @@ def moduleModuleInfo(
416
449
 
417
450
  if moduleFilename:
418
451
  try:
419
- logPluginTrace("Scanning module for plug-in info: {}".format(moduleFilename), logging.INFO)
452
+ logPluginTrace(f"Scanning module for plug-in info: {moduleFilename}", logging.INFO)
420
453
  moduleInfo = parsePluginInfo(moduleURL, moduleFilename, entryPoint)
421
454
  if moduleInfo is None:
422
455
  return None
@@ -426,38 +459,50 @@ def moduleModuleInfo(
426
459
  del moduleInfo["importURLs"]
427
460
  moduleImports = moduleInfo["moduleImports"]
428
461
  del moduleInfo["moduleImports"]
429
- _moduleImportsSubtree = False
462
+ moduleImportsSubtree = False
430
463
  mergedImportURLs = []
431
464
 
432
- for _url in importURLs:
433
- if _url.startswith("module_import"):
465
+ for url in importURLs:
466
+ if url.startswith("module_import"):
434
467
  for moduleImport in moduleImports:
435
468
  mergedImportURLs.append(moduleImport + ".py")
436
- if _url == "module_import_subtree":
437
- _moduleImportsSubtree = True
438
- elif _url == "module_subtree":
469
+ if url == "module_import_subtree":
470
+ moduleImportsSubtree = True
471
+ elif url == "module_subtree":
439
472
  for _dir in os.listdir(moduleDir):
440
- _subtreeModule = os.path.join(moduleDir,_dir)
441
- if os.path.isdir(_subtreeModule) and _dir != "__pycache__":
442
- mergedImportURLs.append(_subtreeModule)
473
+ subtreeModule = os.path.join(moduleDir,_dir)
474
+ if os.path.isdir(subtreeModule) and _dir != "__pycache__":
475
+ mergedImportURLs.append(subtreeModule)
443
476
  else:
444
- mergedImportURLs.append(_url)
445
- if parentImportsSubtree and not _moduleImportsSubtree:
446
- _moduleImportsSubtree = True
477
+ mergedImportURLs.append(url)
478
+ if parentImportsSubtree and not moduleImportsSubtree:
479
+ moduleImportsSubtree = True
447
480
  for moduleImport in moduleImports:
448
481
  mergedImportURLs.append(moduleImport + ".py")
449
482
  imports = []
450
- for _url in mergedImportURLs:
451
- if isAbsolute(_url) or isLegacyAbs(_url):
452
- _importURL = _url # URL is absolute http or local file system
453
- else: # check if exists relative to this module's directory
454
- _importURL = os.path.join(os.path.dirname(moduleURL), os.path.normpath(_url))
455
- if not os.path.exists(_importURL): # not relative to this plugin, assume standard plugin base
456
- _importURL = _url # moduleModuleInfo adjusts relative URL to plugin base
457
- _importModuleInfo = moduleModuleInfo(moduleURL=_importURL, reload=reload, parentImportsSubtree=_moduleImportsSubtree)
458
- if _importModuleInfo:
459
- _importModuleInfo["isImported"] = True
460
- imports.append(_importModuleInfo)
483
+ for url in mergedImportURLs:
484
+ importURL = url
485
+ if not _isAbsoluteModuleURL(url):
486
+ # Handle relative imports when plugin is loaded from external directory.
487
+ # When EDGAR/render imports EDGAR/validate, this works if EDGAR is in the plugin directory
488
+ # but fails if loaded externally (e.g., dev repo clone at /dev/path/to/EDGAR/).
489
+ # Solution: Find common path segments to resolve /dev/path/to/EDGAR/validate
490
+ # from the importing module at /dev/path/to/EDGAR/render.
491
+ modulePath = Path(moduleFilename)
492
+ importPath = Path(url)
493
+ if importPath.parts:
494
+ importFirstPart = importPath.parts[0]
495
+ for i, modulePathPart in enumerate(reversed(modulePath.parts)):
496
+ if modulePathPart != importFirstPart:
497
+ continue
498
+ # Found a potential branching point, construct and check a new path
499
+ candidateImportURL = str(modulePath.parents[i] / importPath)
500
+ if normalizeModuleFilename(candidateImportURL):
501
+ importURL = candidateImportURL
502
+ importModuleInfo = moduleModuleInfo(moduleURL=importURL, reload=reload, parentImportsSubtree=moduleImportsSubtree)
503
+ if importModuleInfo:
504
+ importModuleInfo["isImported"] = True
505
+ imports.append(importModuleInfo)
461
506
  moduleInfo["imports"] = imports
462
507
  logPluginTrace(f"Successful module plug-in info: {moduleFilename}", logging.INFO)
463
508
  return moduleInfo
@@ -477,43 +522,30 @@ def moduleInfo(pluginInfo):
477
522
  moduleInfo.getdefault('classes', []).append(name)
478
523
 
479
524
 
480
- def _get_name_dir_prefix(
481
- controller: Cntlr,
482
- pluginBase: str,
483
- moduleURL: str,
484
- packagePrefix: str = "",
485
- ) -> tuple[str, str, str] | tuple[None, None, None]:
486
- """Get the name, directory and prefix of a module."""
487
- moduleFilename: str
488
- moduleDir: str
489
- packageImportPrefix: str
525
+ def _isAbsoluteModuleURL(moduleURL: str) -> bool:
526
+ return isAbsolute(moduleURL) or isLegacyAbs(moduleURL)
490
527
 
491
- moduleFilename = controller.webCache.getfilename(
492
- url=moduleURL, normalize=True, base=pluginBase, allowTransformation=False
493
- )
494
528
 
495
- if moduleFilename:
496
- if os.path.basename(moduleFilename) == "__init__.py" and os.path.isfile(
497
- moduleFilename
498
- ):
499
- moduleFilename = os.path.dirname(
500
- moduleFilename
501
- ) # want just the dirpart of package
502
-
503
- if os.path.isdir(moduleFilename) and os.path.isfile(
504
- os.path.join(moduleFilename, "__init__.py")
505
- ):
506
- moduleDir = os.path.dirname(moduleFilename)
507
- moduleName = os.path.basename(moduleFilename)
508
- packageImportPrefix = moduleName + "."
509
- else:
510
- moduleName = os.path.basename(moduleFilename).partition(".")[0]
511
- moduleDir = os.path.dirname(moduleFilename)
512
- packageImportPrefix = packagePrefix
529
+ def _get_name_dir_prefix(modulePath: Path, packagePrefix: str = "") -> tuple[str, str, str] | tuple[None, None, None]:
530
+ """Get the name, directory and prefix of a module."""
531
+ moduleName = None
532
+ moduleDir = None
533
+ packageImportPrefix = None
534
+ initFileName = "__init__.py"
535
+
536
+ if modulePath.is_file() and modulePath.name == initFileName:
537
+ modulePath = modulePath.parent
513
538
 
514
- return (moduleName, moduleDir, packageImportPrefix)
539
+ if modulePath.is_dir() and (modulePath / initFileName).is_file():
540
+ moduleName = modulePath.name
541
+ moduleDir = str(modulePath.parent)
542
+ packageImportPrefix = moduleName + "."
543
+ elif modulePath.is_file() and modulePath.suffix == ".py":
544
+ moduleName = modulePath.stem
545
+ moduleDir = str(modulePath.parent)
546
+ packageImportPrefix = packagePrefix
515
547
 
516
- return (None, None, None)
548
+ return (moduleName, moduleDir, packageImportPrefix)
517
549
 
518
550
  def _get_location(moduleDir: str, moduleName: str) -> str:
519
551
  """Get the file name of a plugin."""
@@ -543,13 +575,9 @@ def _find_and_load_module(moduleDir: str, moduleName: str) -> ModuleType | None:
543
575
  def loadModule(moduleInfo: dict[str, Any], packagePrefix: str="") -> None:
544
576
  name = moduleInfo['name']
545
577
  moduleURL = moduleInfo['moduleURL']
578
+ modulePath = Path(moduleInfo['path'])
546
579
 
547
- moduleName, moduleDir, packageImportPrefix = _get_name_dir_prefix(
548
- controller=_cntlr,
549
- pluginBase=_pluginBase,
550
- moduleURL=moduleURL,
551
- packagePrefix=packagePrefix,
552
- )
580
+ moduleName, moduleDir, packageImportPrefix = _get_name_dir_prefix(modulePath, packagePrefix)
553
581
 
554
582
  if all(p is None for p in [moduleName, moduleDir, packageImportPrefix]):
555
583
  _cntlr.addToLog(message=_ERROR_MESSAGE_IMPORT_TEMPLATE.format(name), level=logging.ERROR)
@@ -566,10 +594,12 @@ def loadModule(moduleInfo: dict[str, Any], packagePrefix: str="") -> None:
566
594
  localeDir = os.path.dirname(module.__file__) + os.sep + pluginInfo['localeURL']
567
595
  try:
568
596
  _gettext = gettext.translation(pluginInfo['localeDomain'], localeDir, getLanguageCodes())
569
- except IOError:
570
- _gettext = lambda x: x # no translation
597
+ except OSError:
598
+ def _gettext(x):
599
+ return x # no translation
571
600
  else:
572
- _gettext = lambda x: x
601
+ def _gettext(x):
602
+ return x
573
603
  for key, value in pluginInfo.items():
574
604
  if key == 'name':
575
605
  if name:
@@ -627,8 +657,7 @@ def pluginClassMethods(className: str) -> Iterator[Callable[..., Any]]:
627
657
  if className in pluginInfo:
628
658
  pluginMethodsForClass.append(pluginInfo[className])
629
659
  pluginMethodsForClasses[className] = pluginMethodsForClass
630
- for method in pluginMethodsForClass:
631
- yield method
660
+ yield from pluginMethodsForClass
632
661
 
633
662
 
634
663
  def addPluginModule(name: str) -> dict[str, Any] | None:
@@ -827,11 +856,7 @@ class EntryPointRef:
827
856
  Retrieve all installed plugin entry points.
828
857
  :return: List of all discovered entry points.
829
858
  """
830
- entryPoints: list[EntryPoint]
831
- if sys.version_info < (3, 10):
832
- entryPoints = [e for e in entry_points().get('arelle.plugin', [])]
833
- else:
834
- entryPoints = list(entry_points(group='arelle.plugin'))
859
+ entryPoints = list(entry_points(group='arelle.plugin'))
835
860
  entryPointRefs = []
836
861
  for entryPoint in entryPoints:
837
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(':')
@@ -31,6 +42,9 @@ def authority(url: str, includeScheme: bool=True) -> str:
31
42
  def scheme(url: str) -> str | None: # returns None if no scheme part
32
43
  return (url or "").rpartition(":")[0] or None
33
44
 
45
+ def isExternalUrl(url: str) -> bool:
46
+ return scheme(url) in ("http", "https", "ftp")
47
+
34
48
  absoluteUrlPattern = None
35
49
  # http://www.ietf.org/rfc/rfc2396.txt section 4.3
36
50
  # this pattern doesn't allow some valid unicode characters