Skip to content

Commit

Permalink
Python 3.12 dependency specification (#219)
Browse files Browse the repository at this point in the history
* Update Makefile

* Update command_line.py

* Update timezonefinder.py

* add test_get_polygon_boundaries

* v6.4.1

* added official support for python 3.8 again, by specifying numba as multiple constraint dependency

* fix tox.ini python 3.8
  • Loading branch information
jannikmi authored Feb 8, 2024
1 parent e3895f4 commit 6dad778
Show file tree
Hide file tree
Showing 9 changed files with 361 additions and 184 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
fail-fast: false
matrix:
python-version:
- "3.8"
- "3.9"
- "3.10"
- "3.11"
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
Changelog
=========

6.4.1 (2024-02-08)
------------------

* added official support for python 3.8 again, by specifying numba as multiple constraint dependency


internal:

* added unit tests for polygon boundary binary reading


6.4.0 (2024-02-02)
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ test:
test1: test

tox:
@tox --parallel auto
@tox

test2: tox

Expand Down
447 changes: 285 additions & 162 deletions poetry.lock

Large diffs are not rendered by default.

17 changes: 12 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "timezonefinder"
version = "6.4.0"
version = "6.4.1"
description = "python package for finding the timezone of any point on earth (coordinates) offline"
authors = ["jannikmi <github@michelfe.it>"]
license = "MIT"
Expand Down Expand Up @@ -30,15 +30,21 @@ include = [
timezonefinder = "timezonefinder.command_line:main"

[tool.poetry.dependencies]
python = ">=3.9,<4"
numpy = ">=1.18,<2"
python = ">=3.8,<4"
numpy = [
{ version = "<1.25", python = "<3.9"},
{ version = ">=1.25,<2", python = ">=3.9"}
]
h3 = ">=3.7.6,<4" # python3.11 support
cffi = ">=1.15.1,<2"
# build dependencies. workaround to always have these installed
setuptools = ">=65.5"

# optional dependencies (extras)
numba = { version = ">=0.59,<1", optional = true } # python3.12 support since 0.59
numba = [
{ version = "<0.59", python = "<3.12", optional = true },
{ version = ">=0.59,<1", python = ">=3.12", optional = true }
]
pytz = { version = ">=2022.7.1", optional = true }

[tool.poetry.group.docs]
Expand Down Expand Up @@ -82,10 +88,11 @@ legacy_tox_ini = """
[tox]
isolated_build = true
envlist =
docs,py{39,310,311,312}{,-numba,-pytz}
docs,py{38,39,310,311,312}{,-numba,-pytz}
[gh-actions]
python =
3.8: py38{,-numba,-pytz}
3.9: py39{,-numba,-pytz}
3.10: py310{,-numba,-pytz}
3.11: py311{,-numba,-pytz}
Expand Down
36 changes: 34 additions & 2 deletions tests/main_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@

from tests.auxiliaries import time_preprocess
from tests.locations import BASIC_TEST_LOCATIONS, BOUNDARY_TEST_CASES, TEST_LOCATIONS
from timezonefinder.configs import INT2COORD_FACTOR, THRES_DTYPE_H, TIMEZONE_NAMES_FILE
from timezonefinder.configs import (
INT2COORD_FACTOR,
MAX_LAT_VAL_INT,
MAX_LNG_VAL_INT,
THRES_DTYPE_H,
TIMEZONE_NAMES_FILE,
)
from timezonefinder.timezonefinder import (
AbstractTimezoneFinder,
TimezoneFinder,
Expand Down Expand Up @@ -56,6 +62,14 @@ def check_pairwise_geometry(geometry_obj: List):
assert len(first_coord_pair) == 2, "the polygon does not consist of coordinate pairs as expected."


def is_valid_lng_int(x: int) -> bool:
return -MAX_LNG_VAL_INT <= x <= MAX_LNG_VAL_INT


def is_valid_lat_int(y: int) -> bool:
return -MAX_LAT_VAL_INT <= y <= MAX_LAT_VAL_INT


# tests for TimezonefinderL class
class BaseTimezoneFinderClassTest(unittest.TestCase):
in_memory_mode = False
Expand Down Expand Up @@ -216,7 +230,7 @@ def test_shortcut_boundary_result(self):
self.check_boundary(lng, lat, expected)

def test_certain_timezone_at(self):
print("\ntestin certain_timezone_at():") # expected equal results to timezone_at(), is just slower
print("\ntesting certain_timezone_at():") # expected equal results to timezone_at(), is just slower
self.run_location_tests(self.test_instance.certain_timezone_at, self.test_locations)

def test_overflow(self):
Expand Down Expand Up @@ -282,6 +296,24 @@ def test_get_geometry(self):
self.test_instance.get_geometry(tz_name=None, tz_id=nr_timezones, use_id=True, coords_as_pairs=False)
self.test_instance.get_geometry(tz_name="", tz_id=-1, use_id=True, coords_as_pairs=False)

# TODO add unit tests for all other binary data reading functions (all possible inputs)
def test_get_polygon_boundaries(self):
# boundaries should be defined for each polygon
instance = self.test_instance
nr_of_polygons = instance.nr_of_polygons
for poly_id in range(nr_of_polygons):
boundaries = instance.get_polygon_boundaries(poly_id=poly_id)
assert isinstance(boundaries, tuple)
assert len(boundaries) == 4
xmax, xmin, ymax, ymin = boundaries
# test value range:
assert is_valid_lat_int(ymin)
assert is_valid_lat_int(ymax)
assert is_valid_lng_int(xmin)
assert is_valid_lng_int(xmax)
assert ymin < ymax
assert xmin < xmax


class TimezonefinderClassTestMEM(TimezonefinderClassTest):
in_memory_mode = True
Expand Down
5 changes: 2 additions & 3 deletions timezonefinder/command_line.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import argparse
from typing import Callable
from typing import Callable, Union

from timezonefinder import TimezoneFinder, TimezoneFinderL
from timezonefinder.timezonefinder import AbstractTimezoneFinder


def get_timezone_function(function_id: int) -> Callable:
"""
Note: script is being called for each point individually. Caching TimezoneFinder() instances is useless.
-> avoid constructing unnecessary instances
"""
tf_instance: AbstractTimezoneFinder
tf_instance: Union[TimezoneFinder, TimezoneFinderL]
if function_id in [0, 1, 5]:
tf_instance = TimezoneFinder()
functions = {
Expand Down
4 changes: 3 additions & 1 deletion timezonefinder/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@
COORD2INT_FACTOR = 10**DECIMAL_PLACES_SHIFT
MAX_LNG_VAL = 180.0
MAX_LAT_VAL = 90.0
MAX_INT_VAL = int(MAX_LNG_VAL * COORD2INT_FACTOR)
MAX_LNG_VAL_INT = int(MAX_LNG_VAL * COORD2INT_FACTOR)
MAX_LAT_VAL_INT = int(MAX_LAT_VAL * COORD2INT_FACTOR)
MAX_INT_VAL = MAX_LNG_VAL_INT
assert MAX_INT_VAL < MAX_ALLOWED_COORD_VAL

# TYPES
Expand Down
24 changes: 14 additions & 10 deletions timezonefinder/timezonefinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from io import BytesIO
from pathlib import Path
from struct import unpack
from typing import List, Optional, Union
from typing import List, Optional, Tuple, Union

import numpy as np
from h3.api import numpy_int as h3
Expand Down Expand Up @@ -464,6 +464,17 @@ def get_geometry(
# read and return all polygons from this zone:
return [self.get_polygon(poly_id, coords_as_pairs) for poly_id in range(this_zone_poly_id, next_zone_poly_id)]

def get_polygon_boundaries(self, poly_id: int) -> Tuple[int, int, int, int]:
"""returns the boundaries of the polygon = (lng_max, lng_min, lat_max, lat_min) converted to int32"""
poly_max_values = getattr(self, POLY_MAX_VALUES)
poly_max_values.seek(4 * NR_BYTES_I * poly_id)
xmax, xmin, ymax, ymin = self._fromfile(
poly_max_values,
dtype=DTYPE_FORMAT_SIGNED_I_NUMPY,
count=4,
)
return xmax, xmin, ymax, ymin

def outside_the_boundaries_of(self, poly_id: int, x: int, y: int) -> bool:
"""
Check if a point is outside the boundaries of a polygon.
Expand All @@ -473,14 +484,7 @@ def outside_the_boundaries_of(self, poly_id: int, x: int, y: int) -> bool:
:param y: Y-coordinate of the point
:return: True if the point is outside the boundaries, False otherwise
"""
# get the boundaries of the polygon = (lng_max, lng_min, lat_max, lat_min) converted to int32
poly_max_values = getattr(self, POLY_MAX_VALUES)
poly_max_values.seek(4 * NR_BYTES_I * poly_id)
xmax, xmin, ymax, ymin = self._fromfile(
poly_max_values,
dtype=DTYPE_FORMAT_SIGNED_I_NUMPY,
count=4,
)
xmax, xmin, ymax, ymin = self.get_polygon_boundaries(poly_id)
return x > xmax or x < xmin or y > ymax or y < ymin

def inside_of_polygon(self, poly_id: int, x: int, y: int) -> bool:
Expand Down Expand Up @@ -567,7 +571,7 @@ def certain_timezone_at(self, *, lng: float, lat: float) -> Optional[str]:
.. note:: this is only meaningful when you have compiled your own timezone data
where there are areas without timezone polygon coverage.
Otherwise some timezone will always be matched and the functionality is equal to using `.timezone_at()`
Otherwise, some timezone will always be matched and the functionality is equal to using `.timezone_at()`
-> useless to actually test all polygons.
.. note:: using this function is less performant than `.timezone_at()`
Expand Down

0 comments on commit 6dad778

Please sign in to comment.