Skip to content

Commit dccced0

Browse files
committed
Merge remote-tracking branch 'upstream/tests' into feat/osm-filters
# Conflicts: # tests/test_osmnx.py
2 parents fc1e02b + 795cf55 commit dccced0

26 files changed

Lines changed: 2852 additions & 1267 deletions

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# This workflow runs the repo's CI tests.
1+
# This workflow runs the repo's (local/offline) CI tests.
22
name: CI
33

44
on:
@@ -7,7 +7,7 @@ on:
77
pull_request:
88
branches: [main]
99
schedule:
10-
- cron: 5 4 * * 1 # every monday at 04:05 UTC
10+
- cron: 30 4 * * 1 # every monday at 04:30 UTC
1111
workflow_dispatch:
1212

1313
concurrency:
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# This workflow runs live online public web API compatibility tests.
2+
name: Test online public web APIs
3+
4+
on:
5+
schedule:
6+
- cron: 5 4 * * 1 # every monday at 04:05 UTC
7+
workflow_dispatch:
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
permissions:
14+
contents: read
15+
16+
jobs:
17+
test_online_apis:
18+
if: ${{ github.repository == 'gboeing/osmnx' }}
19+
name: Public web APIs
20+
runs-on: ubuntu-latest
21+
timeout-minutes: 30
22+
23+
defaults:
24+
run:
25+
shell: bash -elo pipefail {0}
26+
27+
steps:
28+
- name: Checkout repo
29+
uses: actions/checkout@v6
30+
31+
- name: Install uv
32+
uses: astral-sh/setup-uv@v7
33+
with:
34+
cache-dependency-glob: pyproject.toml
35+
enable-cache: true
36+
python-version: '3.14'
37+
38+
- name: Install OSMnx
39+
run: |
40+
uv python pin 3.14
41+
uv sync --all-extras --group test
42+
43+
- name: Test public web APIs
44+
run: uv run pytest -m online --numprocesses=3
45+
env:
46+
OSMNX_RUN_ONLINE_TESTS: '1'

docs/source/further-reading.rst

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ This is the official reference paper and citation for the OSMnx package.
99

1010
----
1111

12+
Boeing, G. (2026). `Urban Science Beyond Samples: Up-to-Date Street Network Models and Indicators for Every Urban Area in the World`_. *Environment and Planning B*, published online ahead of print. doi:10.1177/23998083261446991
13+
14+
This study uses OSMnx to model and analyze the street networks of every urban area in the world: over 180 million OpenStreetMap street network nodes and over 360 million edges across 10,351 urban areas in 189 countries.
15+
16+
.. _Urban Science Beyond Samples\: Up-to-Date Street Network Models and Indicators for Every Urban Area in the World: https://geoffboeing.com/publications/street-network-models-indicators-world/
17+
18+
----
19+
1220
Boeing, G. (2025). `Topological Graph Simplification Solutions to the Street Intersection Miscount Problem`_. *Transactions in GIS* 29 (3), e70037. doi:10.1111/tgis.70037
1321

1422
This paper describes and validates the algorithms implemented in OSMnx's :code:`simplification` module and explains why graph simplification is necessary to accurately measure intersection density, street segment length, node degree, etc.
@@ -17,11 +25,12 @@ This paper describes and validates the algorithms implemented in OSMnx's :code:`
1725

1826
----
1927

20-
Boeing, G. (2021). `Street Network Models and Indicators for Every Urban Area in the World`_. *Geographical Analysis* 54 (3), 519-535. doi:10.1111/gean.12281
28+
Boeing, G. (2020). `Planarity and Street Network Representation in Urban Form Analysis`_. *Environment and Planning B* 47 (5), 855-869. doi:10.1177/2399808318802941
29+
30+
This paper demonstrates the need for nonplanar graphs when modeling urban street networks, which was one of the original motivations for developing OSMnx.
2131

22-
This study uses OSMnx to model and analyze the street networks of every urban area in the world: over 160 million OpenStreetMap street network nodes and over 320 million edges across 8,914 urban areas in 178 countries.
32+
.. _Planarity and Street Network Representation in Urban Form Analysis: https://geoffboeing.com/publications/planarity-street-network-representation/
2333

