From 29b4d8d9aa4b916266d93d9f1b16ff7a5378394f Mon Sep 17 00:00:00 2001 From: anthonypoh Date: Tue, 17 Mar 2026 14:55:49 +0800 Subject: [PATCH 1/2] fix: construct BBox properly in single-output mode typing.cast() is a static-analysis no-op and does not construct a BBox NamedTuple at runtime. The area string was being parsed into a plain list and passed downstream, causing AttributeError when make_bounds_tag() accessed bbox.min_lat (XML/o5m output paths). Replace cast("BBox", [...]) with BBox(*[...]) to actually instantiate the NamedTuple, and add a regression test verifying the correct type is passed to get_osm_output in single-output mode. Co-Authored-By: Claude Opus 4.6 --- pyhgtmap/hgt/processor.py | 7 ++----- tests/hgt/test_processor.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/pyhgtmap/hgt/processor.py b/pyhgtmap/hgt/processor.py index 81903da..08ec2ad 100644 --- a/pyhgtmap/hgt/processor.py +++ b/pyhgtmap/hgt/processor.py @@ -4,6 +4,7 @@ import multiprocessing from typing import TYPE_CHECKING, cast +from pyhgtmap import BBox from pyhgtmap.hgt.file import HgtFile from pyhgtmap.output.factory import get_osm_output @@ -12,7 +13,6 @@ from multiprocessing.context import ForkProcess # type: ignore[attr-defined] from multiprocessing.sharedctypes import Synchronized - from pyhgtmap import BBox from pyhgtmap.configuration import Configuration from pyhgtmap.hgt.tile import HgtTile from pyhgtmap.output import Output @@ -277,10 +277,7 @@ def process_files(self, files: list[tuple[str, bool]]) -> None: raise ValueError("self.options.area is not defined") self.get_osm_output( [file_tuple[0] for file_tuple in files], - cast( - "BBox", - [float(b) for b in self.options.area.split(":")], - ), + BBox(*[float(b) for b in self.options.area.split(":")]), ) # import objgraph diff --git a/tests/hgt/test_processor.py b/tests/hgt/test_processor.py index a931629..582c50a 100644 --- a/tests/hgt/test_processor.py +++ b/tests/hgt/test_processor.py @@ -331,6 +331,36 @@ def test_get_osm_output(default_options: Configuration) -> None: ) assert output1 is not output2 + @staticmethod + def test_process_files_single_output_bbox_type( + default_options: Configuration, + ) -> None: + """Regression test: process_files must pass a BBox (not a plain list) to + get_osm_output in single output mode. + + Previously, typing.cast() was used instead of BBox(), leaving the area + string parsed as a plain list at runtime, which caused AttributeError + when make_bounds_tag() accessed bbox.min_lat (XML / o5m output paths). + """ + default_options.maxNodesPerTile = 0 + default_options.area = "6:43:8:44" + processor = HgtFilesProcessor( + 1, + node_start_id=100, + way_start_id=200, + options=default_options, + ) + with mock.patch("pyhgtmap.hgt.processor.get_osm_output") as get_osm_output_mock: + get_osm_output_mock.return_value = Mock() + processor.process_files([]) # empty file list — only initializes output + + get_osm_output_mock.assert_called_once() + bbox_arg = get_osm_output_mock.call_args[0][2] + assert isinstance(bbox_arg, BBox), f"Expected BBox, got {type(bbox_arg)}" + assert bbox_arg == BBox( + min_lon=6.0, min_lat=43.0, max_lon=8.0, max_lat=44.0 + ) + @staticmethod def test_get_osm_output_single_output(default_options: Configuration) -> None: # Enable single output mode From a73a3dfa60e5ef621940166d832cad7c987e2d71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Grenotton?= Date: Tue, 17 Mar 2026 21:03:38 +0100 Subject: [PATCH 2/2] Use BBox instead of string for area config --- README.md | 2 +- pyhgtmap/NASASRTMUtil.py | 27 ++++++++-------- pyhgtmap/cli.py | 17 ++++++++-- pyhgtmap/configuration.py | 4 +-- pyhgtmap/hgt/file.py | 7 ++-- pyhgtmap/hgt/processor.py | 4 +-- pyhgtmap/main.py | 9 ++---- tests/hgt/test_file.py | 8 ++--- tests/hgt/test_processor.py | 4 +-- tests/test_NASASRTMUtil.py | 64 +++++++++++++++++++++---------------- tests/test_cli.py | 23 ++++++------- tests/test_main.py | 15 ++++----- 12 files changed, 98 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 416fce6..44a823a 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ This source requires creating an AW3D30 account on https://www.eorc.jaxa.jp/ALOS For ubuntu-like system: -## Ligther deployment (without GeoTiff support) +## Lighter deployment (without GeoTiff support) GDAL dependency is required only to add GeoTiff support, and is quite painful to install. If you don't need GeoTiff support, simply install the default version of pyhgtmap: diff --git a/pyhgtmap/NASASRTMUtil.py b/pyhgtmap/NASASRTMUtil.py index 624d71b..23ac270 100644 --- a/pyhgtmap/NASASRTMUtil.py +++ b/pyhgtmap/NASASRTMUtil.py @@ -12,20 +12,19 @@ if TYPE_CHECKING: from collections.abc import Iterable - from pyhgtmap import PolygonsList + from pyhgtmap import BBox, PolygonsList IntBBox: TypeAlias = tuple[int, int, int, int] -def calc_bbox(area: str, corrx: float = 0.0, corry: float = 0.0) -> IntBBox: - """Parse bounding box string and calculates the appropriate bounding box for the needed files""" - min_lon, min_lat, max_lon, max_lat = [ - float(value) - inc - for value, inc in zip( - area.split(":"), [corrx, corry, corrx, corry], strict=True - ) - ] +def calc_bbox(area: BBox, corrx: float = 0.0, corry: float = 0.0) -> IntBBox: + """Convert a BBox to integer bounding box coordinates for the needed files.""" + min_lon = area.min_lon - corrx + min_lat = area.min_lat - corry + max_lon = area.max_lon - corrx + max_lat = area.max_lat - corry + if min_lon < 0: bbox_min_lon = int(min_lon) if min_lon % 1 == 0 else int(min_lon) - 1 else: @@ -235,7 +234,7 @@ def get_file(self, area: str, source: str): def get_files( - area: str, + area: BBox, polygons: PolygonsList | None, corrx: float, corry: float, @@ -247,14 +246,14 @@ def get_files( files: list[tuple[str, bool]] = [] sources_pool = SourcesPool(configuration) - for area, check_poly in area_prefixes: + for area_prefix, check_poly in area_prefixes: for source in sources: - print(f"{area:s}: trying {source:s} ...") - save_filename = sources_pool.get_file(area, source) + print(f"{area_prefix:s}: trying {source:s} ...") + save_filename = sources_pool.get_file(area_prefix, source) if save_filename: files.append((save_filename, check_poly)) break else: - print(f"{area:s}: no file found on server.") + print(f"{area_prefix:s}: no file found on server.") continue return files diff --git a/pyhgtmap/cli.py b/pyhgtmap/cli.py index 97ac51d..6a5cfbf 100644 --- a/pyhgtmap/cli.py +++ b/pyhgtmap/cli.py @@ -6,7 +6,7 @@ from configargparse import ArgumentDefaultsHelpFormatter, ArgumentParser -from pyhgtmap import __version__ +from pyhgtmap import BBox, __version__ from pyhgtmap.configuration import CONFIG_FILENAME, Configuration, NestedConfig from pyhgtmap.hgt.file import parse_polygons_file from pyhgtmap.sources.pool import ALL_SUPPORTED_SOURCES, Pool @@ -15,6 +15,14 @@ from pyhgtmap.sources import Source +def _str_to_bbox(area_str: str) -> BBox: + """Convert area string format 'left:bottom:right:top' to BBox.""" + parts = area_str.split(":") + if len(parts) != 4: + raise ValueError(f"Invalid area format: {area_str}") + return BBox(*[float(x) for x in parts]) + + def build_common_parser() -> ArgumentParser: """Build the common argument parser for pyhgtmap.""" default_config = Configuration() @@ -53,6 +61,7 @@ def build_common_parser() -> ArgumentParser: metavar="LEFT:BOTTOM:RIGHT:TOP", action="store", default=default_config.area, + type=_str_to_bbox, ) parser.add_argument( "--polygon", @@ -401,8 +410,10 @@ def parse_command_line(sys_args: list[str]) -> tuple[Configuration, list[str]]: if not os.path.isfile(opts.polygon_file): print(f"Polygon file '{opts.polygon_file:s}' is not a regular file") sys.exit(1) - opts.area, opts.polygons = parse_polygons_file(opts.polygon_file) - elif opts.downloadOnly and not opts.area: + area_str, opts.polygons = parse_polygons_file(opts.polygon_file) + opts.area = _str_to_bbox(area_str) + + if not opts.area and opts.downloadOnly: # no area, no polygon, so nothing to download sys.stderr.write( "Nothing to download. Combine the --download-only option with" diff --git a/pyhgtmap/configuration.py b/pyhgtmap/configuration.py index b92e441..f7479d2 100644 --- a/pyhgtmap/configuration.py +++ b/pyhgtmap/configuration.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING, Any if TYPE_CHECKING: - from pyhgtmap import PolygonsList + from pyhgtmap import BBox, PolygonsList CONFIG_DIR = str(Path.home() / ".pyhgtmap") CONFIG_FILENAME = str(Path(CONFIG_DIR, "config.yaml")) @@ -48,7 +48,7 @@ def __init__(self, *args, **kwargs) -> None: # providing typing. # Sadly some parts have to be duplicated... - area: str | None = None + area: BBox | None = None polygon_file: str | None = None polygons: PolygonsList | None = None downloadOnly: bool = False diff --git a/pyhgtmap/hgt/file.py b/pyhgtmap/hgt/file.py index 9a73626..5d6d5be 100644 --- a/pyhgtmap/hgt/file.py +++ b/pyhgtmap/hgt/file.py @@ -569,7 +569,7 @@ def make_tiles(self, opts: Configuration) -> list[HgtTile]: step = int(opts.contourStepSize) or 20 def truncate_data( - area: str | None, inputData: numpy.ma.masked_array + area: BBox | None, inputData: numpy.ma.masked_array ) -> tuple[BBox, numpy.ma.masked_array]: """truncates a numpy array. returns (, , , ) and an array of the @@ -577,7 +577,10 @@ def truncate_data( """ if area: bboxMinLon, bboxMinLat, bboxMaxLon, bboxMaxLat = ( - float(bound) for bound in area.split(":") + area.min_lon, + area.min_lat, + area.max_lon, + area.max_lat, ) if self.reverseTransform is not None: bboxMinLon, bboxMinLat, bboxMaxLon, bboxMaxLat = transform_lon_lats( diff --git a/pyhgtmap/hgt/processor.py b/pyhgtmap/hgt/processor.py index 08ec2ad..5d39832 100644 --- a/pyhgtmap/hgt/processor.py +++ b/pyhgtmap/hgt/processor.py @@ -4,7 +4,6 @@ import multiprocessing from typing import TYPE_CHECKING, cast -from pyhgtmap import BBox from pyhgtmap.hgt.file import HgtFile from pyhgtmap.output.factory import get_osm_output @@ -13,6 +12,7 @@ from multiprocessing.context import ForkProcess # type: ignore[attr-defined] from multiprocessing.sharedctypes import Synchronized + from pyhgtmap import BBox from pyhgtmap.configuration import Configuration from pyhgtmap.hgt.tile import HgtTile from pyhgtmap.output import Output @@ -277,7 +277,7 @@ def process_files(self, files: list[tuple[str, bool]]) -> None: raise ValueError("self.options.area is not defined") self.get_osm_output( [file_tuple[0] for file_tuple in files], - BBox(*[float(b) for b in self.options.area.split(":")]), + self.options.area, ) # import objgraph diff --git a/pyhgtmap/main.py b/pyhgtmap/main.py index 74b504a..9131e67 100644 --- a/pyhgtmap/main.py +++ b/pyhgtmap/main.py @@ -29,12 +29,7 @@ def main_internal(sys_args: list[str]) -> None: for arg in args if os.path.splitext(arg)[1].lower() in (".hgt", ".tif", ".tiff", ".vrt") ] - opts.area = ":".join( - [ - str(i) - for i in calc_hgt_area(hgtDataFiles, opts.srtmCorrx, opts.srtmCorry) - ], - ) + opts.area = calc_hgt_area(hgtDataFiles, opts.srtmCorrx, opts.srtmCorry) # sources are not used in this case opts.dataSources = [] else: @@ -53,7 +48,7 @@ def main_internal(sys_args: list[str]) -> None: opts, ) if len(hgtDataFiles) == 0: - print(f"No files for this area {opts.area:s} from desired source(s).") + print(f"No files for this area {opts.area} from desired source(s).") sys.exit(0) elif opts.downloadOnly: sys.exit(0) diff --git a/tests/hgt/test_file.py b/tests/hgt/test_file.py index f254dcb..9476ced 100644 --- a/tests/hgt/test_file.py +++ b/tests/hgt/test_file.py @@ -1,12 +1,11 @@ from __future__ import annotations import os -from typing import TYPE_CHECKING import numpy import pytest -from pyhgtmap import Coordinates, Polygon, PolygonsList, hgt +from pyhgtmap import BBox, Coordinates, Polygon, PolygonsList, hgt from pyhgtmap.configuration import Configuration from pyhgtmap.hgt.file import ( HgtFile, @@ -19,9 +18,6 @@ from tests import TEST_DATA_PATH from tests.hgt import handle_optional_geotiff_support -if TYPE_CHECKING: - from pyhgtmap import BBox - HGT_SIZE: int = 1201 @@ -98,7 +94,7 @@ def test_make_tiles_chopped() -> None: def test_make_tiles_chopped_with_area() -> None: """Tiles chopped due to nodes threshold and area.""" custom_options = Configuration( - area="6.2:43.1:7.1:43.8", + area=BBox(6.2, 43.1, 7.1, 43.8), maxNodesPerTile=500000, contourStepSize=20, ) diff --git a/tests/hgt/test_processor.py b/tests/hgt/test_processor.py index 582c50a..959c5b3 100644 --- a/tests/hgt/test_processor.py +++ b/tests/hgt/test_processor.py @@ -281,7 +281,7 @@ def _test_process_files_single_output(nb_jobs: int, options) -> None: (os.path.join(TEST_DATA_PATH, "N43E007.hgt"), False), ] # This is usually done by main() (could be improved) - options.area = "6:43:8:44" + options.area = BBox(6.0, 43.0, 8.0, 44.0) # Increase step size to speed up test case options.contourStepSize = 500 # Instrument method without changing its behavior @@ -343,7 +343,7 @@ def test_process_files_single_output_bbox_type( when make_bounds_tag() accessed bbox.min_lat (XML / o5m output paths). """ default_options.maxNodesPerTile = 0 - default_options.area = "6:43:8:44" + default_options.area = BBox(6.0, 43.0, 8.0, 44.0) processor = HgtFilesProcessor( 1, node_start_id=100, diff --git a/tests/test_NASASRTMUtil.py b/tests/test_NASASRTMUtil.py index b3cd5d0..7a1f4f6 100644 --- a/tests/test_NASASRTMUtil.py +++ b/tests/test_NASASRTMUtil.py @@ -4,7 +4,7 @@ import numpy as np import pytest -from pyhgtmap import Coordinates, PolygonsList +from pyhgtmap import BBox, Coordinates, PolygonsList from pyhgtmap.configuration import Configuration from pyhgtmap.hgt.file import parse_polygons_file from pyhgtmap.NASASRTMUtil import ( @@ -23,7 +23,7 @@ def test_getFiles_no_source( test_configuration: Configuration, ) -> None: """No source, no file...""" - files = get_files("1:2:3:4", None, 0, 0, [], test_configuration) + files = get_files(BBox(1.0, 2.0, 3.0, 4.0), None, 0, 0, [], test_configuration) assert files == [] @@ -44,7 +44,9 @@ def test_getFiles_sonn3_no_poly( pool_mock.return_value.available_sources_names.return_value = ["sonn"] - files = get_files("1:2:3:4", None, 0, 0, ["sonn3"], test_configuration) + files = get_files( + BBox(1.0, 2.0, 3.0, 4.0), None, 0, 0, ["sonn3"], test_configuration + ) pool_mock.return_value.get_source.assert_called_with("sonn") assert pool_mock.return_value.get_source.return_value.get_file.call_args_list == [ @@ -77,7 +79,9 @@ def test_getFiles_multi_sources( "hgt/SONN3/N03E002.hgt", ] - files = get_files("1:2:3:4", None, 0, 0, ["sonn3", "view1"], test_configuration) + files = get_files( + BBox(1.0, 2.0, 3.0, 4.0), None, 0, 0, ["sonn3", "view1"], test_configuration + ) assert sources_pool_mock.return_value.get_file.call_args_list == [ call("N02E001", "sonn3"), @@ -628,7 +632,7 @@ class TestCalcBbox: @staticmethod def test_calc_bbox_positive_integer_coordinates() -> None: """Test with positive integer coordinates.""" - area = "0:0:10:10" + area = BBox(0.0, 0.0, 10.0, 10.0) result = calc_bbox(area) assert result == (0, 0, 10, 10) @@ -636,7 +640,7 @@ def test_calc_bbox_positive_integer_coordinates() -> None: @staticmethod def test_calc_bbox_positive_decimal_coordinates() -> None: """Test with positive decimal coordinates.""" - area = "0.5:0.5:10.5:10.5" + area = BBox(0.5, 0.5, 10.5, 10.5) result = calc_bbox(area) # Decimals round up for max values, min stays same @@ -645,7 +649,7 @@ def test_calc_bbox_positive_decimal_coordinates() -> None: @staticmethod def test_calc_bbox_negative_integer_coordinates() -> None: """Test with negative integer coordinates.""" - area = "-10:-10:0:0" + area = BBox(-10.0, -10.0, 0.0, 0.0) result = calc_bbox(area) assert result == (-10, -10, 0, 0) @@ -653,7 +657,7 @@ def test_calc_bbox_negative_integer_coordinates() -> None: @staticmethod def test_calc_bbox_negative_decimal_coordinates() -> None: """Test with negative decimal coordinates.""" - area = "-10.5:-10.5:-0.5:-0.5" + area = BBox(-10.5, -10.5, -0.5, -0.5) result = calc_bbox(area) # Negative decimals round down for min values @@ -662,7 +666,7 @@ def test_calc_bbox_negative_decimal_coordinates() -> None: @staticmethod def test_calc_bbox_mixed_sign_coordinates() -> None: """Test with mixed positive and negative coordinates.""" - area = "-5:0:5:10" + area = BBox(-5.0, 0.0, 5.0, 10.0) result = calc_bbox(area) assert result == (-5, 0, 5, 10) @@ -670,7 +674,7 @@ def test_calc_bbox_mixed_sign_coordinates() -> None: @staticmethod def test_calc_bbox_with_positive_corrections() -> None: """Test bounding box calculation with positive corrections.""" - area = "0:0:10:10" + area = BBox(0.0, 0.0, 10.0, 10.0) result = calc_bbox(area, corrx=0.5, corry=0.5) # Corrections are subtracted from parsed values @@ -683,7 +687,7 @@ def test_calc_bbox_with_positive_corrections() -> None: @staticmethod def test_calc_bbox_with_negative_corrections() -> None: """Test bounding box calculation with negative corrections.""" - area = "5:5:15:15" + area = BBox(5.0, 5.0, 15.0, 15.0) result = calc_bbox(area, corrx=-1.0, corry=-1.0) # Negative corrections (subtracted) expand the bbox @@ -696,7 +700,7 @@ def test_calc_bbox_with_negative_corrections() -> None: @staticmethod def test_calc_bbox_zero_area() -> None: """Test with zero-area bounding box.""" - area = "5:5:5:5" + area = BBox(5.0, 5.0, 5.0, 5.0) result = calc_bbox(area) assert result == (5, 5, 5, 5) @@ -704,7 +708,7 @@ def test_calc_bbox_zero_area() -> None: @staticmethod def test_calc_bbox_small_area() -> None: """Test with very small area (less than 1 degree).""" - area = "5.1:5.1:5.9:5.9" + area = BBox(5.1, 5.1, 5.9, 5.9) result = calc_bbox(area) # minLon: 5.1 (non-int) -> 5 @@ -716,7 +720,7 @@ def test_calc_bbox_small_area() -> None: @staticmethod def test_calc_bbox_large_area() -> None: """Test with large area spanning multiple tiles.""" - area = "-180:-90:180:90" + area = BBox(-180.0, -90.0, 180.0, 90.0) result = calc_bbox(area) assert result == (-180, -90, 180, 90) @@ -724,7 +728,7 @@ def test_calc_bbox_large_area() -> None: @staticmethod def test_calc_bbox_negative_min_positive_max() -> None: """Test with negative min and positive max.""" - area = "-5:-5:5:5" + area = BBox(-5.0, -5.0, 5.0, 5.0) result = calc_bbox(area) assert result == (-5, -5, 5, 5) @@ -732,7 +736,7 @@ def test_calc_bbox_negative_min_positive_max() -> None: @staticmethod def test_calc_bbox_negative_min_positive_decimal_max() -> None: """Test with negative min (integer) and positive decimal max.""" - area = "-5:-5:5.5:5.5" + area = BBox(-5.0, -5.0, 5.5, 5.5) result = calc_bbox(area) # minLon: -5 (integer) -> -5 @@ -744,7 +748,7 @@ def test_calc_bbox_negative_min_positive_decimal_max() -> None: @staticmethod def test_calc_bbox_negative_decimal_min_positive_max() -> None: """Test with negative decimal min and positive integer max.""" - area = "-5.5:-5.5:5:5" + area = BBox(-5.5, -5.5, 5.0, 5.0) result = calc_bbox(area) # minLon: -5.5 (non-int, negative) -> -6 @@ -756,7 +760,7 @@ def test_calc_bbox_negative_decimal_min_positive_max() -> None: @staticmethod def test_calc_bbox_asymmetric_area() -> None: """Test with asymmetric bounding box.""" - area = "10:20:30:40" + area = BBox(10.0, 20.0, 30.0, 40.0) result = calc_bbox(area) assert result == (10, 20, 30, 40) @@ -764,16 +768,20 @@ def test_calc_bbox_asymmetric_area() -> None: @pytest.mark.parametrize( ("area", "corrx", "corry", "expected"), [ - ("0:0:1:1", 0, 0, (0, 0, 1, 1)), - ("0:0:1:1", 0.5, 0.5, (-1, -1, 1, 1)), - ("10:10:20:20", 0, 0, (10, 10, 20, 20)), - ("10:10:20:20", 1, 1, (9, 9, 19, 19)), - ("-10:-10:0:0", 0, 0, (-10, -10, 0, 0)), - ("-10:-10:0:0", 0.5, 0.5, (-11, -11, 0, 0)), + (BBox(0.0, 0.0, 1.0, 1.0), 0, 0, (0, 0, 1, 1)), + (BBox(0.0, 0.0, 1.0, 1.0), 0.5, 0.5, (-1, -1, 1, 1)), + (BBox(10.0, 10.0, 20.0, 20.0), 0, 0, (10, 10, 20, 20)), + (BBox(10.0, 10.0, 20.0, 20.0), 1, 1, (9, 9, 19, 19)), + (BBox(-10.0, -10.0, 0.0, 0.0), 0, 0, (-10, -10, 0, 0)), + (BBox(-10.0, -10.0, 0.0, 0.0), 0.5, 0.5, (-11, -11, 0, 0)), ], ) def test_calc_bbox_parametrized( - self, area: str, corrx: float, corry: float, expected: tuple[int, int, int, int] + self, + area: BBox, + corrx: float, + corry: float, + expected: tuple[int, int, int, int], ) -> None: """Parametrized tests for calcBbox with various inputs.""" result = calc_bbox(area, corrx, corry) @@ -782,7 +790,7 @@ def test_calc_bbox_parametrized( @staticmethod def test_calc_bbox_equator_crossing() -> None: """Test bounding box that crosses the equator.""" - area = "-2:2:2:8" + area = BBox(-2.0, 2.0, 2.0, 8.0) result = calc_bbox(area) assert result == (-2, 2, 2, 8) @@ -790,7 +798,7 @@ def test_calc_bbox_equator_crossing() -> None: @staticmethod def test_calc_bbox_return_type() -> None: """Test that calcBbox returns tuple of 4 integers.""" - area = "5.5:5.5:15.5:15.5" + area = BBox(5.5, 5.5, 15.5, 15.5) result = calc_bbox(area) assert isinstance(result, tuple) @@ -800,7 +808,7 @@ def test_calc_bbox_return_type() -> None: @staticmethod def test_calc_bbox_min_max_ordering() -> None: """Test that result maintains minLon, minLat, maxLon, maxLat order.""" - area = "10:5:20:15" + area = BBox(10.0, 5.0, 20.0, 15.0) lon_min, lat_min, lon_max, lat_max = calc_bbox(area) # Verify ordering diff --git a/tests/test_cli.py b/tests/test_cli.py index 2105cf4..3a3806c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,6 +4,7 @@ import pytest +from pyhgtmap import BBox from pyhgtmap.cli import parse_command_line @@ -38,7 +39,7 @@ def test_parse_with_area_option() -> None: """Test parsing with --area option.""" opts, _ = parse_command_line(["--area", "0:0:1:1"]) - assert opts.area == "0:0:1:1" + assert opts.area == BBox(0.0, 0.0, 1.0, 1.0) assert isinstance(opts.dataSources, list) @staticmethod @@ -46,7 +47,7 @@ def test_parse_with_area_and_pbf() -> None: """Test parsing with --area and --pbf options.""" opts, _ = parse_command_line(["--area", "0:0:1:1", "--pbf"]) - assert opts.area == "0:0:1:1" + assert opts.area == BBox(0.0, 0.0, 1.0, 1.0) assert opts.pbf is True assert opts.gzip == 0 assert opts.o5m is False @@ -56,7 +57,7 @@ def test_parse_with_area_and_gzip() -> None: """Test parsing with --area and --gzip options.""" opts, _ = parse_command_line(["--area", "0:0:1:1", "--gzip", "5"]) - assert opts.area == "0:0:1:1" + assert opts.area == BBox(0.0, 0.0, 1.0, 1.0) assert opts.gzip == 5 assert opts.pbf is False @@ -65,7 +66,7 @@ def test_parse_with_area_and_o5m() -> None: """Test parsing with --area and --o5m options.""" opts, _ = parse_command_line(["--area", "0:0:1:1", "--o5m"]) - assert opts.area == "0:0:1:1" + assert opts.area == BBox(0.0, 0.0, 1.0, 1.0) assert opts.o5m is True assert opts.pbf is False assert opts.gzip == 0 @@ -75,7 +76,7 @@ def test_parse_with_sources_option() -> None: """Test parsing with --sources option.""" opts, _ = parse_command_line(["--area", "0:0:1:1", "--sources", "srtm1,view3"]) - assert opts.area == "0:0:1:1" + assert opts.area == BBox(0.0, 0.0, 1.0, 1.0) assert opts.dataSources == ["srtm1", "view3"] @staticmethod @@ -126,7 +127,7 @@ def test_parse_with_download_only_and_area() -> None: opts, _ = parse_command_line(["--area", "0:0:1:1", "--download-only"]) assert opts.downloadOnly is True - assert opts.area == "0:0:1:1" + assert opts.area == BBox(0.0, 0.0, 1.0, 1.0) @staticmethod def test_parse_with_corrx_and_corry() -> None: @@ -273,7 +274,7 @@ def test_parse_multiple_options_combined() -> None: ] ) - assert opts.area == "0:0:10:10" + assert opts.area == BBox(0.0, 0.0, 10.0, 10.0) assert opts.pbf is True assert opts.contourStepSize == "50" assert opts.contourFeet is True @@ -328,7 +329,7 @@ def test_parse_with_valid_polygon_file(self, mock_parse_poly: MagicMock) -> None try: opts, _ = parse_command_line(["--polygon", poly_file]) - assert opts.area == "0:0:1:1" + assert opts.area == BBox(0.0, 0.0, 1.0, 1.0) assert opts.polygons == [[0, 0], [1, 0], [1, 1], [0, 1]] mock_parse_poly.assert_called_once_with(poly_file) finally: @@ -339,14 +340,14 @@ def test_parse_with_positive_area_coordinates() -> None: """Test parsing with positive area coordinates.""" opts, _ = parse_command_line(["--area", "0:0:10:10"]) - assert opts.area == "0:0:10:10" + assert opts.area == BBox(0.0, 0.0, 10.0, 10.0) @staticmethod def test_parse_with_float_area_coordinates() -> None: """Test parsing with float area coordinates.""" opts, _ = parse_command_line(["--area", "10.5:20.3:30.7:40.1"]) - assert opts.area == "10.5:20.3:30.7:40.1" + assert opts.area == BBox(10.5, 20.3, 30.7, 40.1) @staticmethod def test_parse_void_range_max_default() -> None: @@ -367,7 +368,7 @@ def test_parse_download_only_with_area() -> None: opts, _ = parse_command_line(["--download-only", "--area", "0:0:1:1"]) assert opts.downloadOnly is True - assert opts.area == "0:0:1:1" + assert opts.area == BBox(0.0, 0.0, 1.0, 1.0) @staticmethod def test_parse_with_plot_prefix() -> None: diff --git a/tests/test_main.py b/tests/test_main.py index 0e825f1..165e18a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -6,7 +6,7 @@ import pytest -from pyhgtmap import main +from pyhgtmap import BBox, main from . import TEST_DATA_PATH @@ -51,9 +51,8 @@ def test_main_download_from_poly( # Check NASASRTMUtil_mock.get_files.assert_called_once() - assert ( - NASASRTMUtil_mock.get_files.call_args[0][0] - == "-6.9372070:41.2386600:9.9000000:51.4288000" + assert NASASRTMUtil_mock.get_files.call_args[0][0] == BBox( + -6.9372070, 41.2386600, 9.9000000, 51.4288000 ) assert NASASRTMUtil_mock.get_files.call_args[0][1][0][0:5] == [ (9.9, 42.43788), @@ -68,7 +67,7 @@ def test_main_download_from_poly( HgtFilesProcessor_mock.assert_called_once() parsed_options: Configuration = HgtFilesProcessor_mock.call_args.args[3] - assert parsed_options.area == "-6.9372070:41.2386600:9.9000000:51.4288000" + assert parsed_options.area == BBox(-6.9372070, 41.2386600, 9.9000000, 51.4288000) HgtFilesProcessor_mock.return_value.process_files.assert_called_once_with( [("hgt/VIEW1/N45E006.hgt", True), ("hgt/VIEW1/N46E006.hgt", True)], @@ -99,7 +98,7 @@ def test_main_manual_input_poly( HgtFilesProcessor_mock.assert_called_once() parsed_options: Configuration = HgtFilesProcessor_mock.call_args.args[3] # area must be properly computed from files names - assert parsed_options.area == "7.0:45.0:8.0:48.0" + assert parsed_options.area == BBox(7.0, 45.0, 8.0, 48.0) # Polygon check must be enabled for all files HgtFilesProcessor_mock.return_value.process_files.assert_called_once_with( [("N45E007.hgt", True), ("N46E007.hgt", True), ("N47E007.hgt", True)], @@ -128,7 +127,7 @@ def test_main_manual_input_poly_no_source( HgtFilesProcessor_mock.assert_called_once() parsed_options: Configuration = HgtFilesProcessor_mock.call_args.args[3] # area must be properly computed from files names - assert parsed_options.area == "7.0:45.0:8.0:48.0" + assert parsed_options.area == BBox(7.0, 45.0, 8.0, 48.0) # Polygon check must be enabled for all files HgtFilesProcessor_mock.return_value.process_files.assert_called_once_with( [("N45E007.hgt", True), ("N46E007.hgt", True), ("N47E007.hgt", True)], @@ -157,7 +156,7 @@ def test_main_manual_input_no_poly( HgtFilesProcessor_mock.assert_called_once() parsed_options: Configuration = HgtFilesProcessor_mock.call_args.args[3] # area must be properly computed from files names - assert parsed_options.area == "7.0:45.0:8.0:48.0" + assert parsed_options.area == BBox(7.0, 45.0, 8.0, 48.0) # Polygon check must NOT be enabled for any files HgtFilesProcessor_mock.return_value.process_files.assert_called_once_with( [("N45E007.hgt", False), ("N46E007.hgt", False), ("N47E007.hgt", False)],