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
78 changes: 69 additions & 9 deletions .github/workflows/packages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ name: "📦 Workspace packages"

# Stage 2 of ROADMAP.md (outcome 2): loop_common/loop_interpolation live under
# packages/ as independent uv-workspace members so the interpolation code is
# usable outside the LoopStructural framework. This job installs and tests
# them on their own, separately from the root LoopStructural test suite in
# tester.yml. loopstructural-test waits on packages-test (needs:) so the root
# suite only runs once loop_common/loop_interpolation are confirmed working.
# usable outside the LoopStructural framework. Stage 4 (outcomes 5, 7) added
# loopstructural_visualisation (packages-test matrix, below) and map2loop
# (map2loop-test, its own job: map2loop hard-depends on GDAL bindings, which
# aren't pip-installable on Windows/macOS the way this matrix installs
# everything else, so it only runs on ubuntu with an apt-installed libgdal —
# matching map2loop's own upstream CI, which uses conda for the same reason).
# This job installs and tests packages on their own, separately from the root
# LoopStructural test suite in tester.yml. loopstructural-test waits on both
# jobs (needs:) so the root suite only runs once every workspace package is
# confirmed working.

on:
push:
Expand All @@ -27,12 +33,21 @@ permissions:

jobs:
packages-test:
name: ${{ matrix.package }} (python ${{ matrix.python-version }})
# matrix.package.name is the uv/PyPI package name (as declared in each
# package's pyproject.toml `[project] name`); matrix.package.dir is its
# directory under packages/. These differ for loopstructuralvisualisation:
# its published/import name has no separator, but its directory uses an
# underscore, and `uv --package` matches on the declared name (with only
# hyphen/underscore treated as equivalent, not a missing separator).
name: ${{ matrix.package.name }} (python ${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
package: [loop_common, loop_interpolation]
package:
- { name: loop_common, dir: loop_common }
- { name: loop_interpolation, dir: loop_interpolation }
- { name: loopstructuralvisualisation, dir: loopstructural_visualisation }
os: ${{ fromJSON(vars.BUILD_OS)}}
python-version: ${{ fromJSON(vars.PYTHON_VERSIONS)}}
steps:
Expand All @@ -48,14 +63,59 @@ jobs:

- name: Install package with test extras
run: |
uv sync --package ${{ matrix.package }} --extra tests --python ${{ matrix.python-version }}
uv sync --package ${{ matrix.package.name }} --extra tests --python ${{ matrix.python-version }}

- name: pytest
run: |
uv run pytest packages/${{ matrix.package }}/tests
uv run --package ${{ matrix.package.name }} pytest packages/${{ matrix.package.dir }}/tests

map2loop-test:
# Separate job (not part of the packages-test matrix above): map2loop
# imports osgeo/gdal at module load time, which has no pip-installable
# wheel on Windows/macOS (confirmed: building GDAL from the PyPI sdist
# fails without system libgdal headers). Scoped to ubuntu-latest only,
# installing libgdal via apt, mirroring how map2loop's own upstream CI
# special-cases GDAL per-OS (it uses conda-forge everywhere). Revisit
# widening the OS matrix if/when a reliable cross-platform GDAL install
# path is found.
name: map2loop (python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ${{ fromJSON(vars.PYTHON_VERSIONS) }}
steps:
- uses: actions/checkout@v4

- name: Install GDAL system libraries
run: |
sudo apt-get update
sudo apt-get install -y gdal-bin libgdal-dev
echo "CPLUS_INCLUDE_PATH=/usr/include/gdal" >> "$GITHUB_ENV"
echo "C_INCLUDE_PATH=/usr/include/gdal" >> "$GITHUB_ENV"

- name: Set up uv
uses: astral-sh/setup-uv@v3
with:
version: "latest"

- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}

- name: Install package with test extras
run: |
uv sync --package map2loop --extra tests --python ${{ matrix.python-version }}
GDAL_VERSION=$(gdal-config --version)
uv pip install setuptools
uv pip install "gdal==${GDAL_VERSION}" --no-build-isolation-package gdal

- name: pytest
run: |
uv run --package map2loop pytest packages/map2loop/tests

loopstructural-test:
name: LoopStructural (python ${{ matrix.python-version }})
needs: packages-test
needs: [packages-test, map2loop-test]
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
Expand Down
17 changes: 11 additions & 6 deletions .github/workflows/pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@ permissions:
contents: read

jobs:
# loop_common/loop_interpolation (ROADMAP.md Stage 2) are workspace-local
# deps of LoopStructural (see [tool.uv.sources] in pyproject.toml). They
# must land on PyPI before the LoopStructural sdist below is uploaded, or
# `pip install LoopStructural` breaks for anyone not using uv.
# loop_common/loop_interpolation/loopstructural_visualisation are
# workspace-local deps of LoopStructural (see [tool.uv.sources] in
# pyproject.toml) — loop_common/loop_interpolation are runtime deps,
# loopstructural_visualisation backs the `visualisation`/`docs` extras.
# They must land on PyPI before the LoopStructural sdist below is
# uploaded, or `pip install LoopStructural[...]` breaks for anyone not
# using uv. map2loop (ROADMAP.md Stage 4, outcome 7) has no such reverse
# dependency — it rides in the same matrix purely for its own independent
# PyPI publishing, not for a gating reason.
make_sdist_packages:
name: Make SDist (${{ matrix.package }})
runs-on: ubuntu-latest
strategy:
matrix:
package: [loop_common, loop_interpolation]
package: [loop_common, loop_interpolation, map2loop, loopstructural_visualisation]
steps:
- uses: actions/checkout@v4

Expand All @@ -36,7 +41,7 @@ jobs:
runs-on: "ubuntu-latest"
strategy:
matrix:
package: [loop_common, loop_interpolation]
package: [loop_common, loop_interpolation, map2loop, loopstructural_visualisation]
steps:
- uses: actions/download-artifact@v4
with:
Expand Down
4 changes: 3 additions & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
".": "1.7.3",
"packages/loop_common": "0.0.2",
"packages/loop_interpolation": "0.0.2"
"packages/loop_interpolation": "0.0.2",
"packages/map2loop": "3.3.1",
"packages/loopstructural_visualisation": "0.1.17"
}
84 changes: 82 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -451,8 +451,77 @@ just at release time.
- [x] **3c — Serialization API + fixtures.** Add read/write helpers and
golden tests that prove both 3a and 3b stay aligned with the current
`GeologicalModel` API.
- [ ] **Stage 4 — Bring in `loopresources` + `map2loop` (outcomes 5, 7).**
Workspace packages, now that the pattern is proven internally in Stage 2.
- [x] **Stage 4 — Bring in `map2loop` + `loopstructural-visualisation`
(outcomes 5, 7).** Workspace packages, now that the pattern is proven
internally in Stage 2. `loopresources` was already attempted on branch
`lachlan/add-loop-resources` (commit `db67e6c0`) but that branch was never
merged to `master` — a prior status-log note here incorrectly claimed it
was done; corrected 2026-08-05. `packages/loopresources` on disk currently
contains only stray `__pycache__`/`.egg-info` left over from checking that
branch out and back, with zero tracked source — landing it for real is
follow-up work, not done in this pass.
- `packages/map2loop`: ported from `Loop3D/map2loop` (`master`,
commit `078f8a6d`), src-layout, own test suite brought over (78 passed
locally where GDAL bindings aren't needed). Hard-depends on `osgeo`/GDAL
at import time, which has no pip-installable wheel on Windows/macOS
(confirmed: PyPI `gdal` sdist fails to build without system headers) —
upstream itself only tests via conda for this reason. Given
`.github/workflows/packages.yml` is uv/pip-based, `map2loop` gets its
own CI job (`map2loop-test`) scoped to `ubuntu-latest` with an
apt-installed `libgdal-dev`, rather than joining the cross-platform
`packages-test` matrix. Does not depend on `LoopStructural`/`loop_common`
(confirmed standalone) — no `[tool.uv.sources]` entry needed. `loop_common`
type-reuse audit: no swap made. Its `bounding_box` is a plain geographic
extent `dict`, architecturally unrelated to `loop_common.geometry
.BoundingBox` (an interpolation-mesh local/global-frame transform
object); its structural-measurement data stays in GeoDataFrame columns,
no custom point/orientation class to swap; its ad-hoc `print()` calls
sit alongside its own already-used `map2loop.logging` module, so routing
through `loop_common.logging` instead would mean adding `loop-common` as
a new hard dependency for a cosmetic swap — not made.
- `packages/loopstructural_visualisation`: ported from
`Loop3D/loopstructural-visualisation` (`main`, commit `df79dc5`),
moved to this repo's `src`-layout convention (import name
`loopstructuralvisualisation` unchanged). Added to `[tool.uv.sources]`
as `loopstructuralvisualisation = { workspace = true }` (it already backs
root `pyproject.toml`'s `visualisation`/`docs` extras) — this in turn
required adding `LoopStructural = { workspace = true }` too, since the
package depends on `LoopStructural` itself and uv treats the workspace
root as an implicit member once any member depends on it by name (same
fix already recorded, unmerged, on the `lachlan/add-loop-resources`
branch). Had **zero existing tests upstream**; added a minimal
import-smoke test (`tests/test_import.py`) rather than leaving the CI
matrix step with nothing to run — real behavioral coverage is still a
gap. `loop_common` audit: one real swap (a bare `print()` in the
trame-UI import guard now goes through `LoopStructural.utils.getLogger`);
two flagged-not-forced items — `_3d_viewer.py` keeps importing
`BoundingBox`/`ValuePoints`/`VectorPoints` via the deprecated
`LoopStructural.datatypes` shim rather than `.geometry` directly, since
`.geometry` doesn't exist in any published `LoopStructural` release yet
and the package's declared floor is `>=1.6.17` (cosmetic cost: an extra
deprecation-warning hop within this workspace, confirmed harmless);
`_2d_viewer.py`'s `np.zeros((2, 2))` 2D map-extent representation was
left alone rather than forced into `loop_common.geometry.BoundingBox`,
which is documented elsewhere in this file as 3D-only.
- Wired both into `.github/workflows/pypi.yml` (sdist build + PyPI
upload — `loopstructural_visualisation` for the same "must land before
the root sdist" reason `loop_common`/`loop_interpolation` are there;
`map2loop` rides along for its own independent publishing, no gating
relationship), `release-please-config.json` +
`.release-please-manifest.json` (new components `map2loop` and
`loopstructuralvisualisation`, the latter using release-please's
`extra-files` mechanism to bump `version.py`'s `__version__` since that
package uses `dynamic = ["version"]` rather than a static pyproject
version field), and `pyproject.toml`'s ruff `per-file-ignores` (D/ANN
grandfathered for both, matching the loop_common/loop_interpolation
precedent — `ruff check` on both surfaced ~272 default-ruleset findings,
mostly import-sort/pyupgrade noise typical of freshly-ported external
code; `linter.yml` runs with `--fix --exit-zero` and opens an auto-PR
rather than blocking, so this isn't a merge blocker, just a known
pile left for that auto-PR / a future dedicated pass, same as Stage 2's
equivalent note).
- Full `tests/unit` re-run after all of the above: 695 passed, 4 skipped,
zero regressions.
- [ ] **Stage 5 — Graph backend (outcome 4).** The `2.0` breaking change,
using the Stage 3 YAML schema as the serialization contract and the
`GeologicalModel` API as a compat facade.
Expand Down Expand Up @@ -750,3 +819,14 @@ just at release time.
baseline via `git stash`. This is a large improvement on the
192-failed/458-passed baseline noted above, since most of those failures
were exactly this `BoundingBox.global_origin` attribute gap.
- **2026-08-05:** Stage 4 done for `map2loop`/`loopstructural-visualisation`
(branch `stage4-map2loop-visualisation`); `loopresources` remains
unmerged (see Stage 4 bullet above for the correction to a prior,
inaccurate "done" claim about it). Full detail in the Stage 4 bullet
above: package ports, the GDAL/CI special-casing `map2loop` needed, the
`[tool.uv.sources]` wiring `loopstructural_visualisation` needed (and why
it also required adding a `LoopStructural = { workspace = true }` entry),
the `loop_common` type-reuse audit outcome for each (one real swap in
`loopstructuralvisualisation`, none warranted in `map2loop`), and the
release-please/PyPI/ruff wiring. `tests/unit`: 695 passed/4 skipped, zero
regressions.
35 changes: 35 additions & 0 deletions packages/loopstructural_visualisation/tests/test_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Import smoke test.

