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.
- arelle/BetaFeatures.py +0 -21
- arelle/Cntlr.py +15 -8
- arelle/CntlrCmdLine.py +121 -56
- arelle/CntlrWinMain.py +143 -70
- arelle/DialogFind.py +1 -1
- arelle/DialogPluginManager.py +6 -4
- arelle/DisclosureSystem.py +7 -0
- arelle/ErrorManager.py +21 -6
- arelle/FileSource.py +11 -4
- arelle/FunctionIxt.py +16 -11
- arelle/HtmlUtil.py +5 -4
- arelle/LeiUtil.py +63 -43
- arelle/ModelDocument.py +20 -15
- arelle/ModelDtsObject.py +8 -0
- arelle/ModelInstanceObject.py +1 -1
- arelle/ModelObject.py +16 -18
- arelle/ModelObjectFactory.py +35 -17
- arelle/ModelXbrl.py +28 -11
- arelle/PluginManager.py +130 -105
- arelle/RuntimeOptions.py +1 -0
- arelle/UrlUtil.py +14 -0
- arelle/Validate.py +17 -12
- arelle/ValidateDuplicateFacts.py +3 -1
- arelle/ValidateFileSource.py +38 -0
- arelle/ValidateFilingText.py +3 -3
- arelle/ValidateXbrl.py +5 -2
- arelle/ValidateXbrlCalcs.py +210 -186
- arelle/ValidateXbrlDTS.py +1 -1
- arelle/ViewFile.py +1 -0
- arelle/ViewFileFactTable.py +2 -2
- arelle/ViewWinDTS.py +4 -1
- arelle/WebCache.py +28 -24
- arelle/XbrlConst.py +22 -0
- arelle/XmlUtil.py +16 -21
- arelle/XmlValidate.py +6 -9
- arelle/_version.py +16 -3
- arelle/api/Session.py +11 -2
- arelle/config/disclosuresystems.xsd +2 -0
- arelle/config/rosettaEntitlements.plist +8 -0
- arelle/conformance/CSVTestcaseLoader.py +1 -1
- arelle/formula/XPathContext.py +3 -3
- arelle/logging/formatters/LogFormatter.py +3 -1
- arelle/packages/report/ReportPackage.py +26 -13
- arelle/packages/report/ReportPackageConst.py +0 -1
- arelle/plugin/inlineXbrlDocumentSet.py +19 -5
- arelle/plugin/validate/DBA/DisclosureSystems.py +19 -1
- arelle/plugin/validate/DBA/PluginValidationDataExtension.py +2 -4
- arelle/plugin/validate/DBA/ValidationPluginExtension.py +2 -1
- arelle/plugin/validate/DBA/resources/config.xml +5 -0
- arelle/plugin/validate/DBA/rules/__init__.py +2 -2
- arelle/plugin/validate/DBA/rules/fr.py +19 -2
- arelle/plugin/validate/DBA/rules/tc.py +2 -0
- arelle/plugin/validate/DBA/rules/th.py +6 -0
- arelle/plugin/validate/DBA/rules/tm.py +18 -5
- arelle/plugin/validate/DBA/rules/tr.py +11 -5
- arelle/plugin/validate/EDINET/Constants.py +193 -9
- arelle/plugin/validate/EDINET/ContextRequirement.py +58 -0
- arelle/plugin/validate/EDINET/ControllerPluginData.py +220 -1
- arelle/plugin/validate/EDINET/CoverItemRequirements.py +42 -0
- arelle/plugin/validate/EDINET/DeiRequirements.py +118 -0
- arelle/plugin/validate/EDINET/FilingFormat.py +275 -0
- arelle/plugin/validate/EDINET/FormType.py +134 -0
- arelle/plugin/validate/EDINET/ManifestInstance.py +72 -5
- arelle/plugin/validate/EDINET/NamespaceConfig.py +50 -0
- arelle/plugin/validate/EDINET/PluginValidationDataExtension.py +493 -132
- arelle/plugin/validate/EDINET/{InstanceType.py → ReportFolderType.py} +72 -15
- arelle/plugin/validate/EDINET/Statement.py +139 -0
- arelle/plugin/validate/EDINET/TableOfContentsBuilder.py +595 -0
- arelle/plugin/validate/EDINET/UploadContents.py +48 -0
- arelle/plugin/validate/EDINET/ValidationPluginExtension.py +20 -2
- arelle/plugin/validate/EDINET/__init__.py +31 -6
- arelle/plugin/validate/EDINET/resources/config.xml +8 -1
- arelle/plugin/validate/EDINET/resources/cover-item-requirements.json +793 -0
- arelle/plugin/validate/EDINET/resources/dei-requirements.csv +27 -0
- arelle/plugin/validate/EDINET/resources/edinet-taxonomies.xml +2 -0
- arelle/plugin/validate/EDINET/rules/contexts.py +375 -14
- arelle/plugin/validate/EDINET/rules/edinet.py +1934 -45
- arelle/plugin/validate/EDINET/rules/frta.py +122 -3
- arelle/plugin/validate/EDINET/rules/gfm.py +1907 -11
- arelle/plugin/validate/EDINET/rules/upload.py +989 -141
- arelle/plugin/validate/ESEF/Const.py +3 -1
- arelle/plugin/validate/ESEF/ESEF_2021/DTS.py +5 -0
- arelle/plugin/validate/ESEF/ESEF_2021/Image.py +2 -2
- arelle/plugin/validate/ESEF/ESEF_2021/ValidateXbrlFinally.py +23 -20
- arelle/plugin/validate/ESEF/ESEF_Current/DTS.py +47 -14
- arelle/plugin/validate/ESEF/ESEF_Current/ValidateXbrlFinally.py +100 -25
- arelle/plugin/validate/ESEF/__init__.py +20 -6
- arelle/plugin/validate/ESEF/resources/authority-validations.json +76 -9
- arelle/plugin/validate/ESEF/resources/config.xml +20 -0
- arelle/plugin/validate/NL/DisclosureSystems.py +22 -0
- arelle/plugin/validate/NL/PluginValidationDataExtension.py +27 -9
- arelle/plugin/validate/NL/ValidationPluginExtension.py +51 -7
- arelle/plugin/validate/NL/resources/config.xml +18 -0
- arelle/plugin/validate/NL/rules/br_kvk.py +17 -61
- arelle/plugin/validate/NL/rules/fg_nl.py +7 -38
- arelle/plugin/validate/NL/rules/fr_kvk.py +7 -42
- arelle/plugin/validate/NL/rules/fr_nl.py +31 -147
- arelle/plugin/validate/NL/rules/nl_kvk.py +142 -28
- arelle/plugin/validate/ROS/PluginValidationDataExtension.py +2 -0
- arelle/plugin/validate/ROS/ValidationPluginExtension.py +4 -1
- arelle/plugin/validate/ROS/rules/ros.py +41 -9
- arelle/plugin/validate/UK/ValidateUK.py +130 -66
- arelle/plugin/validate/UK/__init__.py +89 -103
- arelle/utils/EntryPointDetection.py +79 -13
- arelle/utils/PluginHooks.py +125 -0
- arelle/utils/validate/ESEFImage.py +6 -6
- arelle/utils/validate/Validation.py +18 -0
- arelle/utils/validate/ValidationPlugin.py +76 -11
- arelle/utils/validate/ValidationUtil.py +35 -3
- {arelle_release-2.37.46.dist-info → arelle_release-2.38.0.dist-info}/METADATA +30 -20
- {arelle_release-2.37.46.dist-info → arelle_release-2.38.0.dist-info}/RECORD +115 -191
- {arelle_release-2.37.46.dist-info → arelle_release-2.38.0.dist-info}/licenses/LICENSE.md +0 -3
- arelle/archive/CustomLogger.py +0 -43
- arelle/archive/LoadEFMvalidate.py +0 -32
- arelle/archive/LoadSavePreLbCsv.py +0 -26
- arelle/archive/LoadValidate.cs +0 -31
- arelle/archive/LoadValidate.py +0 -36
- arelle/archive/LoadValidateCmdLine.java +0 -69
- arelle/archive/LoadValidatePostedZip.java +0 -57
- arelle/archive/LoadValidateWebService.java +0 -34
- arelle/archive/SaveTableToExelle.py +0 -140
- arelle/archive/TR3toTR4.py +0 -88
- arelle/archive/plugin/ESEF_2022/__init__.py +0 -47
- arelle/archive/plugin/bigInstance.py +0 -394
- arelle/archive/plugin/cmdWebServerExtension.py +0 -43
- arelle/archive/plugin/crashTest.py +0 -38
- arelle/archive/plugin/functionsXmlCreation.py +0 -106
- arelle/archive/plugin/hello_i18n.pot +0 -26
- arelle/archive/plugin/hello_i18n.py +0 -32
- arelle/archive/plugin/importTestChild1.py +0 -21
- arelle/archive/plugin/importTestChild2.py +0 -22
- arelle/archive/plugin/importTestGrandchild1.py +0 -21
- arelle/archive/plugin/importTestGrandchild2.py +0 -21
- arelle/archive/plugin/importTestImported1.py +0 -23
- arelle/archive/plugin/importTestImported11.py +0 -22
- arelle/archive/plugin/importTestParent.py +0 -48
- arelle/archive/plugin/instanceInfo.py +0 -306
- arelle/archive/plugin/loadFromOIM-2018.py +0 -1282
- arelle/archive/plugin/locale/fr/LC_MESSAGES/hello_i18n.po +0 -25
- arelle/archive/plugin/objectmaker.py +0 -285
- arelle/archive/plugin/packagedImportTest/__init__.py +0 -47
- arelle/archive/plugin/packagedImportTest/importTestChild1.py +0 -21
- arelle/archive/plugin/packagedImportTest/importTestChild2.py +0 -22
- arelle/archive/plugin/packagedImportTest/importTestGrandchild1.py +0 -21
- arelle/archive/plugin/packagedImportTest/importTestGrandchild2.py +0 -21
- arelle/archive/plugin/packagedImportTest/importTestImported1.py +0 -24
- arelle/archive/plugin/packagedImportTest/importTestImported11.py +0 -21
- arelle/archive/plugin/packagedImportTest/subdir/importTestImported111.py +0 -21
- arelle/archive/plugin/packagedImportTest/subdir/subsubdir/importTestImported1111.py +0 -21
- arelle/archive/plugin/sakaCalendar.py +0 -215
- arelle/archive/plugin/saveInstanceInfoset.py +0 -121
- arelle/archive/plugin/sphinx/FormulaGenerator.py +0 -823
- arelle/archive/plugin/sphinx/SphinxContext.py +0 -404
- arelle/archive/plugin/sphinx/SphinxEvaluator.py +0 -783
- arelle/archive/plugin/sphinx/SphinxMethods.py +0 -1287
- arelle/archive/plugin/sphinx/SphinxParser.py +0 -1093
- arelle/archive/plugin/sphinx/SphinxValidator.py +0 -163
- arelle/archive/plugin/sphinx/US-GAAP Ratios Example.xsr +0 -52
- arelle/archive/plugin/sphinx/__init__.py +0 -285
- arelle/archive/plugin/streamingExtensions.py +0 -335
- arelle/archive/plugin/updateTableLB.py +0 -242
- arelle/archive/plugin/validate/SBRnl/CustomLoader.py +0 -19
- arelle/archive/plugin/validate/SBRnl/DTS.py +0 -305
- arelle/archive/plugin/validate/SBRnl/Dimensions.py +0 -357
- arelle/archive/plugin/validate/SBRnl/Document.py +0 -799
- arelle/archive/plugin/validate/SBRnl/Filing.py +0 -467
- arelle/archive/plugin/validate/SBRnl/__init__.py +0 -75
- arelle/archive/plugin/validate/SBRnl/config.xml +0 -26
- arelle/archive/plugin/validate/SBRnl/sbr-nl-taxonomies.xml +0 -754
- arelle/archive/plugin/validate/USBestPractices.py +0 -570
- arelle/archive/plugin/validate/USCorpAction.py +0 -557
- arelle/archive/plugin/validate/USSecTagging.py +0 -337
- arelle/archive/plugin/validate/XDC/__init__.py +0 -77
- arelle/archive/plugin/validate/XDC/config.xml +0 -20
- arelle/archive/plugin/validate/XFsyntax/__init__.py +0 -64
- arelle/archive/plugin/validate/XFsyntax/xf.py +0 -2227
- arelle/archive/plugin/validate/calc2.py +0 -536
- arelle/archive/plugin/validateSchemaLxml.py +0 -156
- arelle/archive/plugin/validateTableInfoset.py +0 -52
- arelle/archive/us-gaap-dei-docType-extraction-frm.xml +0 -90
- arelle/archive/us-gaap-dei-ratio-cash-frm.xml +0 -150
- arelle/examples/plugin/formulaSuiteConverter.py +0 -212
- arelle/examples/plugin/functionsCustom.py +0 -59
- arelle/examples/plugin/hello_dolly.py +0 -64
- arelle/examples/plugin/multi.py +0 -58
- arelle/examples/plugin/rssSaveOim.py +0 -96
- arelle/examples/plugin/validate/XYZ/DisclosureSystems.py +0 -2
- arelle/examples/plugin/validate/XYZ/PluginValidationDataExtension.py +0 -10
- arelle/examples/plugin/validate/XYZ/ValidationPluginExtension.py +0 -49
- arelle/examples/plugin/validate/XYZ/__init__.py +0 -75
- arelle/examples/plugin/validate/XYZ/resources/config.xml +0 -16
- arelle/examples/plugin/validate/XYZ/rules/__init__.py +0 -0
- arelle/examples/plugin/validate/XYZ/rules/rules01.py +0 -110
- arelle/examples/plugin/validate/XYZ/rules/rules02.py +0 -59
- arelle/model/CommentBase.py +0 -9
- arelle/model/ElementBase.py +0 -11
- arelle/model/PIBase.py +0 -10
- arelle/model/__init__.py +0 -15
- arelle/scripts-macOS/startWebServer.command +0 -3
- arelle/scripts-unix/startWebServer.sh +0 -1
- arelle/scripts-windows/startWebServer.bat +0 -5
- {arelle_release-2.37.46.dist-info → arelle_release-2.38.0.dist-info}/WHEEL +0 -0
- {arelle_release-2.37.46.dist-info → arelle_release-2.38.0.dist-info}/entry_points.txt +0 -0
- {arelle_release-2.37.46.dist-info → arelle_release-2.38.0.dist-info}/top_level.txt +0 -0
arelle/ModelObjectFactory.py
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
110
|
-
return
|
|
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
|
|
164
|
+
return etree.PIBase
|
|
148
165
|
elif node_type == "entity":
|
|
149
|
-
return etree.EntityBase
|
|
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):
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
if
|
|
197
|
-
if
|
|
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,
|
|
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))])
|
|
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
|
|
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) ->
|
|
872
|
-
|
|
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
|
|
881
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
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: {}"
|
|
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
|
-
|
|
462
|
+
moduleImportsSubtree = False
|
|
430
463
|
mergedImportURLs = []
|
|
431
464
|
|
|
432
|
-
for
|
|
433
|
-
if
|
|
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
|
|
437
|
-
|
|
438
|
-
elif
|
|
469
|
+
if url == "module_import_subtree":
|
|
470
|
+
moduleImportsSubtree = True
|
|
471
|
+
elif url == "module_subtree":
|
|
439
472
|
for _dir in os.listdir(moduleDir):
|
|
440
|
-
|
|
441
|
-
if os.path.isdir(
|
|
442
|
-
mergedImportURLs.append(
|
|
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(
|
|
445
|
-
if parentImportsSubtree and not
|
|
446
|
-
|
|
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
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
|
481
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
570
|
-
_gettext
|
|
597
|
+
except OSError:
|
|
598
|
+
def _gettext(x):
|
|
599
|
+
return x # no translation
|
|
571
600
|
else:
|
|
572
|
-
_gettext
|
|
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
|
-
|
|
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
|
|
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
|