24-
.. _Street Network Models and Indicators for Every Urban Area in the World: https://geoffboeing.com/publications/street-network-models-indicators-world/
2534

2635
----
2736

@@ -30,11 +39,3 @@ Boeing, G. (2020). `The Right Tools for the Job: The Case for Spatial Science To
3039
This paper was presented as the 8th annual Transactions in GIS plenary address at the American Association of Geographers annual meeting in Washington, DC. It describes the early development of OSMnx and reviews its use in scientific research over the previous few years.
3140

3241
.. _The Right Tools for the Job\: The Case for Spatial Science Tool-Building: https://geoffboeing.com/publications/right-tools-for-job/
33-
34-
----
35-
36-
Boeing, G. (2020). `Planarity and Street Network Representation in Urban Form Analysis`_. *Environment and Planning B: Urban Analytics and City Science* 47 (5), 855-869. doi:10.1177/2399808318802941
37-
38-
This paper demonstrates the need for nonplanar graphs when modeling urban street networks, which was one of the original motivations for developing OSMnx.
39-
40-
.. _Planarity and Street Network Representation in Urban Form Analysis: https://geoffboeing.com/publications/planarity-street-network-representation/

osmnx/_overpass.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -372,16 +372,16 @@ def _overpass_request(data: OrderedDict[str, Any]) -> dict[str, Any]:
372372
response_json
373373
The Overpass API's response.
374374
"""
375-
# resolve url to same IP even if there is server round-robin redirecting
376-
_http._config_dns(settings.overpass_url)
377-
378375
# prepare the Overpass API URL and see if request already exists in cache
379376
url = settings.overpass_url.rstrip("/") + "/interpreter"
380377
prepared_url = str(requests.Request("GET", url, params=data).prepare().url)
381378
cached_response_json = _http._retrieve_from_cache(prepared_url)
382379
if isinstance(cached_response_json, dict):
383380
return cached_response_json
384381

382+
# resolve url to same IP even if there is server round-robin redirecting
383+
_http._config_dns(settings.overpass_url)
384+
385385
# pause then request this URL
386386
pause = _get_overpass_pause(settings.overpass_url)
387387
hostname = _http._hostname_from_url(url)

osmnx/stats.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ def circuity_avg(Gu: nx.MultiGraph) -> float | None:
264264
sl_dists_total = sl_dists[~np.isnan(sl_dists)].sum()
265265
try:
266266
return float(edge_length_total(Gu) / sl_dists_total)
267-
except ZeroDivisionError:
267+
except ZeroDivisionError: # pragma: no cover
268268
return None
269269

270270

osmnx/utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,15 +144,15 @@ def log(
144144
message = f"{ts()} {message}"
145145
message = ud.normalize("NFKD", message).encode("ascii", errors="replace").decode()
146146

147-
try:
147+
try: # pragma: no cover
148148
# print explicitly to terminal in case Jupyter has captured stdout
149149
if getattr(sys.stdout, "_original_stdstream_copy", None) is not None:
150150
# redirect the Jupyter-captured pipe back to original
151151
os.dup2(sys.stdout._original_stdstream_copy, sys.__stdout__.fileno()) # type: ignore[union-attr]
152152
sys.stdout._original_stdstream_copy = None # type: ignore[union-attr]
153153
with redirect_stdout(sys.__stdout__):
154154
print(message, file=sys.__stdout__, flush=True)
155-
except OSError:
155+
except OSError: # pragma: no cover
156156
# handle pytest on Windows raising OSError from sys.__stdout__
157157
print(message, flush=True) # noqa: T201
158158

pyproject.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[build-system]
22
build-backend = "uv_build"
3-
requires = ["uv_build==0.10.*"]
3+
requires = ["uv_build==0.11.*"]
44

55
[project]
66
authors = [{ name = "Geoff Boeing", email = "boeing@usc.edu" }]
@@ -62,6 +62,7 @@ Documentation = "https://osmnx.readthedocs.io"
6262

6363
[tool.coverage.report]
6464
exclude_also = ["@overload", "if TYPE_CHECKING:"]
65+
fail_under = 99
6566

6667
[tool.mypy]
6768
cache_dir = "~/.cache/prek/cache/mypy"
@@ -80,6 +81,10 @@ addopts = ["-ra", "--verbose", "--maxfail=1", "--numprocesses=3", "--dist=loadgr
8081
cache_dir = "~/.cache/pytest"
8182
filterwarnings = ["error", "ignore::UserWarning"]
8283
log_level = "INFO"
84+
markers = [
85+
"offline: deterministic tests that do not make online public web API calls",
86+
"online: tests that make live online public web API calls",
87+
]
8388
minversion = 9
8489
strict = true
8590
testpaths = ["tests"]
@@ -109,7 +114,7 @@ convention = "numpy"
109114
max-args = 8
110115

111116
[tool.uv]
112-
required-version = "==0.10.*" # match version above and in Dockerfile
117+
required-version = "==0.11.*" # match version above and in Dockerfile
113118

114119
[tool.uv.build-backend]
115120
module-root = ""

tests/conftest.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
#!/usr/bin/env python
2+
"""Shared pytest fixtures for deterministic OSMnx tests."""
3+
4+
from __future__ import annotations
5+
6+
import json
7+
import os
8+
from pathlib import Path
9+
from typing import TYPE_CHECKING
10+
from typing import Any
11+
from typing import TypeAlias
12+
13+
import matplotlib as mpl
14+
import networkx as nx
15+
import pytest
16+
import requests
17+
from shapely import LineString
18+
19+
mpl.use("Agg")
20+
21+
if TYPE_CHECKING:
22+
from collections.abc import Iterator
23+
24+
LOCATION_POINT = (37.791427, -122.410018)
25+
ADDRESS = "Transamerica Pyramid, 600 Montgomery Street, San Francisco, California, USA"
26+
PLACE = {"city": "Piedmont", "state": "California", "country": "USA"}
27+
TAGS: dict[str, bool | str | list[str]] = {
28+
"landuse": True,
29+
"building": True,
30+
"highway": True,
31+
"amenity": True,
32+
}
33+
34+
_ResponseJson: TypeAlias = dict[str, object] | list[dict[str, object]]
35+
HTTP_OK = 200
36+
HTTP_ERROR = 500
37+
38+
39+
def _drive_graph() -> nx.MultiDiGraph:
40+
import osmnx as ox # noqa: PLC0415
41+
42+
return ox.graph_from_point(
43+
LOCATION_POINT,
44+
dist=500,
45+
network_type="drive",
46+
simplify=False,
47+
retain_all=True,
48+
)
49+
50+
51+
def _toy_graph(*, crs: str = "epsg:4326") -> nx.MultiDiGraph:
52+
G = nx.MultiDiGraph(crs=crs)
53+
G.add_node(1, x=0.0, y=0.0, street_count=1, elevation=0.0)
54+
G.add_node(2, x=1.0, y=0.0, street_count=2, elevation=10.0)
55+
G.add_node(3, x=2.0, y=0.0, street_count=1, elevation=20.0)
56+
G.add_edge(
57+
1,
58+
2,
59+
osmid=10,
60+
length=1.0,
61+
highway="residential",
62+
maxspeed="25 mph",
63+
geometry=LineString([(0, 0), (1, 0)]),
64+
)
65+
G.add_edge(
66+
2,
67+
3,
68+
osmid=11,
69+
length=1.0,
70+
highway=["primary", "secondary"],
71+
maxspeed=["30 mph", "50"],
72+
geometry=LineString([(1, 0), (2, 0)]),
73+
)
74+
return G
75+
76+
77+
class _Response(requests.Response):
78+
def __init__(self, payload: _ResponseJson, *, ok: bool = True, status_code: int = 200) -> None:
79+
super().__init__()
80+
self._payload = payload
81+
self.status_code = status_code if ok or status_code != HTTP_OK else HTTP_ERROR
82+
self.reason = "OK" if ok else "Error"
83+
self._content = json.dumps(payload).encode()
84+
self.url = "https://example.com/api"
85+
86+
def json(self, **kwargs: object) -> _ResponseJson:
87+
del kwargs
88+
return self._payload
89+
90+
91+
drive_graph = _drive_graph
92+
toy_graph = _toy_graph
93+
Response = _Response
94+
95+
HTTP_CACHE_DIR = Path(__file__).parent / "input_data" / "http_cache"
96+
97+
98+
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
99+
"""
100+
Skip live online tests unless explicitly requested.
101+
102+
Parameters
103+
----------
104+
config
105+
Pytest configuration object.
106+
items
107+
Collected test items.
108+
"""
109+
del config
110+
111+
if os.environ.get("OSMNX_RUN_ONLINE_TESTS"):
112+
return
113+
114+
skip_online = pytest.mark.skip(reason="set OSMNX_RUN_ONLINE_TESTS=1 to run online tests")
115+
for item in items:
116+
if item.get_closest_marker("online"):
117+
item.add_marker(skip_online)
118+
119+
120+
@pytest.fixture(autouse=True)
121+
def _isolate_settings(tmp_path: Path) -> Iterator[None]:
122+
"""
123+
Restore global settings after each test and isolate generated files.
124+
125+
Parameters
126+
----------
127+
tmp_path
128+
Temporary directory unique to the test.
129+
130+
Yields
131+
------
132+
None
133+
Control returns to pytest after each test.
134+
"""
135+
import osmnx as ox # noqa: PLC0415
136+
137+
original_settings = {
138+
name: getattr(ox.settings, name)
139+
for name in dir(ox.settings)
140+
if not name.startswith("_") and name.islower()
141+
}
142+
143+
ox.settings.data_folder = tmp_path / "data"
144+
ox.settings.logs_folder = tmp_path / "logs"
145+
ox.settings.imgs_folder = tmp_path / "imgs"
146+
ox.settings.cache_folder = tmp_path / "cache"
147+
ox.settings.log_console = False
148+
ox.settings.log_file = False
149+
ox.settings.use_cache = True
150+
151+
yield
152+
153+
for name, value in original_settings.items():
154+
setattr(ox.settings, name, value)
155+
156+
157+
@pytest.fixture(autouse=True)
158+
def _block_network(
159+
monkeypatch: pytest.MonkeyPatch,
160+
request: pytest.FixtureRequest,
161+
) -> None:
162+
"""
163+
Prevent accidental live HTTP calls in the default offline suite.
164+
165+
Parameters
166+
----------
167+
monkeypatch
168+
Pytest fixture for temporary object replacement.
169+
request
170+
Pytest request object for the active test.
171+
"""
172+
if request.node.get_closest_marker("online") and os.environ.get("OSMNX_RUN_ONLINE_TESTS"):
173+
return
174+
175+
def _blocked_request(*args: Any, **kwargs: Any) -> None: # noqa: ANN401, ARG001
176+
msg = (
177+
"Network access is blocked in offline tests. Mark the test with "
178+
"`@pytest.mark.online` and set OSMNX_RUN_ONLINE_TESTS=1 to allow it."
179+
)
180+
raise AssertionError(msg)
181+
182+
monkeypatch.setattr(requests, "get", _blocked_request)
183+
monkeypatch.setattr(requests, "post", _blocked_request)
184+
185+
186+
@pytest.fixture
187+
def http_cache() -> Path:
188+
"""
189+
Point OSMnx cache lookups at committed raw HTTP response fixtures.
190+
191+
Returns
192+
-------
193+
pathlib.Path
194+
Directory containing committed raw HTTP cache files.
195+
"""
196+
import osmnx as ox # noqa: PLC0415
197+
198+
ox.settings.cache_folder = HTTP_CACHE_DIR
199+
ox.settings.use_cache = True
200+
ox.settings.overpass_rate_limit = False
201+
return HTTP_CACHE_DIR
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[]

0 commit comments

Comments
 (0)