Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add type annotations #1107

Merged
merged 94 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
14d1d40
update mypy config
gboeing Dec 24, 2023
9a429ef
add type annotations
gboeing Dec 24, 2023
d467bd7
update mypy config
gboeing Dec 24, 2023
8369e09
add type annotations
gboeing Dec 24, 2023
8a02787
formatting
gboeing Dec 25, 2023
98f3db7
update mypy config
gboeing Dec 25, 2023
b583c8b
add type annotations to utils_geo module
gboeing Dec 25, 2023
da958f5
ignore deprecated function
gboeing Dec 25, 2023
bc60bb5
remove nx types that are causing trouble
gboeing Dec 25, 2023
118fa20
add type annotations to utils_graph module
gboeing Dec 25, 2023
2b37d15
use future annotations to replace old Union with modern pipe syntax
gboeing Dec 28, 2023
cb0a773
require python 3.9+ for typing
gboeing Dec 28, 2023
f4ecea4
add type annotations and make var names unique
gboeing Dec 28, 2023
ed755a7
add overloads for graph_to_gdfs
gboeing Dec 28, 2023
a50ab05
add type hints to stats module
gboeing Jan 2, 2024
f1dc43a
add type hints to speed module
gboeing Jan 2, 2024
6db9bb9
update mypy config
gboeing Jan 2, 2024
d630f90
add type hints to simplification module
gboeing Jan 2, 2024
b0b4fa8
add type hints to routing module
gboeing Jan 2, 2024
217e85c
add type hints to projection module
gboeing Jan 3, 2024
155b7d6
add type hints to _downloader module
gboeing Jan 3, 2024
3e77e27
add type hints to _nominatim module
gboeing Jan 3, 2024
4c0cb97
add type hints to _overpass module
gboeing Jan 3, 2024
98c893b
fix var type
gboeing Jan 3, 2024
df601b6
add type hints to bearing module
gboeing Jan 3, 2024
a48de37
ignore untyped defs in deprecated modules
gboeing Jan 3, 2024
17c657f
add type hints to geocoder module
gboeing Jan 3, 2024
a095ba2
make type more-flexible iterable instead of list
gboeing Jan 4, 2024
60d71e4
add type hints to distance module
gboeing Jan 4, 2024
3a8a698
add type hints to elevation module
gboeing Jan 4, 2024
8bc2754
allow implicit optional
gboeing Jan 4, 2024
42ffca7
add type hints to io module
gboeing Jan 4, 2024
1101fdb
fix types
gboeing Jan 5, 2024
02585c5
add overloads to nearest_nodes and nearest_edges functions
gboeing Jan 5, 2024
5daa6f6
add type hints to graph module
gboeing Jan 5, 2024
16b37cf
initial pass at adding type hints to plot module
gboeing Jan 5, 2024
6d17148
clean up plot module typing
gboeing Jan 7, 2024
f657824
add type hints to the _OSMContentHandler class
gboeing Jan 7, 2024
c07837d
add type hints to osm_xml module
gboeing Jan 7, 2024
268b350
Merge branch 'main' into typing
gboeing Jan 7, 2024
d33c50d
resolve merge conflict
gboeing Jan 7, 2024
5edea96
ignore errors from calls to untyped deprecated functions
gboeing Jan 7, 2024
3a492ac
add type annotations to features module
gboeing Jan 7, 2024
9b6fa3b
replace list type with more flexible Iterable
gboeing Jan 7, 2024
84745a8
add type hints to the settings module
gboeing Jan 7, 2024
13ffbde
fix docstring
gboeing Jan 7, 2024
605de01
update mypy config
gboeing Jan 7, 2024
1d24d89
update mypy config
gboeing Jan 7, 2024
c50ae0c
fix mypy validation
gboeing Jan 7, 2024
1924ea1
get tests to pass mypy validation
gboeing Jan 7, 2024
d563554
remove unused var
gboeing Jan 7, 2024
ba3ebdf
update mypy config and pass validation
gboeing Jan 8, 2024
8f8abde
disallow implicit optional
gboeing Jan 8, 2024
6436230
add some missing type parameters for generic types
gboeing Jan 8, 2024
b66c5bc
only check for remark key when response_json is a dict
gboeing Jan 8, 2024
84d3522
add typing packages
gboeing Jan 10, 2024
9c4a0c5
update script
gboeing Jan 10, 2024
4ac685c
add some more missing type parameters for generic types
gboeing Jan 10, 2024
caa89da
fix response_json typing
gboeing Jan 10, 2024
d8e1e4e
add type hints to plot module
gboeing Jan 10, 2024
902dd7c
add missing type parameters to osm_xml module
gboeing Jan 11, 2024
3d98ae8
update pre-commit versions
gboeing Jan 11, 2024
b6d12ef
update changelog
gboeing Jan 11, 2024
53781f7
fix docstring
gboeing Jan 11, 2024
90a0867
improve var names and docstrings
gboeing Jan 11, 2024
8f0b750
update changelog
gboeing Jan 11, 2024
b212c1f
narrow the response_json(s) types
gboeing Jan 11, 2024
e9e9a0e
narrow the return type of shortest_path function and overload it
gboeing Jan 11, 2024
573debf
update docs config
gboeing Jan 11, 2024
21c2376
narrow types
gboeing Jan 11, 2024
3ffdd61
use type iterable instead of sequence where possible
gboeing Jan 11, 2024
c52c712
deprecate return_hex param
gboeing Jan 12, 2024
69e0597
fix type annotations
gboeing Jan 12, 2024
b67aef0
ensure utils_graph receives GeoDataFrame type
gboeing Jan 12, 2024
0a03811
fix type annotations and inconsistencies via runtime testing
gboeing Jan 13, 2024
73fe1d4
add typeguard
gboeing Jan 13, 2024
58ff865
replace hasattr(__iter__) with isinstance(Iterable)
gboeing Jan 13, 2024
dfc4ef3
fix type:ignore[assignment] skips
gboeing Jan 13, 2024
2242ce4
remove unnecessary duplicative type annotations
gboeing Jan 13, 2024
82337bc
fix typo
gboeing Jan 14, 2024
6455ca2
simplify save_graph_xml arg types
gboeing Jan 16, 2024
2af97ef
Merge branch 'typing' of https://github.com/gboeing/osmnx into typing
gboeing Jan 16, 2024
1a5974d
Merge branch 'v2' into typing
gboeing Jan 16, 2024
ff1e588
fix tests
gboeing Jan 16, 2024
57447de
fix geocoder
gboeing Jan 16, 2024
3dd890e
skip link checks
gboeing Jan 16, 2024
35af461
add tests
gboeing Jan 16, 2024
2785129
improve error raising
gboeing Jan 16, 2024
e58eed7
no coverage for function typing overloads
gboeing Jan 16, 2024
4ab170d
fix coverage upload
gboeing Jan 16, 2024
2228314
improve test coverage
gboeing Jan 16, 2024
d693181
update ci config
gboeing Jan 16, 2024
32bff74
update config
gboeing Jan 16, 2024
19ac164
make api_version str and change order of encoding param
gboeing Jan 16, 2024
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI

on:
push:
branches: [main, typing]
branches: [main]
pull_request:
branches: [main, v2]
schedule:
Expand Down Expand Up @@ -50,7 +50,7 @@ jobs:
run: make -C ./docs html

- name: Test code
run: pytest --cov=./osmnx --cov-report=xml --cov-report=term-missing --verbose
run: pytest --maxfail=1 --typeguard-packages=osmnx --cov=./osmnx --cov-report=xml --cov-report=term-missing --verbose

- name: Upload coverage report
uses: codecov/codecov-action@v3
2 changes: 1 addition & 1 deletion .github/workflows/test-minimal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,4 @@ jobs:
python -m sphinx -b linkcheck ./docs/source ./docs/build/linkcheck

- name: Test code
run: pytest --cov=./osmnx --cov-report=term-missing --verbose
run: pytest --maxfail=1 --typeguard-packages=osmnx --cov=./osmnx --cov-report=term-missing --verbose
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ repos:
types_or: [markdown, yaml]

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.1.9"
rev: "v0.1.13"
hooks:
- id: ruff
args: [--fix]
Expand All @@ -40,4 +40,4 @@ repos:
rev: "v1.8.0"
hooks:
- id: mypy
additional_dependencies: [types-requests]
additional_dependencies: [matplotlib, pandas-stubs, types-requests]
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog

## Unreleased
## 2.0.0 (Unreleased)

