arelle-release 2.37.43__py3-none-any.whl → 2.37.45__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of arelle-release might be problematic. Click here for more details.

arelle/Cntlr.py CHANGED
@@ -24,6 +24,8 @@ import regex as re
24
24
 
25
25
  from arelle import Locale, ModelManager, PackageManager, PluginManager, XbrlConst
26
26
  from arelle.BetaFeatures import BETA_FEATURES_AND_DESCRIPTIONS
27
+ from arelle.ErrorManager import ErrorManager
28
+ from arelle.FileSource import FileSource
27
29
  from arelle.SystemInfo import PlatformOS, getSystemWordSize, hasFileSystem, hasWebServer, isCGI, isGAE
28
30
  from arelle.WebCache import WebCache
29
31
  from arelle.logging.formatters.LogFormatter import LogFormatter, logRefsFileLines # noqa: F401 - for reimport
@@ -123,6 +125,7 @@ class Cntlr:
123
125
  """
124
126
  __version__ = "1.6.0"
125
127
  betaFeatures: dict[str, bool]
128
+ errorManager: ErrorManager | None
126
129
  hasWin32gui: bool
127
130
  hasGui: bool
128
131
  hasFileSystem: bool
@@ -164,6 +167,7 @@ class Cntlr:
164
167
  b: betaFeatures.get(b, False)
165
168
  for b in BETA_FEATURES_AND_DESCRIPTIONS.keys()
166
169
  }
170
+ self.errorManager = None
167
171
  self.hasWin32gui = False
168
172
  self.hasGui = hasGui
169
173
  self.hasFileSystem = hasFileSystem() # no file system on Google App Engine servers
@@ -291,6 +295,32 @@ class Cntlr:
291
295
  PackageManager.init(self, loadPackagesConfig=hasGui)
292
296
 
293
297
  self.startLogging(logFileName, logFileMode, logFileEncoding, logFormat)
298
+ self.errorManager = ErrorManager(self.modelManager, logging._checkLevel("INCONSISTENCY")) # type: ignore[attr-defined]
299
+
300
+ def error(self, codes: Any, msg: str, level: str = "ERROR", fileSource: FileSource | None = None, **args: Any) -> None:
301
+ if self.logger is None or self.errorManager is None:
302
+ self.addToLog(
303
+ message=msg,
304
+ messageCode=str(codes),
305
+ messageArgs=args,
306
+ level=level
307
+ )
308
+ return
309
+ self.errorManager.log(
310
+ self.logger,
311
+ level,
312
+ codes,
313
+ msg,
314
+ fileSource=fileSource,
315
+ logRefObjectProperties=getattr(self.logger, "logRefObjectProperties", False),
316
+ **args
317
+ )
318
+
319
+ @property
320
+ def errors(self) -> list[str | None]:
321
+ if self.errorManager is None:
322
+ return []
323
+ return self.errorManager.errors
294
324
 
295
325
  @property
296
326
  def uiLangDir(self) -> str:
@@ -662,3 +692,8 @@ class Cntlr:
662
692
 
663
693
  def _clearPluginData(self) -> None:
664
694
  self.__pluginData.clear()
695
+
696
+ def testcaseVariationReset(self) -> None:
697
+ self._clearPluginData()
698
+ if self.errorManager is not None:
699
+ self.errorManager.clear()
arelle/ErrorManager.py ADDED
@@ -0,0 +1,306 @@
1
+ """
2
+ See COPYRIGHT.md for copyright information.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import logging
7
+ import os
8
+ from collections import defaultdict
9
+ from decimal import Decimal
10
+ from typing import TYPE_CHECKING, Any, Union, cast
11
+
12
+ from arelle import UrlUtil, XmlUtil, ModelValue, XbrlConst
13
+ from arelle.FileSource import FileSource
14
+ from arelle.Locale import format_string
15
+ from arelle.ModelObject import ModelObject, ObjectPropertyViewWrapper
16
+ from arelle.PluginManager import pluginClassMethods
17
+ from arelle.PythonUtil import flattenSequence
18
+
19
+ if TYPE_CHECKING:
20
+ from arelle.ModelManager import ModelManager
21
+ from arelle.ModelXbrl import ModelXbrl
22
+ from arelle.typing import EmptyTuple
23
+
24
+ LoggableValue = Union[str, dict[Any, Any], list[Any], set[Any], tuple[Any, ...]]
25
+ EMPTY_TUPLE: EmptyTuple = ()
26
+
27
+
28
+ class ErrorManager:
29
+ _errorCaptureLevel: int
30
+ _errors: list[str | None]
31
+ _logCount: dict[str, int] = {}
32
+ _logHasRelevelerPlugin: bool
33
+ _logRefFileRelUris: defaultdict[Any, dict[str, str]]
34
+ _modelManager: ModelManager
35
+
36
+ def __init__(self, modelManager: ModelManager, errorCaptureLevel: int):
37
+ self._errorCaptureLevel = errorCaptureLevel
38
+ self._errors = []
39
+ self._logCount = {}
40
+ self._logRefFileRelUris = defaultdict(dict)
41
+ self._modelManager = modelManager
42
+
43
+ @property
44
+ def errors(self) -> list[str | None]:
45
+ return self._errors
46
+
47
+ @property
48
+ def logCount(self) -> dict[str, int]:
49
+ return self._logCount
50
+
51
+ def _effectiveMessageCode(self, messageCodes: tuple[Any] | str) -> str | None:
52
+ """
53
+ If codes includes EFM, GFM, HMRC, or SBR-coded error then the code chosen (if a sequence)
54
+ corresponds to whether EFM, GFM, HMRC, or SBR validation is in effect.
55
+ """
56
+ effectiveMessageCode = None
57
+ _validationType = self._modelManager.disclosureSystem.validationType
58
+ _exclusiveTypesPattern = self._modelManager.disclosureSystem.exclusiveTypesPattern
59
+
60
+ for argCode in messageCodes if isinstance(messageCodes,tuple) else (messageCodes,):
61
+ if (isinstance(argCode, ModelValue.QName) or
62
+ (_validationType and argCode and argCode.startswith(_validationType)) or
63
+ (not _exclusiveTypesPattern or _exclusiveTypesPattern.match(argCode or "") == None)):
64
+ effectiveMessageCode = argCode
65
+ break
66
+ return effectiveMessageCode
67
+
68
+ def clear(self) -> None:
69
+ self._errors.clear()
70
+ self._logCount.clear()
71
+
72
+ def isLoggingEffectiveFor(self, logger: logging.Logger, **kwargs: Any) -> bool: # args can be messageCode(s) and level
73
+ assert hasattr(logger, 'messageCodeFilter'), 'messageCodeFilter not set on controller logger.'
74
+ assert hasattr(logger, 'messageLevelFilter'), 'messageLevelFilter not set on controller logger.'
75
+ if "messageCodes" in kwargs or "messageCode" in kwargs:
76
+ if "messageCodes" in kwargs:
77
+ messageCodes = kwargs["messageCodes"]
78
+ else:
79
+ messageCodes = kwargs["messageCode"]
80
+ messageCode = self._effectiveMessageCode(messageCodes)
81
+ codeEffective = (messageCode and
82
+ (not logger.messageCodeFilter or logger.messageCodeFilter.match(messageCode)))
83
+ else:
84
+ codeEffective = True
85
+ if "level" in kwargs and logger.messageLevelFilter:
86
+ levelEffective = logger.messageLevelFilter.match(kwargs["level"].lower())
87
+ else:
88
+ levelEffective = True
89
+ return bool(codeEffective and levelEffective)
90
+
91
+ def log(
92
+ self,
93
+ logger: logging.Logger,
94
+ level: str,
95
+ codes: Any,
96
+ msg: str,
97
+ sourceModelXbrl: ModelXbrl | None = None,
98
+ fileSource: FileSource | None = None,
99
+ entryLoadingUrl: str | None = None,
100
+ logRefObjectProperties: bool = False,
101
+ **args: Any
102
+ ) -> None:
103
+ """Same as error(), but level passed in as argument
104
+ """
105
+ assert hasattr(logger, 'messageCodeFilter'), 'messageCodeFilter not set on controller logger.'
106
+ messageCodeFilter = getattr(logger, 'messageCodeFilter')
107
+ assert hasattr(logger, 'messageLevelFilter'), 'messageLevelFilter not set on controller logger.'
108
+ messageLevelFilter = getattr(logger, 'messageLevelFilter')
109
+ # determine logCode
110
+ messageCode = self._effectiveMessageCode(codes)
111
+ if messageCode == "asrtNoLog":
112
+ self._errors.append(args["assertionResults"])
113
+ return
114
+ if sourceModelXbrl is not None and any(True for m in pluginClassMethods("Logging.Severity.Releveler")):
115
+ for pluginXbrlMethod in pluginClassMethods("Logging.Severity.Releveler"):
116
+ level, messageCode = pluginXbrlMethod(sourceModelXbrl, level, messageCode, args) # args must be passed as dict because it may contain modelXbrl or messageCode key value
117
+ if (messageCode and
118
+ (not messageCodeFilter or messageCodeFilter.match(messageCode)) and
119
+ (not messageLevelFilter or messageLevelFilter.match(level.lower()))):
120
+ # note that plugin Logging.Message.Parameters may rewrite messageCode which now occurs after filtering on messageCode
121
+ messageCode, logArgs, extras = self._logArguments(
122
+ messageCode,
123
+ msg,
124
+ args,
125
+ sourceModelXbrl=sourceModelXbrl,
126
+ fileSource=fileSource,
127
+ entryLoadingUrl=entryLoadingUrl,
128
+ logRefObjectProperties=logRefObjectProperties,
129
+ )
130
+ numericLevel = logging._checkLevel(level) #type: ignore[attr-defined]
131
+ self._logCount[numericLevel] = self._logCount.get(numericLevel, 0) + 1
132
+ if numericLevel >= self._errorCaptureLevel:
133
+ try: # if there's a numeric errorCount arg, extend messages codes by count
134
+ self._errors.extend([messageCode] * int(logArgs[1]["errorCount"]))
135
+ except (IndexError, KeyError, ValueError): # no msgArgs, no errorCount, or not int
136
+ self._errors.append(messageCode) # assume one error occurence
137
+ """@messageCatalog=[]"""
138
+ logger.log(numericLevel, *logArgs, exc_info=args.get("exc_info"), extra=extras)
139
+
140
+ def _logArguments(
141
+ self,
142
+ messageCode: str,
143
+ msg: str,
144
+ codedArgs: dict[str, str],
145
+ sourceModelXbrl: ModelXbrl | None = None,
146
+ fileSource: FileSource | None = None,
147
+ entryLoadingUrl: str | None = None,
148
+ logRefObjectProperties: bool = False,
149
+ ) -> Any:
150
+ # Prepares arguments for logger function as per info() below.
151
+
152
+ def propValues(properties: Any) -> Any:
153
+ # deref objects in properties
154
+ return [(p[0], str(p[1])) if len(p) == 2 else (p[0], str(p[1]), propValues(p[2]))
155
+ for p in properties if 2 <= len(p) <= 3]
156
+ # determine message and extra arguments
157
+ fmtArgs: dict[str, LoggableValue] = {}
158
+ extras: dict[str, Any] = {"messageCode":messageCode}
159
+ modelObjectArgs: tuple[Any, ...] | list[Any] = ()
160
+ sourceModelDocument = None
161
+ if sourceModelXbrl is not None:
162
+ sourceModelDocument = sourceModelXbrl.modelDocument
163
+
164
+ for argName, argValue in codedArgs.items():
165
+ if argName in ("modelObject", "modelXbrl", "modelDocument"):
166
+ if sourceModelDocument is not None:
167
+ entryUrl = sourceModelDocument.uri
168
+ else:
169
+ if entryLoadingUrl is not None:
170
+ entryUrl = entryLoadingUrl
171
+ else:
172
+ assert fileSource is not None, 'Expected FileSource to be available for fallback entry URL.'
173
+ entryUrl = fileSource.url
174
+ refs: list[dict[str, Any]] = []
175
+ modelObjectArgs_complex = argValue if isinstance(argValue, (tuple,list,set)) else (argValue,)
176
+ modelObjectArgs = flattenSequence(modelObjectArgs_complex)
177
+ for arg in modelObjectArgs:
178
+ if arg is not None:
179
+ if isinstance(arg, str):
180
+ objectUrl = arg
181
+ else:
182
+ try:
183
+ objectUrl = arg.modelDocument.displayUri
184
+ except AttributeError:
185
+ try:
186
+ objectUrl = arg.displayUri
187
+ except AttributeError:
188
+ try:
189
+ objectUrl = sourceModelDocument.displayUri # type: ignore[union-attr]
190
+ except AttributeError:
191
+ objectUrl = entryLoadingUrl or ""
192
+ try:
193
+ if objectUrl.endswith("/_IXDS"):
194
+ file = objectUrl[:-6] # inline document set or report package
195
+ elif objectUrl in self._logRefFileRelUris.get(entryUrl, EMPTY_TUPLE):
196
+ file = self._logRefFileRelUris[entryUrl][objectUrl]
197
+ else:
198
+ file = UrlUtil.relativeUri(entryUrl, objectUrl)
199
+ self._logRefFileRelUris[entryUrl][objectUrl] = file
200
+ except:
201
+ file = ""
202
+ ref: dict[str, Any] = {}
203
+ if isinstance(arg,(ModelObject, ObjectPropertyViewWrapper)):
204
+ _arg:ModelObject = arg.modelObject if isinstance(arg, ObjectPropertyViewWrapper) else arg
205
+ if len(modelObjectArgs) > 1 and getattr(arg,"tag",None) == "instance":
206
+ continue # skip IXDS top level element
207
+ fragmentIdentifier = cast(str, XmlUtil.elementFragmentIdentifier(_arg))
208
+ if not hasattr(_arg, 'modelDocument') and _arg.namespaceURI == XbrlConst.svg:
209
+ # This is an embedded SVG document without its own file.
210
+ ref["href"] = "#" + fragmentIdentifier
211
+ else:
212
+ ref["href"] = file + "#" + fragmentIdentifier
213
+ ref["sourceLine"] = _arg.sourceline
214
+ ref["objectId"] = _arg.objectId()
215
+ if logRefObjectProperties:
216
+ try:
217
+ ref["properties"] = propValues(arg.propertyView)
218
+ except AttributeError:
219
+ pass # is a default properties entry appropriate or needed?
220
+ if any(True for m in pluginClassMethods("Logging.Ref.Properties")):
221
+ refProperties: Any = ref.get("properties", {})
222
+ for pluginXbrlMethod in pluginClassMethods("Logging.Ref.Properties"):
223
+ pluginXbrlMethod(arg, refProperties, codedArgs)
224
+ if refProperties:
225
+ ref["properties"] = refProperties
226
+ else:
227
+ ref["href"] = file
228
+ try:
229
+ ref["sourceLine"] = arg.sourceline
230
+ except AttributeError:
231
+ pass # arg may not have sourceline, ignore if so
232
+ if any(True for m in pluginClassMethods("Logging.Ref.Attributes")):
233
+ refAttributes: dict[str, str] = {}
234
+ for pluginXbrlMethod in pluginClassMethods("Logging.Ref.Attributes"):
235
+ pluginXbrlMethod(arg, refAttributes, codedArgs)
236
+ if refAttributes:
237
+ ref["customAttributes"] = refAttributes
238
+ refs.append(ref)
239
+ extras["refs"] = refs
240
+ elif argName == "sourceFileLine":
241
+ # sourceFileLines is pairs of file and line numbers, e.g., ((file,line),(file2,line2),...)
242
+ ref = {}
243
+ if isinstance(argValue, (tuple,list)):
244
+ ref["href"] = str(argValue[0])
245
+ if len(argValue) > 1 and argValue[1]:
246
+ ref["sourceLine"] = str(argValue[1])
247
+ else:
248
+ ref["href"] = str(argValue)
249
+ extras["refs"] = [ref]
250
+ elif argName == "sourceFileLines":
251
+ # sourceFileLines is tuple/list of pairs of file and line numbers, e.g., ((file,line),(file2,line2),...)
252
+ sf_refs: list[dict[str, str]] = []
253
+ argvalues: tuple[Any, ...] | list[Any] = argValue if isinstance(argValue, (tuple, list)) else (argValue,)
254
+ for arg in argvalues:
255
+ ref = {}
256
+ if isinstance(arg, (tuple, list)):
257
+ arg_: tuple[Any, ...] | list[Any] = arg
258
+ ref["href"] = str(arg_[0])
259
+ if len(arg_) > 1 and arg_[1]:
260
+ ref["sourceLine"] = str(arg_[1])
261
+ else:
262
+ ref["href"] = str(arg)
263
+ sf_refs.append(ref)
264
+ extras["refs"] = sf_refs
265
+ elif argName == "sourceLine":
266
+ if isinstance(argValue, int): # must be sortable with int's in logger
267
+ extras["sourceLine"] = argValue
268
+ elif argName not in ("exc_info", "messageCodes"):
269
+ fmtArgs[argName] = self._loggableValue(argValue) # dereference anything not loggable
270
+
271
+ if "refs" not in extras:
272
+ if sourceModelDocument is not None:
273
+ file = sourceModelDocument.displayUri
274
+ else:
275
+ if entryLoadingUrl is not None:
276
+ file = os.path.basename(entryLoadingUrl)
277
+ else:
278
+ file = ""
279
+ extras["refs"] = [{"href": file}]
280
+ for pluginXbrlMethod in pluginClassMethods("Logging.Message.Parameters"):
281
+ # plug in can rewrite msg string or return msg if not altering msg
282
+ msg = pluginXbrlMethod(messageCode, msg, modelObjectArgs, fmtArgs) or msg
283
+ return (messageCode,
284
+ (msg, fmtArgs) if fmtArgs else (msg,),
285
+ extras)
286
+
287
+ def _loggableValue(self, argValue: Any) -> LoggableValue: # must be dereferenced and not related to object lifetimes
288
+ if argValue is None:
289
+ return "(none)"
290
+ if isinstance(argValue, bool):
291
+ return str(argValue).lower() # show lower case true/false xml values
292
+ if isinstance(argValue, int):
293
+ # need locale-dependent formatting
294
+ return format_string(self._modelManager.locale, '%i', argValue)
295
+ if isinstance(argValue, (float, Decimal)):
296
+ # need locale-dependent formatting
297
+ return format_string(self._modelManager.locale, '%f', argValue)
298
+ if isinstance(argValue, tuple):
299
+ return tuple(self._loggableValue(x) for x in argValue)
300
+ if isinstance(argValue, list):
301
+ return [self._loggableValue(x) for x in argValue]
302
+ if isinstance(argValue, set):
303
+ return {self._loggableValue(x) for x in argValue}
304
+ if isinstance(argValue, dict):
305
+ return dict((self._loggableValue(k), self._loggableValue(v)) for k, v in argValue.items())
306
+ return str(argValue)
@@ -288,7 +288,7 @@ class ModelRelationshipSet:
288
288
 
289
289
  # if modelFrom and modelTo are provided determine that they have specified relationship
290
290
  # if only modelFrom, determine that there are relationships present of specified axis
291
- def isRelated(self, modelFrom, axis, modelTo=None, visited=None, isDRS=False, consecutiveLinkrole=False): # either model concept or qname
291
+ def isRelated(self, modelFrom, axis, modelTo=None, visited=None, isDRS=False, consecutiveLinkrole=False) -> bool: # either model concept or qname
292
292
  assert self.modelXbrl is not None
293
293
  if getattr(self.modelXbrl, "isSupplementalIxdsTarget", False):
294
294
  if modelFrom is not None and modelFrom.modelXbrl != self.modelXbrl: