Skip to content

Commit

Permalink
Merge pull request #861 from austinmatherne-wk/XT-3160
Browse files Browse the repository at this point in the history
Create example validation plugin
  • Loading branch information
derekgengenbacher-wf committed Sep 20, 2023
2 parents f48963b + c7f9bc8 commit 012c756
Show file tree
Hide file tree
Showing 49 changed files with 1,590 additions and 204 deletions.
54 changes: 25 additions & 29 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,35 +1,40 @@
__pycache__
*.iml
*.pyc
build
dist
buildRenameX64.bat
buildRenameX86.bat
.DS_Store
.eggs
.python-version
.tox
/.mypy_cache/
/.pytest_cache/
/venv/
Arelle.egg-info
Pipfile
Pipfile.lock
__pycache__
arelle-release*
arelle/_version.py
arelle/plugin/EdgarRenderer
arelle/plugin/EdgarRendererWithBuiltinHtml
arelle/plugin/FERC
arelle/plugin/Xince.py
arelle/plugin/iXBRLViewerPlugin
arelle/plugin/validate/DQC_test
arelle/plugin/validate/DQC_US_Rules
arelle/plugin/validate/DQC.py
arelle/plugin/validate/eforms.py
arelle/plugin/validate/DQC_US_Rules
arelle/plugin/validate/DQC_test
arelle/plugin/validate/ESEF-DQC.py
arelle/plugin/validate/eforms.py
arelle/plugin/xendr
arelle/plugin/Xince.py
arelle/plugin/xule
Arelle.egg-info
arelleGUI.py
.eggs
.python-version
*.iml

.tox
arelle_release.egg-info
arelle-release*
/.mypy_cache/
/.pytest_cache/

/venv/
bin/
build
buildRenameX64.bat
buildRenameX86.bat
dist
docs/_build
docs/source/apidocs
obj/
tests/resources/conformance_suites/

### IDE-specific ignores
Expand All @@ -43,12 +48,3 @@ tests/resources/conformance_suites/

# Visual Studio
.vs/

bin/
obj/

Pipfile.lock
Pipfile

.DS_Store
docs/_build
2 changes: 1 addition & 1 deletion arelle/CntlrWinMain.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ def __init__(self, parent):
helpMenu.add_command(label=label, underline=0, command=command, accelerator=shortcut_text)
self.parent.bind(shortcut, command)
for pluginMenuExtender in pluginClassMethods("CntlrWinMain.Menu.Help"):
pluginMenuExtender(self, toolsMenu)
pluginMenuExtender(self, helpMenu)
self.menubar.add_cascade(label=_("Help"), menu=helpMenu, underline=0)

windowFrame = Frame(self.parent)
Expand Down
6 changes: 0 additions & 6 deletions arelle/FunctionIxt.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,9 +538,6 @@ def datemonthdayLongEnTR1(arg):
def datemonthdayShortEnTR1(arg):
return datedaymonth(arg, monthdayShortEnTR1Pattern, dy=2, mo=1)

def datedaymonthel(arg):
return datedaymonth(arg, daymonthElPattern)

def datedaymonthes(arg):
return datedaymonth(arg, daymonthEsPattern)

Expand Down Expand Up @@ -680,9 +677,6 @@ def datemonthyeares(arg):
def dateyearmonthen(arg):
return datemonthyear(arg, yearmonthEnPattern, mo=2, yr=1)

def datemonthyeares(arg):
return datemonthyear(arg, monthyearEsPattern)

def datemonthyearet(arg):
return datemonthyear(arg, monthyearEtPattern)

Expand Down
15 changes: 10 additions & 5 deletions arelle/PluginManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,8 @@
import arelle.FileSource
from arelle.UrlUtil import isAbsolute
from pathlib import Path
try:
from collections import OrderedDict
except ImportError:
OrderedDict = dict # python 3.0 lacks OrderedDict, json file will be in weird order
from collections.abc import Generator, Callable
from collections import OrderedDict, defaultdict
from collections.abc import Callable


if TYPE_CHECKING:
Expand Down Expand Up @@ -233,6 +230,7 @@ def moduleModuleInfo(moduleURL, reload=False, parentImportsSubtree=False):
tree = ast.parse(f.read(), filename=moduleFilename)
constantStrings = {}
functionDefNames = set()
methodDefNamesByClass = defaultdict(set)
moduleImports = []
for item in tree.body:
if isinstance(item, ast.Assign):
Expand All @@ -259,6 +257,9 @@ def moduleModuleInfo(moduleURL, reload=False, parentImportsSubtree=False):
moduleInfo[_key] = constantStrings[_value.id]
elif _value.id in functionDefNames:
classMethods.append(_key)
elif _valueType == 'Attribute':
if _value.attr in methodDefNamesByClass[_value.value.id]:
classMethods.append(_key)
elif _key == "imports" and _valueType in ("List", "Tuple"):
importURLs = [elt.s for elt in _value.elts]
moduleInfo['classMethods'] = classMethods
Expand Down Expand Up @@ -325,6 +326,10 @@ def moduleModuleInfo(moduleURL, reload=False, parentImportsSubtree=False):
moduleImports.append(_importeePfxName)
elif isinstance(item, ast.FunctionDef): # possible functionDef used in plugininfo
functionDefNames.add(item.name)
elif isinstance(item, ast.ClassDef): # possible ClassDef used in plugininfo
for classItem in item.body:
if isinstance(classItem, ast.FunctionDef):
methodDefNamesByClass[item.name].add(classItem.name)
logPluginTrace(f"Successful module plug-in info: {moduleFilename}", logging.INFO)
except Exception as err:
_msg = _("Exception obtaining plug-in module info: {moduleFilename}\n{error}\n{traceback}").format(
Expand Down
17 changes: 17 additions & 0 deletions arelle/ValidateXbrl.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from arelle.XmlValidateConst import VALID
from collections import defaultdict
from arelle.typing import TypeGetText
from arelle.utils.validate.PluginValidationData import PluginValidationData
from arelle.ModelRelationshipSet import ModelRelationshipSet
from arelle.ModelDtsObject import ModelRelationship
from arelle.ModelFormulaObject import ModelCustomFunctionSignature
Expand Down Expand Up @@ -116,6 +117,7 @@ def validate(self, modelXbrl: ModelXbrl, parameters: dict[Any, Any] | None = Non
for concept in self.modelXbrl.nameConcepts.get("UTR",()))))
self.validateIXDS = False # set when any inline document found
self.validateEnum = bool(XbrlConst.enums & modelXbrl.namespaceDocs.keys())
self._pluginData: dict[str, PluginValidationData] = {}

for pluginXbrlMethod in pluginClassMethods("Validate.XBRL.Start"):
pluginXbrlMethod(self, parameters)
Expand Down Expand Up @@ -1108,3 +1110,18 @@ def isGenericReference(self, elt: ModelObject) -> bool:
def executeCallTest(self, modelXbrl: ModelXbrl, name: str, callTuple: tuple[Any, ...], testTuple: tuple[Any, ...]) -> None:
self.modelXbrl = modelXbrl
ValidateFormula.executeCallTest(self, name, callTuple, testTuple)

def getPluginData(self, pluginName: str) -> PluginValidationData | None:
allPluginData = getattr(self, "_pluginData", None)
if allPluginData is None:
raise RuntimeError("PluginData can't be retrieved until validation has begun.")
pluginData: PluginValidationData = allPluginData.get(pluginName)
return pluginData

def setPluginData(self, pluginData: PluginValidationData) -> None:
allPluginData = getattr(self, "_pluginData", None)
if allPluginData is None:
raise RuntimeError("PluginData can't be set until validation has begun.")
if pluginData.name in allPluginData:
raise RuntimeError(f"PluginData already set on ValidateXbrl for {pluginData.name}.")
allPluginData[pluginData.name] = pluginData
2 changes: 2 additions & 0 deletions arelle/examples/plugin/validate/XYZ/DisclosureSystems.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DISCLOSURE_SYSTEM_2022 = "XYZ 2022"
DISCLOSURE_SYSTEM_2023 = "XYZ 2023"
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""
See COPYRIGHT.md for copyright information.
"""
from __future__ import annotations

from arelle.utils.validate.PluginValidationData import PluginValidationData


class PluginValidationDataExtension(PluginValidationData):
positiveFactConcepts: set[str] | None = None
48 changes: 48 additions & 0 deletions arelle/examples/plugin/validate/XYZ/ValidationPluginExtension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""
See COPYRIGHT.md for copyright information.
"""
from __future__ import annotations

from typing import Any

from arelle.ModelDocument import LoadingException, ModelDocument
from arelle.ModelXbrl import ModelXbrl
from arelle.typing import TypeGetText
from arelle.utils.validate.ValidationPlugin import ValidationPlugin
from .PluginValidationDataExtension import PluginValidationDataExtension

_: TypeGetText


class ValidationPluginExtension(ValidationPlugin):
def newPluginData(self) -> PluginValidationDataExtension:
return PluginValidationDataExtension(self.name)

def modelDocumentPullLoader(
self,
modelXbrl: ModelXbrl,
normalizedUri: str,
filepath: str,
isEntry: bool,
namespace: str | None,
*args: Any,
**kwargs: Any,
) -> ModelDocument | LoadingException | None:
if self.disclosureSystemFromPluginSelected(modelXbrl):
return LoadingException(_("XYZ validation plugin is a template for new validation plugins and shouldn't be used directly."))
return None

def modelXbrlLoadComplete(
self,
modelXbrl: ModelXbrl,
*args: Any,
**kwargs: Any,
) -> None:
if self.disclosureSystemFromPluginSelected(modelXbrl):
if modelXbrl.modelDocument is None:
modelXbrl.error(
codes="XYZ.01.01",
msg=_("An XBRL Report Package is required but could not be loaded"),
modelObject=modelXbrl,
)
return None
70 changes: 70 additions & 0 deletions arelle/examples/plugin/validate/XYZ/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""
See COPYRIGHT.md for copyright information.
Filer Manual Guidelines: https://www.example.com/fake-xyz-filer-manual-v0.0.1.pdf
"""
from __future__ import annotations

from pathlib import Path
from typing import Any

from arelle.ModelDocument import LoadingException, ModelDocument
from arelle.Version import authorLabel, copyrightLabel
from .ValidationPluginExtension import ValidationPluginExtension
from .rules import rules01, rules02

PLUGIN_NAME = "Validate XYZ"
DISCLOSURE_SYSTEM_VALIDATION_TYPE = "XYZ"


validationPlugin = ValidationPluginExtension(
name=PLUGIN_NAME,
disclosureSystemConfigUrl=Path(__file__).parent / "resources" / "config.xml",
validationTypes=[DISCLOSURE_SYSTEM_VALIDATION_TYPE],
validationRuleModules=[rules01, rules02],
)


def disclosureSystemTypes(*args: Any, **kwargs: Any) -> tuple[tuple[str, str], ...]:
return validationPlugin.disclosureSystemTypes


def disclosureSystemConfigURL(*args: Any, **kwargs: Any) -> str:
return validationPlugin.disclosureSystemConfigURL


def modelDocumentPullLoader(*args: Any, **kwargs: Any) -> ModelDocument | LoadingException | None:
return validationPlugin.modelDocumentPullLoader(*args, **kwargs)


def modelXbrlLoadComplete(*args: Any, **kwargs: Any) -> None:
return validationPlugin.modelXbrlLoadComplete(*args, **kwargs)


def validateXbrlFinally(*args: Any, **kwargs: Any) -> None:
return validationPlugin.validateXbrlFinally(*args, **kwargs)


def validateXbrlDtsDocument(*args: Any, **kwargs: Any) -> None:
return validationPlugin.validateXbrlDtsDocument(*args, **kwargs)


def validateFinally(*args: Any, **kwargs: Any) -> None:
return validationPlugin.validateFinally(*args, **kwargs)


__pluginInfo__ = {
"name": PLUGIN_NAME,
"version": "0.0.1",
"description": "Example validation plugin for the fictitious XYZ taxonomy.",
"license": "Apache-2",
"author": authorLabel,
"copyright": copyrightLabel,
"DisclosureSystem.Types": disclosureSystemTypes,
"DisclosureSystem.ConfigURL": disclosureSystemConfigURL,
"ModelDocument.PullLoader": modelDocumentPullLoader,
"ModelXbrl.LoadComplete": modelXbrlLoadComplete,
"Validate.XBRL.Finally": validateXbrlFinally,
"Validate.XBRL.DTS.document": validateXbrlDtsDocument,
"Validate.Finally": validateFinally,
}
16 changes: 16 additions & 0 deletions arelle/examples/plugin/validate/XYZ/resources/config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<DisclosureSystems
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../config/disclosuresystems.xsd">
<!-- see arelle/config/disclosuresystems.xml for full comments -->
<DisclosureSystem
names="XYZ 2023|XYZ-2023|xyz-2023|XYZ|xyz"
description="Checks for example XYZ validation plugin for year 2023"
validationType="XYZ"
/>
<DisclosureSystem
names="XYZ 2022|XYZ-2022|xyz-2022"
description="Checks for example XYZ validation plugin for year 2022"
validationType="XYZ"
/>
</DisclosureSystems>
Empty file.

0 comments on commit 012c756

Please sign in to comment.