Upstream loopstructural-visualisation ships with no test suite at all; this
is a minimal placeholder so the package has real CI coverage (packages.yml
runs `pytest packages/<package>/tests`, which needs a tests dir to exist)
rather than skipping it entirely. Extend with real behavioral tests as a
follow-up.
"""

import loopstructuralvisualisation


def test_import():
assert loopstructuralvisualisation is not None


def test_public_api_importable():
from loopstructuralvisualisation import (
Loop2DView,
Loop3DView,
RotationAnglePlotter,
StratigraphicColumnView,
)

assert Loop2DView is not None
assert Loop3DView is not None
assert RotationAnglePlotter is not None
assert StratigraphicColumnView is not None


def test_api_module_importable():
from loopstructuralvisualisation.api import plot_block_model, plot_surface

assert callable(plot_block_model)
assert callable(plot_surface)
7 changes: 5 additions & 2 deletions packages/map2loop/tests/project/test_plot_hamersley.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,12 @@ def test_project_execution():
def test_timeout_handling():
# Mock `openURL` in `owslib.util` to raise a ReadTimeout directly
with patch("owslib.util.openURL"):
# Run `test_project_execution` and check if the skip occurs
# Run `test_project_execution` and check if the skip occurs. Scoped to
# this file so the nested run collects only this test, rather than
# falling back to the root pyproject.toml's `testpaths = ["tests"]`
# and colliding with the top-level tests/ package of the same name.
result = pytest.main(
["-q", "--tb=short", "--disable-warnings", "-k", "test_project_execution"]
["-q", "--tb=short", "--disable-warnings", "-k", "test_project_execution", __file__]
)
assert (
result.value == pytest.ExitCode.OK
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ allow-dict-calls-with-keyword-arguments = true
# policy in a later stage.
"packages/loop_common/src/**/*.py" = ["D", "ANN"]
"packages/loop_interpolation/src/**/*.py" = ["D", "ANN"]
"packages/map2loop/src/**/*.py" = ["D", "ANN"]
"packages/loopstructural_visualisation/src/**/*.py" = ["D", "ANN"]
"LoopStructural/__init__.py" = ["D", "ANN"]
"LoopStructural/datasets/__init__.py" = ["D", "ANN"]
"LoopStructural/datasets/_base.py" = ["D", "ANN"]
Expand Down Expand Up @@ -313,3 +315,5 @@ allow-dict-calls-with-keyword-arguments = true
"setup.py" = ["D", "ANN"]
"packages/loop_common/tests/*" = ["D", "ANN"]
"packages/loop_interpolation/tests/*" = ["D", "ANN"]
"packages/map2loop/tests/*" = ["D", "ANN"]
"packages/loopstructural_visualisation/tests/*" = ["D", "ANN"]
8 changes: 8 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
"packages/loop_interpolation": {
"release-type": "python",
"component": "loop-interpolation"
},
"packages/map2loop": {
"release-type": "python",
"component": "map2loop"
},
"packages/loopstructural_visualisation": {
"release-type": "python",
"component": "loopstructuralvisualisation"
}
}
}
Loading
Loading