Repository: Arelle/Arelle

Issue:
arelle.XbrlConst circular import
### What happened?

A circular import was detected while attempting to consume the latest version of Arelle in another service:
`arelle.XbrlConst` -> `arelle.ModelValue` -> `arelle.ModelObject` -> `arelle.XmlUtil` -> `arelle.XbrlConst`

```
Traceback (most recent call last):
  File "...py", line 0, in <module>
    from arelle import XbrlConst
  File "/Users/austinmatherne/.local/share/virtualenvs/service/lib/python3.9/site-packages/arelle/XbrlConst.py", line 1, in <module>
    from arelle.ModelValue import qname
  File "/Users/austinmatherne/.local/share/virtualenvs/service/lib/python3.9/site-packages/arelle/ModelValue.py", line 184, in <module>
    from arelle.ModelObject import ModelObject
  File "/Users/austinmatherne/.local/share/virtualenvs/service/lib/python3.9/site-packages/arelle/ModelObject.py", line 13, in <module>
    import arelle.XmlUtil
  File "/Users/austinmatherne/.local/share/virtualenvs/service/lib/python3.9/site-packages/arelle/XmlUtil.py", line 11, in <module>
    from arelle.XbrlConst import ixbrlAll, qnLinkFootnote, xhtml, xml, xsd, xhtml
ImportError: cannot import name 'ixbrlAll' from partially initialized module 'arelle.XbrlConst' (most likely due to a circular import) (/Users/austinmatherne/.local/share/virtualenvs/service/lib/python3.9/site-packages/arelle/XbrlConst.py)
```

### Documents

_No response_

### If running from the command line, what command did you run?

_No response_

### Interface

Python library (pip install)

### Version

29ba43a3452b17e87b1fe6862a7041d3697e3148

### Download

pip install

### Operating System

macOS 12 Monterey

Discussion:

Patch:
diff --git a/arelle/ModelObject.py b/arelle/ModelObject.py
index 302f7b25..a527ac48 100644
--- a/arelle/ModelObject.py
+++ b/arelle/ModelObject.py
@@ -10,7 +10,6 @@ from typing import TYPE_CHECKING, Any, Generator, Optional, cast
 from lxml import etree
 from arelle import Locale
 from arelle.ModelValue import qname, qnameEltPfxName, QName
-import arelle.XmlUtil
 
 if TYPE_CHECKING:
     from arelle.ModelDocument import ModelDocument
@@ -25,13 +24,16 @@ if TYPE_CHECKING:
     from arelle.ModelInstanceObject import ModelInlineFact
     from arelle.ModelInstanceObject import ModelDimensionValue
 
+XmlUtil: Any = None
 VALID_NO_CONTENT = None
 
 emptySet: set[Any] = set()
 
 def init() -> None: # init globals
-    global VALID_NO_CONTENT
-    from arelle.XmlValidate import VALID_NO_CONTENT # type: ignore[misc]
+    global XmlUtil, VALID_NO_CONTENT
+    if XmlUtil is None:
+        from arelle import XmlUtil
+        from arelle.XmlValidate import VALID_NO_CONTENT  # type: ignore[misc]
 
 class ModelObject(etree.ElementBase):
     """ModelObjects represent the XML elements within a document, and are implemented as custom
@@ -348,7 +350,7 @@ class ModelObject(etree.ElementBase):
             elif id in doc.idObjects:
                 return cast(ModelObject, doc.idObjects[id])
             else:
-                xpointedElement = arelle.XmlUtil.xpointerElement(doc,id)
+                xpointedElement = XmlUtil.xpointerElement(doc,id)
                 # find element
                 for docModelObject in doc.xmlRootElement.iter():
                     if docModelObject == xpointedElement:
@@ -388,7 +390,7 @@ class ModelObject(etree.ElementBase):
     @property
     def propertyView(self) -> tuple[Any, ...]:
         return (("QName", self.elementQname),) + tuple(
-                (arelle.XmlUtil.clarkNotationToPrefixedName(self, _tag, isAttribute=True), _value) # type: ignore[arg-type, misc]
+                (XmlUtil.clarkNotationToPrefixedName(self, _tag, isAttribute=True), _value)
                 for _tag, _value in self.items())
 
     def __repr__(self) -> str:


Test patch:
diff --git a/tests/unit_tests/arelle/test_import.py b/tests/unit_tests/arelle/test_import.py
new file mode 100644
index 00000000..4ca4918c
--- /dev/null
+++ b/tests/unit_tests/arelle/test_import.py
@@ -0,0 +1,47 @@
+import glob
+import os.path
+import subprocess
+import sys
+
+import pytest
+
+KNOWN_FAILURES = frozenset([
+    'CntlrProfiler.py',
+    'FormulaConsisAsser.py',
+    'FormulaEvaluator.py',
+    'FunctionCustom.py',
+    'FunctionFn.py',
+    'FunctionIxt.py',
+    'FunctionUtil.py',
+    'FunctionXfi.py',
+    'FunctionXs.py',
+    'ModelFormulaObject.py',
+    'ModelRenderingObject.py',
+    'ModelVersReport.py',
+    'PrototypeDtsObject.py',
+    'RenderingEvaluator.py',
+    'RenderingResolver.py',
+    'ValidateFormula.py',
+    'ValidateXbrlCalcs.py',
+    'ViewFileFormulae.py',
+    'ViewFileRelationshipSet.py',
+    'ViewFileRenderedGrid.py',
+    'ViewWinFormulae.py',
+    'XbrlUtil.py',
+    'XmlValidate.py',
+    'XPathContext.py',
+])
+FILE_NAMES = list(map(os.path.basename, glob.glob('arelle/*.py')))
+TEST_PARAMS = [
+    pytest.param(
+        file_name.replace('.py', ''),
+        id=file_name,
+        marks=[pytest.mark.xfail()] if file_name in KNOWN_FAILURES else []
+    ) for file_name in FILE_NAMES
+]
+
+
+@pytest.mark.parametrize('module_name', TEST_PARAMS)
+def test(module_name):
+    assert module_name.isidentifier()
+    subprocess.run([sys.executable, '-c', f'import arelle.{module_name}'], check=True)