Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions tests/test_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Every public module imports on its own, in a fresh interpreter.

A module that only imports when something else was imported first is a cycle
waiting for a caller: `import xbrlkit.periods` failed in 0.18.1 because
`xbrlkit.parse` loaded Arelle and `to_model` on import, and `to_model` imports
`periods`. Each module is imported in its own subprocess, since one process
would mask the order dependence the test exists to find.
"""

from __future__ import annotations

import pkgutil
import subprocess
import sys

import pytest

import xbrlkit


def _public_modules() -> list[str]:
names = [xbrlkit.__name__]
for info in pkgutil.walk_packages(xbrlkit.__path__, prefix=f"{xbrlkit.__name__}."):
if any(part.startswith("_") for part in info.name.split(".")):
continue
names.append(info.name)
return sorted(names)


@pytest.mark.parametrize("module", _public_modules())
def test_a_public_module_imports_on_its_own(module: str) -> None:
result = subprocess.run(
[sys.executable, "-c", f"import {module}"],
capture_output=True,
text=True,
timeout=120,
)
assert result.returncode == 0, result.stderr.strip().splitlines()[-1:]


def test_periods_does_not_load_arelle() -> None:
"""A caller that wants a period id should not pay for Arelle."""
result = subprocess.run(
[
sys.executable,
"-c",
"import sys, xbrlkit.periods; print('arelle' in sys.modules)",
],
capture_output=True,
text=True,
timeout=120,
)
assert result.stdout.strip() == "False", result.stderr


@pytest.mark.parametrize(
"module", ["xbrlkit.serialize.tavi", "xbrlkit.deserialize.tavi"]
)
def test_tavi_does_not_load_rdflib(module: str) -> None:
"""TAVI is JSON; reading or writing it should not pay for an RDF stack."""
result = subprocess.run(
[sys.executable, "-c", f"import sys, {module}; print('rdflib' in sys.modules)"],
capture_output=True,
text=True,
timeout=120,
)
assert result.stdout.strip() == "False", result.stderr
49 changes: 41 additions & 8 deletions xbrlkit/deserialize/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,47 @@

from __future__ import annotations

from .clawdog import (
ClawDogError,
from_clawdog,
from_clawdog_json,
from_clawdog_report,
)
from .holon import HolonError, from_holon, from_holon_json, from_holon_report
from .tavi import TaviError, from_tavi, from_tavi_json, from_tavi_report
import importlib
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from .clawdog import (
ClawDogError,
from_clawdog,
from_clawdog_json,
from_clawdog_report,
)
from .holon import HolonError, from_holon, from_holon_json, from_holon_report
from .tavi import TaviError, from_tavi, from_tavi_json, from_tavi_report

# Loaded on first use (PEP 562), not on import. Importing any submodule runs
# this file first, so eager imports here made
# `xbrlkit.deserialize.tavi` load rdflib through the holon importer.
_LAZY: dict[str, tuple[str, str]] = {
"ClawDogError": (".clawdog", "ClawDogError"),
"from_clawdog": (".clawdog", "from_clawdog"),
"from_clawdog_json": (".clawdog", "from_clawdog_json"),
"from_clawdog_report": (".clawdog", "from_clawdog_report"),
"HolonError": (".holon", "HolonError"),
"from_holon": (".holon", "from_holon"),
"from_holon_json": (".holon", "from_holon_json"),
"from_holon_report": (".holon", "from_holon_report"),
"TaviError": (".tavi", "TaviError"),
"from_tavi": (".tavi", "from_tavi"),
"from_tavi_json": (".tavi", "from_tavi_json"),
"from_tavi_report": (".tavi", "from_tavi_report"),
}


def __getattr__(name: str) -> Any:
entry = _LAZY.get(name)
if entry is None:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
module, attribute = entry
value = getattr(importlib.import_module(module, __name__), attribute)
globals()[name] = value
return value


__all__ = (
"ClawDogError",
Expand Down
49 changes: 39 additions & 10 deletions xbrlkit/parse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,45 @@

from __future__ import annotations

from xbrlkit.parse.arelle_load import (
DtsResolutionError,
LoadState,
close,
configure_webcache,
load_model,
load_state,
register_sec_transforms,
)
from xbrlkit.parse.to_model import to_xbrl_model
import importlib
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from xbrlkit.parse.arelle_load import (
DtsResolutionError,
LoadState,
close,
configure_webcache,
load_model,
load_state,
register_sec_transforms,
)
from xbrlkit.parse.to_model import to_xbrl_model

# Loaded on first use (PEP 562), not on import. `xbrlkit.parse.ids` is imported
# by `xbrlkit.periods`, and importing any submodule runs this file first: loading
# Arelle and `to_model` here made `import xbrlkit.periods` circular (to_model
# imports periods) and pulled Arelle into every caller that wanted a period id.
_LAZY: dict[str, str] = {
"DtsResolutionError": "xbrlkit.parse.arelle_load",
"LoadState": "xbrlkit.parse.arelle_load",
"close": "xbrlkit.parse.arelle_load",
"configure_webcache": "xbrlkit.parse.arelle_load",
"load_model": "xbrlkit.parse.arelle_load",
"load_state": "xbrlkit.parse.arelle_load",
"register_sec_transforms": "xbrlkit.parse.arelle_load",
"to_xbrl_model": "xbrlkit.parse.to_model",
}


def __getattr__(name: str) -> Any:
module = _LAZY.get(name)
if module is None:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
value = getattr(importlib.import_module(module), name)
globals()[name] = value
return value


__all__ = [
"DtsResolutionError",
Expand Down
60 changes: 49 additions & 11 deletions xbrlkit/serialize/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,55 @@

from __future__ import annotations

from .classify import classify_network, root_qname
from .clawdog import GapReport as ClawDogGapReport
from .clawdog import to_clawdog, to_clawdog_report
from .graph import build_holon_graph
from .holon import to_holon
from .lpg import GraphTables, build_lbug, to_graph_tables, write_parquet
from .oim import to_oim, to_oim_document
from .tavi import GapReport as TaviGapReport
from .tavi import to_tavi, to_tavi_report

GapReport = TaviGapReport
import importlib
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from .classify import classify_network, root_qname
from .clawdog import GapReport as ClawDogGapReport
from .clawdog import to_clawdog, to_clawdog_report
from .graph import build_holon_graph
from .holon import to_holon
from .lpg import GraphTables, build_lbug, to_graph_tables, write_parquet
from .oim import to_oim, to_oim_document
from .tavi import GapReport as TaviGapReport
from .tavi import to_tavi, to_tavi_report

GapReport = TaviGapReport

# Loaded on first use (PEP 562), not on import. Importing any submodule runs
# this file first, so eager imports here made
# `xbrlkit.serialize.tavi` load rdflib through the holon and graph projections.
_LAZY: dict[str, tuple[str, str]] = {
"classify_network": (".classify", "classify_network"),
"root_qname": (".classify", "root_qname"),
"ClawDogGapReport": (".clawdog", "GapReport"),
"to_clawdog": (".clawdog", "to_clawdog"),
"to_clawdog_report": (".clawdog", "to_clawdog_report"),
"build_holon_graph": (".graph", "build_holon_graph"),
"to_holon": (".holon", "to_holon"),
"GraphTables": (".lpg", "GraphTables"),
"build_lbug": (".lpg", "build_lbug"),
"to_graph_tables": (".lpg", "to_graph_tables"),
"write_parquet": (".lpg", "write_parquet"),
"to_oim": (".oim", "to_oim"),
"to_oim_document": (".oim", "to_oim_document"),
"GapReport": (".tavi", "GapReport"),
"TaviGapReport": (".tavi", "GapReport"),
"to_tavi": (".tavi", "to_tavi"),
"to_tavi_report": (".tavi", "to_tavi_report"),
}


def __getattr__(name: str) -> Any:
entry = _LAZY.get(name)
if entry is None:
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
module, attribute = entry
value = getattr(importlib.import_module(module, __name__), attribute)
globals()[name] = value
return value


__all__ = (
"GapReport",
Expand Down
Loading