- add type annotations throughout the package for user type hinting and type checking (#1107)

## 1.x.x (Unreleased)

- fix a bug in the features module's polygon handling (#1104)
- update obsolete numpy random number generation (#1108)
Expand Down
24 changes: 10 additions & 14 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@
https://www.sphinx-doc.org/en/master/usage/configuration.html
"""

# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
import sys
from pathlib import Path

# go up two levels from current working dir (/docs/source) to package root
pkg_root_path = str(Path.cwd().parent.parent)
sys.path.insert(0, pkg_root_path)

# project info
author = "Geoff Boeing"
copyright = "2016-2024, Geoff Boeing" # noqa: A001
project = "OSMnx"

# go up two levels from current working dir (/docs/source) to package root
pkg_root_path = str(Path.cwd().parent.parent)
sys.path.insert(0, pkg_root_path)

# dynamically load version from /osmnx/_version.py
with Path.open(Path("../../osmnx/_version.py")) as f:
version = release = f.read().split(" = ")[1].replace('"', "")
Expand All @@ -38,17 +37,14 @@
"sklearn",
]

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
# general configuration and options for HTML output
# see https://www.sphinx-doc.org/en/master/usage/configuration.html
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon"]
html_static_path: list[str] = []
html_theme = "furo"
language = "en"
needs_sphinx = "7" # same value as pinned in /docs/requirements.txt
root_doc = "index"
source_suffix = ".rst"
templates_path: list = []

# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_static_path: list = []
html_theme = "furo"
templates_path: list[str] = []
6 changes: 6 additions & 0 deletions environments/docker/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ hatch
pip
twine

# typing
mypy
pandas-stubs
typeguard
types-requests

# linting/testing
nbdime
nbqa
Expand Down
2 changes: 1 addition & 1 deletion environments/linux/create-environment.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ set -e
ENV=ox
PACKAGE=osmnx
eval "$(conda shell.bash hook)"
conda deactivate
conda activate base
mamba env remove -n $ENV --yes
mamba clean --all --yes --quiet --no-banner
mamba create -c conda-forge --strict-channel-priority -n $ENV --file "../docker/requirements.txt" --yes --no-banner
Expand Down
63 changes: 42 additions & 21 deletions osmnx/_downloader.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""Handle HTTP requests to web APIs."""

from __future__ import annotations

import json
import logging as lg
import socket
from hashlib import sha1
from pathlib import Path
from typing import Any
from urllib.parse import urlparse

import requests
Expand All @@ -19,7 +22,9 @@
_original_getaddrinfo = socket.getaddrinfo


def _save_to_cache(url, response_json, ok):
def _save_to_cache(
url: str, response_json: dict[str, Any] | list[dict[str, Any]], ok: bool
) -> None:
"""
Save a HTTP response JSON object to a file in the cache folder.

Expand All @@ -39,7 +44,7 @@ def _save_to_cache(url, response_json, ok):
----------
url : string
the URL of the request
response_json : dict
response_json : dict or list
the JSON response
ok : bool
requests response.ok value
Expand Down Expand Up @@ -70,11 +75,12 @@ def _save_to_cache(url, response_json, ok):
utils.log(f"Saved response to cache file {str(cache_filepath)!r}")


def _url_in_cache(url):
def _url_in_cache(url: str) -> Path | None:
"""
Determine if a URL's response exists in the cache.

Calculates the checksum of url to determine the cache file's name.
Calculates the checksum of `url` to determine the cache file's name.
Returns None if it cannot be found in the cache.

Parameters
----------
Expand All @@ -94,33 +100,39 @@ def _url_in_cache(url):
return filepath if filepath.is_file() else None


def _retrieve_from_cache(url, check_remark=True):
def _retrieve_from_cache(
url: str, check_remark: bool = True
) -> dict[str, Any] | list[dict[str, Any]] | None:
"""
Retrieve a HTTP response JSON object from the cache, if it exists.

Parameters
----------
url : string
the URL of the request
check_remark : string
check_remark : bool
if True, only return filepath if cached response does not have a
remark key indicating a server warning

Returns
-------
response_json : dict
response_json : dict or list
cached response for url if it exists in the cache, otherwise None
"""
# if the tool is configured to use the cache
if settings.use_cache:
# return cached response for this url if exists, otherwise return None
cache_filepath = _url_in_cache(url)
if cache_filepath is not None:
response_json = json.loads(cache_filepath.read_text(encoding="utf-8"))
response_json: dict[str, Any] | list[dict[str, Any]] = json.loads(
cache_filepath.read_text(encoding="utf-8")
)

# return None if check_remark is True and there is a server
# remark in the cached response
if check_remark and "remark" in response_json: # pragma: no cover
if (
check_remark and isinstance(response_json, dict) and "remark" in response_json
): # pragma: no cover
utils.log(
f"Ignoring cache file {str(cache_filepath)!r} because "
f"it contains a remark: {response_json['remark']!r}"
Expand All @@ -132,7 +144,9 @@ def _retrieve_from_cache(url, check_remark=True):
return None


def _get_http_headers(user_agent=None, referer=None, accept_language=None):
def _get_http_headers(
user_agent: str | None = None, referer: str | None = None, accept_language: str | None = None
) -> dict[str, str]:
"""
Update the default requests HTTP headers with OSMnx info.

Expand All @@ -157,14 +171,14 @@ def _get_http_headers(user_agent=None, referer=None, accept_language=None):
if accept_language is None:
accept_language = settings.default_accept_language

headers = requests.utils.default_headers()
headers = dict(requests.utils.default_headers())
headers.update(
{"User-Agent": user_agent, "referer": referer, "Accept-Language": accept_language}
)
return headers


def _resolve_host_via_doh(hostname):
def _resolve_host_via_doh(hostname: str) -> str:
"""
Resolve hostname to IP address via Google's public DNS-over-HTTPS API.

Expand Down Expand Up @@ -201,19 +215,20 @@ def _resolve_host_via_doh(hostname):
utils.log(err_msg, level=lg.ERROR)
return hostname

# if there were no exceptions, return
# if there were no request exceptions, return
else:
if response.ok and data["Status"] == 0:
# status 0 means NOERROR, so return the IP address
return data["Answer"][0]["data"]
ip_address: str = data["Answer"][0]["data"]
return ip_address

# otherwise, if we cannot reach DoH server or cannot resolve host
# just return the hostname itself
utils.log(err_msg, level=lg.ERROR)
return hostname


def _config_dns(url):
def _config_dns(url: str) -> None:
"""
Force socket.getaddrinfo to use IP address instead of hostname.

Expand Down Expand Up @@ -251,7 +266,7 @@ def _config_dns(url):
ip = _resolve_host_via_doh(hostname)

# mutate socket.getaddrinfo to map hostname -> IP address
def _getaddrinfo(*args, **kwargs):
def _getaddrinfo(*args, **kwargs): # type: ignore[no-untyped-def]
if args[0] == hostname:
utils.log(f"Resolved {hostname!r} to {ip!r}")
return _original_getaddrinfo(ip, *args[1:], **kwargs)
Expand All @@ -262,7 +277,7 @@ def _getaddrinfo(*args, **kwargs):
socket.getaddrinfo = _getaddrinfo


def _hostname_from_url(url):
def _hostname_from_url(url: str) -> str:
"""
Extract the hostname (domain) from a URL.

Expand All @@ -279,7 +294,7 @@ def _hostname_from_url(url):
return urlparse(url).netloc.split(":")[0]


def _parse_response(response):
def _parse_response(response: requests.Response) -> dict[str, Any] | list[dict[str, Any]]:
"""
Parse JSON from a requests response and log the details.

Expand All @@ -290,7 +305,9 @@ def _parse_response(response):

Returns
-------
response_json : dict
response_json : dict or list
Value will be a dict if the response is from the Google or Overpass
APIs, and a list if the response is from the Nominatim API.
"""
# log the response size and domain
domain = _hostname_from_url(response.url)
Expand All @@ -299,7 +316,7 @@ def _parse_response(response):

# parse the response to JSON and log/raise exceptions
try:
response_json = response.json()
response_json: dict[str, Any] | list[dict[str, Any]] = response.json()
except JSONDecodeError as e: # pragma: no cover
msg = f"{domain!r} responded: {response.status_code} {response.reason} {response.text}"
utils.log(msg, level=lg.ERROR)
Expand All @@ -308,7 +325,11 @@ def _parse_response(response):
raise ResponseStatusCodeError(msg) from e

# log any remarks if they exist
if "remark" in response_json: # pragma: no cover
if isinstance(response_json, dict) and "remark" in response_json: # pragma: no cover
utils.log(f'{domain!r} remarked: {response_json["remark"]!r}', level=lg.WARNING)

# log if the response status_code is not OK
if not response.ok:
utils.log(f"{domain!r} returned HTTP status code {response.status_code}", level=lg.WARNING)

return response_json
Loading