Reorganise

This commit is contained in:
Edward Betts 2026-02-27 14:27:38 +00:00
parent 2a2a42fe5d
commit d3e6d7ac42
12 changed files with 206 additions and 178 deletions

View file

@ -6,20 +6,8 @@ import pytest
import responses as responses_lib
from click.testing import CliRunner
import sys
sys.path.insert(0, str(Path(__file__).parent.parent))
# Import the script as a module. The filename has hyphens so we use importlib.
import importlib.machinery
import importlib.util
_loader = importlib.machinery.SourceFileLoader(
"osm_pt_geojson", str(Path(__file__).parent.parent / "osm-pt-geojson")
)
_spec = importlib.util.spec_from_loader("osm_pt_geojson", _loader)
assert _spec
osm = importlib.util.module_from_spec(_spec)
_loader.exec_module(osm)
from osm_geojson.pt import core
from osm_geojson.pt.cli import cli, output_geojson
FIXTURES = Path(__file__).parent / "fixtures"
FULL_URL = "https://www.openstreetmap.org/api/0.6/relation/15083963/full.json"
@ -35,7 +23,7 @@ def full_data() -> dict:
@pytest.fixture()
def parsed(full_data: dict) -> tuple:
"""Return parsed elements (nodes, ways, stop_ids, way_ids, tags) for relation 15083963."""
return osm.parse_elements(full_data, RELATION_ID)
return core.parse_elements(full_data, RELATION_ID)
# ---------------------------------------------------------------------------
@ -51,8 +39,8 @@ def test_parse_elements_stop_count(parsed: tuple) -> None:
def test_parse_elements_first_and_last_stop(parsed: tuple) -> None:
"""The first and last stops match the route terminus names."""
nodes, ways, stop_ids, way_ids, tags = parsed
assert osm.node_name(nodes[stop_ids[0]]) == "Arnavutköy Hastane"
assert osm.node_name(nodes[stop_ids[-1]]) == "Gayrettepe"
assert core.node_name(nodes[stop_ids[0]]) == "Arnavutköy Hastane"
assert core.node_name(nodes[stop_ids[-1]]) == "Gayrettepe"
def test_parse_elements_way_count(parsed: tuple) -> None:
@ -70,9 +58,8 @@ def test_parse_elements_tags(parsed: tuple) -> None:
def test_parse_elements_unknown_relation(full_data: dict) -> None:
"""Requesting a relation ID not present in the response exits with an error."""
runner = CliRunner()
with pytest.raises(SystemExit):
osm.parse_elements(full_data, 9999999)
core.parse_elements(full_data, 9999999)
# ---------------------------------------------------------------------------
@ -82,7 +69,7 @@ def test_parse_elements_unknown_relation(full_data: dict) -> None:
def test_build_route_coords_returns_coords(parsed: tuple) -> None:
"""Chained coordinates are non-empty and fall within the Istanbul bounding box."""
nodes, ways, stop_ids, way_ids, tags = parsed
coords = osm.build_route_coords(way_ids, ways, nodes)
coords = core.build_route_coords(way_ids, ways, nodes)
assert len(coords) > 0
for coord in coords:
assert len(coord) == 2
@ -93,7 +80,7 @@ def test_build_route_coords_returns_coords(parsed: tuple) -> None:
def test_build_route_coords_empty_ways() -> None:
"""An empty way list returns an empty coordinate list."""
assert osm.build_route_coords([], {}, {}) == []
assert core.build_route_coords([], {}, {}) == []
# ---------------------------------------------------------------------------
@ -103,13 +90,13 @@ def test_build_route_coords_empty_ways() -> None:
def test_nearest_coord_index_exact() -> None:
"""Returns the index of an exact coordinate match."""
coords = [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]
assert osm.nearest_coord_index(3.0, 4.0, coords) == 1
assert core.nearest_coord_index(3.0, 4.0, coords) == 1
def test_nearest_coord_index_approximate() -> None:
"""Returns the index of the closest coordinate when there is no exact match."""
coords = [[0.0, 0.0], [10.0, 0.0], [20.0, 0.0]]
assert osm.nearest_coord_index(9.0, 0.0, coords) == 1
assert core.nearest_coord_index(9.0, 0.0, coords) == 1
# ---------------------------------------------------------------------------
@ -119,25 +106,25 @@ def test_nearest_coord_index_approximate() -> None:
def test_node_name_uses_name_tag() -> None:
"""Prefers the name tag when present."""
node = {"id": 1, "lat": 0.0, "lon": 0.0, "tags": {"name": "Central", "ref": "C1"}}
assert osm.node_name(node) == "Central"
assert core.node_name(node) == "Central"
def test_node_name_falls_back_to_ref() -> None:
"""Falls back to the ref tag when there is no name tag."""
node = {"id": 1, "lat": 0.0, "lon": 0.0, "tags": {"ref": "C1"}}
assert osm.node_name(node) == "C1"
assert core.node_name(node) == "C1"
def test_node_name_falls_back_to_id() -> None:
"""Falls back to the node ID when tags are present but empty."""
node = {"id": 42, "lat": 0.0, "lon": 0.0, "tags": {}}
assert osm.node_name(node) == "42"
assert core.node_name(node) == "42"
def test_node_name_no_tags() -> None:
"""Falls back to the node ID when the tags key is absent."""
node = {"id": 99, "lat": 0.0, "lon": 0.0}
assert osm.node_name(node) == "99"
assert core.node_name(node) == "99"
# ---------------------------------------------------------------------------
@ -147,8 +134,8 @@ def test_node_name_no_tags() -> None:
def test_make_geojson_full(parsed: tuple) -> None:
"""Full output contains one LineString and one Point per stop."""
nodes, ways, stop_ids, way_ids, tags = parsed
coords = osm.build_route_coords(way_ids, ways, nodes)
geojson = osm.make_geojson(coords, stop_ids, nodes, tags)
coords = core.build_route_coords(way_ids, ways, nodes)
geojson = core.make_geojson(coords, stop_ids, nodes, tags)
assert geojson["type"] == "FeatureCollection"
features = geojson["features"]
@ -162,8 +149,8 @@ def test_make_geojson_full(parsed: tuple) -> None:
def test_make_geojson_no_stops(parsed: tuple) -> None:
"""With no_stops=True, only the LineString feature is included."""
nodes, ways, stop_ids, way_ids, tags = parsed
coords = osm.build_route_coords(way_ids, ways, nodes)
geojson = osm.make_geojson(coords, stop_ids, nodes, tags, no_stops=True)
coords = core.build_route_coords(way_ids, ways, nodes)
geojson = core.make_geojson(coords, stop_ids, nodes, tags, no_stops=True)
features = geojson["features"]
assert all(f["geometry"]["type"] == "LineString" for f in features)
@ -172,11 +159,11 @@ def test_make_geojson_no_stops(parsed: tuple) -> None:
def test_make_geojson_slice(parsed: tuple) -> None:
"""Slicing by coord index produces a shorter LineString with the correct length."""
nodes, ways, stop_ids, way_ids, tags = parsed
coords = osm.build_route_coords(way_ids, ways, nodes)
full = osm.make_geojson(coords, stop_ids, nodes, tags)
coords = core.build_route_coords(way_ids, ways, nodes)
full = core.make_geojson(coords, stop_ids, nodes, tags)
full_line_len = len(full["features"][0]["geometry"]["coordinates"])
sliced = osm.make_geojson(coords, stop_ids, nodes, tags, idx_from=10, idx_to=50)
sliced = core.make_geojson(coords, stop_ids, nodes, tags, idx_from=10, idx_to=50)
sliced_line_len = len(sliced["features"][0]["geometry"]["coordinates"])
assert sliced_line_len == 41 # 50 - 10 + 1
@ -186,8 +173,8 @@ def test_make_geojson_slice(parsed: tuple) -> None:
def test_make_geojson_linestring_properties(parsed: tuple) -> None:
"""The LineString feature carries route properties from the OSM relation tags."""
nodes, ways, stop_ids, way_ids, tags = parsed
coords = osm.build_route_coords(way_ids, ways, nodes)
geojson = osm.make_geojson(coords, stop_ids, nodes, tags)
coords = core.build_route_coords(way_ids, ways, nodes)
geojson = core.make_geojson(coords, stop_ids, nodes, tags)
props = geojson["features"][0]["properties"]
assert props["ref"] == "M11"
@ -203,7 +190,7 @@ def test_cli_list_stations(full_data: dict) -> None:
"""list-stations prints the route name and all stop names."""
responses_lib.add(responses_lib.GET, FULL_URL, json=full_data)
runner = CliRunner()
result = runner.invoke(osm.cli, ["list-stations", str(RELATION_ID)])
result = runner.invoke(cli, ["list-stations", str(RELATION_ID)])
assert result.exit_code == 0
assert "M11" in result.output
assert "Arnavutköy Hastane" in result.output
@ -215,7 +202,7 @@ def test_cli_list_stations_http_error() -> None:
"""list-stations exits with code 1 on an HTTP error response."""
responses_lib.add(responses_lib.GET, FULL_URL, status=503)
runner = CliRunner()
result = runner.invoke(osm.cli, ["list-stations", str(RELATION_ID)])
result = runner.invoke(cli, ["list-stations", str(RELATION_ID)])
assert result.exit_code == 1
@ -228,7 +215,7 @@ def test_cli_full_route_geojson(full_data: dict) -> None:
"""full-route outputs a valid GeoJSON FeatureCollection to stdout."""
responses_lib.add(responses_lib.GET, FULL_URL, json=full_data)
runner = CliRunner()
result = runner.invoke(osm.cli, ["full-route", str(RELATION_ID)])
result = runner.invoke(cli, ["full-route", str(RELATION_ID)])
assert result.exit_code == 0
geojson = json.loads(result.output)
assert geojson["type"] == "FeatureCollection"
@ -239,7 +226,7 @@ def test_cli_full_route_no_stops(full_data: dict) -> None:
"""full-route --no-stops omits Point features from the output."""
responses_lib.add(responses_lib.GET, FULL_URL, json=full_data)
runner = CliRunner()
result = runner.invoke(osm.cli, ["full-route", str(RELATION_ID), "--no-stops"])
result = runner.invoke(cli, ["full-route", str(RELATION_ID), "--no-stops"])
assert result.exit_code == 0
geojson = json.loads(result.output)
types = [f["geometry"]["type"] for f in geojson["features"]]
@ -252,7 +239,7 @@ def test_cli_full_route_output_file(full_data: dict, tmp_path) -> None:
responses_lib.add(responses_lib.GET, FULL_URL, json=full_data)
out = tmp_path / "route.geojson"
runner = CliRunner()
result = runner.invoke(osm.cli, ["full-route", str(RELATION_ID), "-o", str(out)])
result = runner.invoke(cli, ["full-route", str(RELATION_ID), "-o", str(out)])
assert result.exit_code == 0
assert out.exists()
geojson = json.loads(out.read_text())
@ -269,7 +256,7 @@ def test_cli_route_between(full_data: dict) -> None:
responses_lib.add(responses_lib.GET, FULL_URL, json=full_data)
runner = CliRunner()
result = runner.invoke(
osm.cli,
cli,
["route-between", str(RELATION_ID), "Arnavutköy Hastane", "Gayrettepe"],
)
assert result.exit_code == 0
@ -289,7 +276,7 @@ def test_cli_route_between_unknown_station(full_data: dict) -> None:
responses_lib.add(responses_lib.GET, FULL_URL, json=full_data)
runner = CliRunner()
result = runner.invoke(
osm.cli,
cli,
["route-between", str(RELATION_ID), "Nonexistent", "Gayrettepe"],
)
assert result.exit_code == 1
@ -301,7 +288,7 @@ def test_cli_route_between_stops_subset(full_data: dict) -> None:
responses_lib.add(responses_lib.GET, FULL_URL, json=full_data)
runner = CliRunner()
result = runner.invoke(
osm.cli,
cli,
["route-between", str(RELATION_ID), "İstanbul Havalimanı", "Hasdal"],
)
assert result.exit_code == 0