diff --git a/src/rootfilespec/bootstrap/RAnchor.py b/src/rootfilespec/bootstrap/RAnchor.py index 38953a2..20bb174 100644 --- a/src/rootfilespec/bootstrap/RAnchor.py +++ b/src/rootfilespec/bootstrap/RAnchor.py @@ -1,11 +1,7 @@ from collections.abc import Callable -from typing import Annotated +from typing import TYPE_CHECKING, Annotated from rootfilespec.bootstrap.streamedobject import StreamedObject -from rootfilespec.rntuple.envelope import REnvelopeLocator -from rootfilespec.rntuple.footer import FooterEnvelope -from rootfilespec.rntuple.header import HeaderEnvelope -from rootfilespec.rntuple.RLocator import LargeLocator from rootfilespec.serializable import ( Locator, ReadBuffer, @@ -14,6 +10,14 @@ ) from rootfilespec.structutil import Fmt +# rootfilespec.rntuple imports rootfilespec.bootstrap (for compression), whose +# __init__ imports this module, so the rntuple names are imported where they are +# used rather than here (#119) +if TYPE_CHECKING: + from rootfilespec.rntuple.envelope import REnvelopeLocator + from rootfilespec.rntuple.footer import FooterEnvelope + from rootfilespec.rntuple.header import HeaderEnvelope + @serializable class ROOT3a3aRNTuple(StreamedObject): @@ -30,8 +34,12 @@ class ROOT3a3aRNTuple(StreamedObject): fMaxKeySize: Annotated[int, Fmt(">Q")] @property - def header_locator(self) -> REnvelopeLocator[HeaderEnvelope]: + def header_locator(self) -> "REnvelopeLocator[HeaderEnvelope]": """Get a locator for the RNTuple Header Envelope.""" + from rootfilespec.rntuple.envelope import REnvelopeLocator + from rootfilespec.rntuple.header import HeaderEnvelope + from rootfilespec.rntuple.RLocator import LargeLocator + return REnvelopeLocator( self.fLenHeader, LargeLocator(self.fNBytesHeader, self.fSeekHeader), @@ -39,8 +47,12 @@ def header_locator(self) -> REnvelopeLocator[HeaderEnvelope]: ) @property - def footer_locator(self) -> REnvelopeLocator[FooterEnvelope]: + def footer_locator(self) -> "REnvelopeLocator[FooterEnvelope]": """Get a locator for the RNTuple Footer Envelope.""" + from rootfilespec.rntuple.envelope import REnvelopeLocator + from rootfilespec.rntuple.footer import FooterEnvelope + from rootfilespec.rntuple.RLocator import LargeLocator + return REnvelopeLocator( self.fLenFooter, LargeLocator(self.fNBytesFooter, self.fSeekFooter), @@ -49,7 +61,7 @@ def footer_locator(self) -> REnvelopeLocator[FooterEnvelope]: def get_header( self, fetch_data: Callable[[Locator[ROOTSerializable]], ReadBuffer] - ) -> HeaderEnvelope: + ) -> "HeaderEnvelope": """Reads the RNTuple Header Envelope from the given buffer.""" loc = self.header_locator buffer = fetch_data(loc) @@ -57,7 +69,7 @@ def get_header( def get_footer( self, fetch_data: Callable[[Locator[ROOTSerializable]], ReadBuffer] - ) -> FooterEnvelope: + ) -> "FooterEnvelope": """Reads the RNTuple Footer Envelope from the given buffer.""" loc = self.footer_locator buffer = fetch_data(loc) diff --git a/tests/test_imports.py b/tests/test_imports.py new file mode 100644 index 0000000..2f75d8d --- /dev/null +++ b/tests/test_imports.py @@ -0,0 +1,46 @@ +import pkgutil +import subprocess +import sys + +import pytest + +import rootfilespec + +MODULES = sorted( + info.name + for info in pkgutil.walk_packages(rootfilespec.__path__, "rootfilespec.") + if ".generated" not in info.name +) + +# The same shape as #119 outside RNTuple: rootfilespec.container imports +# rootfilespec.bootstrap, whose array module imports rootfilespec.container +KNOWN_CYCLES = {"rootfilespec.container"} + + +@pytest.mark.parametrize( + "module", + [ + pytest.param( + m, + marks=pytest.mark.xfail( + strict=True, reason="circular import through rootfilespec.bootstrap" + ), + ) + if m in KNOWN_CYCLES + else m + for m in MODULES + ], +) +def test_import_alone(module: str): + """Issue #119: every module imports in a fresh interpreter, on its own + + Importing rootfilespec.rntuple.* before rootfilespec.bootstrap used to fail + with a circular import. + """ + result = subprocess.run( + [sys.executable, "-c", f"import {module}"], + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr