From: Sarah Hoffmann Date: Wed, 19 Mar 2025 15:01:23 +0000 (+0100) Subject: Merge remote-tracking branch 'upstream/master' X-Git-Url: https://git.openstreetmap.org./nominatim.git/commitdiff_plain/be63329a69762a3d13487b060f3cbc7b815bdb48?hp=a846872f04106b18c1d0cf2b77c3236a88e959d7 Merge remote-tracking branch 'upstream/master' --- diff --git a/.flake8 b/.flake8 index 82d77ed3..cf87715a 100644 --- a/.flake8 +++ b/.flake8 @@ -6,3 +6,6 @@ extend-ignore = E711 per-file-ignores = __init__.py: F401 + test/python/utils/test_json_writer.py: E131 + test/python/conftest.py: E402 + test/bdd/*: F821 diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index f1d9f028..a8bf957f 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -100,7 +100,7 @@ jobs: run: ./venv/bin/pip install -U flake8 - name: Python linting - run: ../venv/bin/python -m flake8 src + run: ../venv/bin/python -m flake8 src test/python test/bdd working-directory: Nominatim - name: Install mypy and typechecking info diff --git a/Makefile b/Makefile index 9e914850..f35c9782 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ pytest: pytest test/python lint: - flake8 src + flake8 src test/python test/bdd bdd: cd test/bdd; behave -DREMOVE_TEMPLATE=1 diff --git a/lib-sql/functions/address_lookup.sql b/lib-sql/functions/address_lookup.sql index cba11dbf..b59b7656 100644 --- a/lib-sql/functions/address_lookup.sql +++ b/lib-sql/functions/address_lookup.sql @@ -47,7 +47,7 @@ BEGIN RETURN trim((avals(name))[array_length(avals(name), 1)]); END; $$ -LANGUAGE plpgsql IMMUTABLE; +LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; --housenumber only needed for tiger data @@ -84,7 +84,7 @@ BEGIN RETURN array_to_string(result,', '); END; $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; DROP TYPE IF EXISTS addressdata_place; CREATE TYPE addressdata_place AS ( @@ -331,4 +331,4 @@ BEGIN RETURN; END; $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; diff --git a/lib-sql/functions/importance.sql b/lib-sql/functions/importance.sql index 1de5899c..4993d70b 100644 --- a/lib-sql/functions/importance.sql +++ b/lib-sql/functions/importance.sql @@ -65,7 +65,7 @@ BEGIN RETURN NULL; END; $$ -LANGUAGE plpgsql IMMUTABLE; +LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; {% else %} @@ -78,7 +78,7 @@ SELECT convert_from(CAST(E'\\x' || array_to_string(ARRAY( FROM regexp_matches($1, '%[0-9a-f][0-9a-f]|.', 'gi') AS r(m) ), '') AS bytea), 'UTF8'); $$ -LANGUAGE SQL IMMUTABLE STRICT; +LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE; CREATE OR REPLACE FUNCTION catch_decode_url_part(p varchar) @@ -91,7 +91,7 @@ EXCEPTION WHEN others THEN return null; END; $$ -LANGUAGE plpgsql IMMUTABLE STRICT; +LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE; CREATE OR REPLACE FUNCTION get_wikipedia_match(extratags HSTORE, country_code varchar(2)) @@ -139,7 +139,7 @@ BEGIN RETURN NULL; END; $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; {% endif %} @@ -203,5 +203,5 @@ BEGIN RETURN result; END; $$ -LANGUAGE plpgsql; +LANGUAGE plpgsql PARALLEL SAFE; diff --git a/lib-sql/functions/interpolation.sql b/lib-sql/functions/interpolation.sql index 2fd21e8a..452ef7c3 100644 --- a/lib-sql/functions/interpolation.sql +++ b/lib-sql/functions/interpolation.sql @@ -34,7 +34,7 @@ BEGIN RETURN in_address; END; $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; @@ -70,7 +70,7 @@ BEGIN RETURN parent_place_id; END; $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION reinsert_interpolation(way_id BIGINT, addr HSTORE, diff --git a/lib-sql/functions/partition-functions.sql b/lib-sql/functions/partition-functions.sql index 595e4a61..d3c83615 100644 --- a/lib-sql/functions/partition-functions.sql +++ b/lib-sql/functions/partition-functions.sql @@ -58,7 +58,7 @@ BEGIN RAISE EXCEPTION 'Unknown partition %', in_partition; END $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION get_address_place(in_partition SMALLINT, feature GEOMETRY, @@ -87,7 +87,7 @@ BEGIN RAISE EXCEPTION 'Unknown partition %', in_partition; END; $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; create or replace function deleteLocationArea(in_partition INTEGER, in_place_id BIGINT, in_rank_search INTEGER) RETURNS BOOLEAN AS $$ @@ -172,7 +172,7 @@ BEGIN RAISE EXCEPTION 'Unknown partition %', in_partition; END $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION getNearestNamedPlacePlaceId(in_partition INTEGER, point GEOMETRY, @@ -202,7 +202,7 @@ BEGIN RAISE EXCEPTION 'Unknown partition %', in_partition; END $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; create or replace function insertSearchName( in_partition INTEGER, in_place_id BIGINT, in_name_vector INTEGER[], @@ -310,7 +310,7 @@ BEGIN RAISE EXCEPTION 'Unknown partition %', in_partition; END $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION getNearestParallelRoadFeature(in_partition INTEGER, line GEOMETRY) @@ -354,4 +354,4 @@ BEGIN RAISE EXCEPTION 'Unknown partition %', in_partition; END $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; diff --git a/lib-sql/functions/placex_triggers.sql b/lib-sql/functions/placex_triggers.sql index 9d0d73b5..8524ffc3 100644 --- a/lib-sql/functions/placex_triggers.sql +++ b/lib-sql/functions/placex_triggers.sql @@ -109,7 +109,7 @@ BEGIN RETURN result; END; $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION find_associated_street(poi_osm_type CHAR(1), @@ -200,7 +200,7 @@ BEGIN RETURN result; END; $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; -- Find the parent road of a POI. @@ -286,7 +286,7 @@ BEGIN RETURN parent_place_id; END; $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; -- Try to find a linked place for the given object. CREATE OR REPLACE FUNCTION find_linked_place(bnd placex) @@ -404,7 +404,7 @@ BEGIN RETURN NULL; END; $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION create_poi_search_terms(obj_place_id BIGINT, diff --git a/lib-sql/functions/ranking.sql b/lib-sql/functions/ranking.sql index 97a0cde3..c16ad1db 100644 --- a/lib-sql/functions/ranking.sql +++ b/lib-sql/functions/ranking.sql @@ -29,7 +29,7 @@ BEGIN RETURN 0.02; END; $$ -LANGUAGE plpgsql IMMUTABLE; +LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; -- Return an approximate update radius according to the search rank. @@ -60,7 +60,7 @@ BEGIN RETURN 0; END; $$ -LANGUAGE plpgsql IMMUTABLE; +LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; -- Compute a base address rank from the extent of the given geometry. -- @@ -107,7 +107,7 @@ BEGIN RETURN 23; END; $$ -LANGUAGE plpgsql IMMUTABLE; +LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; -- Guess a ranking for postcodes from country and postcode format. @@ -167,7 +167,7 @@ BEGIN END; $$ -LANGUAGE plpgsql IMMUTABLE; +LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; -- Get standard search and address rank for an object. @@ -236,7 +236,7 @@ BEGIN END IF; END; $$ -LANGUAGE plpgsql IMMUTABLE; +LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION get_addr_tag_rank(key TEXT, country TEXT, OUT from_rank SMALLINT, @@ -283,7 +283,7 @@ BEGIN END LOOP; END; $$ -LANGUAGE plpgsql IMMUTABLE; +LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION weigh_search(search_vector INT[], @@ -304,4 +304,4 @@ BEGIN RETURN def_weight; END; $$ -LANGUAGE plpgsql IMMUTABLE; +LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; diff --git a/lib-sql/functions/utils.sql b/lib-sql/functions/utils.sql index 534beb58..30f94080 100644 --- a/lib-sql/functions/utils.sql +++ b/lib-sql/functions/utils.sql @@ -24,7 +24,7 @@ BEGIN RETURN ST_PointOnSurface(place); END; $$ -LANGUAGE plpgsql IMMUTABLE; +LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION geometry_sector(partition INTEGER, place GEOMETRY) @@ -34,7 +34,7 @@ BEGIN RETURN (partition*1000000) + (500-ST_X(place)::INTEGER)*1000 + (500-ST_Y(place)::INTEGER); END; $$ -LANGUAGE plpgsql IMMUTABLE; +LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; @@ -60,7 +60,7 @@ BEGIN RETURN r; END; $$ -LANGUAGE plpgsql IMMUTABLE; +LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; -- Return the node members with a given label from a relation member list -- as a set. @@ -88,7 +88,7 @@ BEGIN RETURN; END; $$ -LANGUAGE plpgsql IMMUTABLE; +LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION get_rel_node_members(members JSONB, memberLabels TEXT[]) @@ -107,7 +107,7 @@ BEGIN RETURN; END; $$ -LANGUAGE plpgsql IMMUTABLE; +LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; -- Copy 'name' to or from the default language. @@ -136,7 +136,7 @@ BEGIN END IF; END; $$ -LANGUAGE plpgsql IMMUTABLE; +LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; -- Find the nearest artificial postcode for the given geometry. @@ -172,7 +172,7 @@ BEGIN RETURN outcode; END; $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION get_country_code(place geometry) @@ -233,7 +233,7 @@ BEGIN RETURN NULL; END; $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION get_country_language_code(search_country_code VARCHAR(2)) @@ -251,7 +251,7 @@ BEGIN RETURN NULL; END; $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION get_partition(in_country_code VARCHAR(10)) @@ -268,7 +268,7 @@ BEGIN RETURN 0; END; $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; -- Find the parent of an address with addr:street/addr:place tag. @@ -299,7 +299,7 @@ BEGIN RETURN parent_place_id; END; $$ -LANGUAGE plpgsql STABLE; +LANGUAGE plpgsql STABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION delete_location(OLD_place_id BIGINT) @@ -337,7 +337,7 @@ BEGIN ST_Project(geom::geography, radius, 3.9269908)::geometry)); END; $$ -LANGUAGE plpgsql IMMUTABLE; +LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION add_location(place_id BIGINT, country_code varchar(2), @@ -455,7 +455,7 @@ BEGIN RETURN; END; $$ -LANGUAGE plpgsql IMMUTABLE; +LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION split_geometry(geometry GEOMETRY) @@ -483,7 +483,7 @@ BEGIN RETURN; END; $$ -LANGUAGE plpgsql IMMUTABLE; +LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION simplify_large_polygons(geometry GEOMETRY) RETURNS GEOMETRY @@ -497,7 +497,7 @@ BEGIN RETURN geometry; END; $$ -LANGUAGE plpgsql IMMUTABLE; +LANGUAGE plpgsql IMMUTABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION place_force_delete(placeid BIGINT) diff --git a/lib-sql/tokenizer/icu_tokenizer.sql b/lib-sql/tokenizer/icu_tokenizer.sql index 04fcedcb..f0c30f1b 100644 --- a/lib-sql/tokenizer/icu_tokenizer.sql +++ b/lib-sql/tokenizer/icu_tokenizer.sql @@ -12,7 +12,7 @@ CREATE OR REPLACE FUNCTION token_get_name_search_tokens(info JSONB) RETURNS INTEGER[] AS $$ SELECT (info->>'names')::INTEGER[] -$$ LANGUAGE SQL IMMUTABLE STRICT; +$$ LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE; -- Get tokens for matching the place name against others. @@ -22,7 +22,7 @@ CREATE OR REPLACE FUNCTION token_get_name_match_tokens(info JSONB) RETURNS INTEGER[] AS $$ SELECT (info->>'names')::INTEGER[] -$$ LANGUAGE SQL IMMUTABLE STRICT; +$$ LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE; -- Return the housenumber tokens applicable for the place. @@ -30,7 +30,7 @@ CREATE OR REPLACE FUNCTION token_get_housenumber_search_tokens(info JSONB) RETURNS INTEGER[] AS $$ SELECT (info->>'hnr_tokens')::INTEGER[] -$$ LANGUAGE SQL IMMUTABLE STRICT; +$$ LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE; -- Return the housenumber in the form that it can be matched during search. @@ -38,77 +38,77 @@ CREATE OR REPLACE FUNCTION token_normalized_housenumber(info JSONB) RETURNS TEXT AS $$ SELECT info->>'hnr'; -$$ LANGUAGE SQL IMMUTABLE STRICT; +$$ LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE; CREATE OR REPLACE FUNCTION token_is_street_address(info JSONB) RETURNS BOOLEAN AS $$ SELECT info->>'street' is not null or info->>'place' is null; -$$ LANGUAGE SQL IMMUTABLE; +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION token_has_addr_street(info JSONB) RETURNS BOOLEAN AS $$ SELECT info->>'street' is not null and info->>'street' != '{}'; -$$ LANGUAGE SQL IMMUTABLE; +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION token_has_addr_place(info JSONB) RETURNS BOOLEAN AS $$ SELECT info->>'place' is not null; -$$ LANGUAGE SQL IMMUTABLE; +$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE; CREATE OR REPLACE FUNCTION token_matches_street(info JSONB, street_tokens INTEGER[]) RETURNS BOOLEAN AS $$ SELECT (info->>'street')::INTEGER[] && street_tokens -$$ LANGUAGE SQL IMMUTABLE STRICT; +$$ LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE; CREATE OR REPLACE FUNCTION token_matches_place(info JSONB, place_tokens INTEGER[]) RETURNS BOOLEAN AS $$ SELECT (info->>'place')::INTEGER[] <@ place_tokens -$$ LANGUAGE SQL IMMUTABLE STRICT; +$$ LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE; CREATE OR REPLACE FUNCTION token_addr_place_search_tokens(info JSONB) RETURNS INTEGER[] AS $$ SELECT (info->>'place')::INTEGER[] -$$ LANGUAGE SQL IMMUTABLE STRICT; +$$ LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE; CREATE OR REPLACE FUNCTION token_get_address_keys(info JSONB) RETURNS SETOF TEXT AS $$ SELECT * FROM jsonb_object_keys(info->'addr'); -$$ LANGUAGE SQL IMMUTABLE STRICT; +$$ LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE; CREATE OR REPLACE FUNCTION token_get_address_search_tokens(info JSONB, key TEXT) RETURNS INTEGER[] AS $$ SELECT (info->'addr'->>key)::INTEGER[]; -$$ LANGUAGE SQL IMMUTABLE STRICT; +$$ LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE; CREATE OR REPLACE FUNCTION token_matches_address(info JSONB, key TEXT, tokens INTEGER[]) RETURNS BOOLEAN AS $$ SELECT (info->'addr'->>key)::INTEGER[] <@ tokens; -$$ LANGUAGE SQL IMMUTABLE STRICT; +$$ LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE; CREATE OR REPLACE FUNCTION token_get_postcode(info JSONB) RETURNS TEXT AS $$ SELECT info->>'postcode'; -$$ LANGUAGE SQL IMMUTABLE STRICT; +$$ LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE; -- Return token info that should be saved permanently in the database. @@ -116,7 +116,7 @@ CREATE OR REPLACE FUNCTION token_strip_info(info JSONB) RETURNS JSONB AS $$ SELECT NULL::JSONB; -$$ LANGUAGE SQL IMMUTABLE STRICT; +$$ LANGUAGE SQL IMMUTABLE STRICT PARALLEL SAFE; --------------- private functions ---------------------------------------------- diff --git a/nominatim-cli.py b/nominatim-cli.py index 1f3c1210..7a1aadb8 100755 --- a/nominatim-cli.py +++ b/nominatim-cli.py @@ -3,7 +3,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Helper script for development to run nominatim from the source directory. @@ -15,4 +15,4 @@ sys.path.insert(1, str((Path(__file__) / '..' / 'src').resolve())) from nominatim_db import cli -exit(cli.nominatim(module_dir=None, osm2pgsql_path=None)) +exit(cli.nominatim()) diff --git a/packaging/nominatim-db/scripts/nominatim b/packaging/nominatim-db/scripts/nominatim index 184ab4c6..bc384b02 100755 --- a/packaging/nominatim-db/scripts/nominatim +++ b/packaging/nominatim-db/scripts/nominatim @@ -2,4 +2,4 @@ from nominatim_db import cli -exit(cli.nominatim(osm2pgsql_path=None)) +exit(cli.nominatim()) diff --git a/settings/address-levels.json b/settings/address-levels.json index 322ff707..1b1ef7e7 100644 --- a/settings/address-levels.json +++ b/settings/address-levels.json @@ -216,6 +216,14 @@ } } }, +{ "countries" : ["sa"], + "tags" : { + "place" : { + "province" : 12, + "municipality" : 18 + } + } +}, { "countries" : ["sk"], "tags" : { "boundary" : { diff --git a/src/nominatim_api/search/db_search_builder.py b/src/nominatim_api/search/db_search_builder.py index 4987f156..c63803d2 100644 --- a/src/nominatim_api/search/db_search_builder.py +++ b/src/nominatim_api/search/db_search_builder.py @@ -227,7 +227,7 @@ class SearchBuilder: # To catch remaining results, lookup by name and address # We only do this if there is a reasonable number of results expected. - exp_count = exp_count / (2**len(addr_tokens)) if addr_tokens else exp_count + exp_count /= 2**len(addr_tokens) if exp_count < 10000 and addr_count < 20000: penalty += 0.35 * max(1 if name_fulls else 0.1, 5 - len(name_partials) - len(addr_tokens)) diff --git a/src/nominatim_api/search/icu_tokenizer.py b/src/nominatim_api/search/icu_tokenizer.py index ecc2c1c7..cc5b6cf0 100644 --- a/src/nominatim_api/search/icu_tokenizer.py +++ b/src/nominatim_api/search/icu_tokenizer.py @@ -199,10 +199,12 @@ class ICUQueryAnalyzer(AbstractQueryAnalyzer): self.add_extra_tokens(query) for start, end, pc in self.postcode_parser.parse(query): + term = ' '.join(n.term_lookup for n in query.nodes[start + 1:end + 1]) query.add_token(qmod.TokenRange(start, end), qmod.TOKEN_POSTCODE, ICUToken(penalty=0.1, token=0, count=1, addr_count=1, - lookup_word=pc, word_token=pc, info=None)) + lookup_word=pc, word_token=term, + info=None)) self.rerank_tokens(query) log().table_dump('Word tokens', _dump_word_tokens(query)) @@ -273,10 +275,10 @@ class ICUQueryAnalyzer(AbstractQueryAnalyzer): """ for i, node, tlist in query.iter_token_lists(): if tlist.ttype == qmod.TOKEN_POSTCODE: + tlen = len(cast(ICUToken, tlist.tokens[0]).word_token) for repl in node.starting: if repl.end == tlist.end and repl.ttype != qmod.TOKEN_POSTCODE \ - and (repl.ttype != qmod.TOKEN_HOUSENUMBER - or len(tlist.tokens[0].lookup_word) > 4): + and (repl.ttype != qmod.TOKEN_HOUSENUMBER or tlen > 4): repl.add_penalty(0.39) elif (tlist.ttype == qmod.TOKEN_HOUSENUMBER and len(tlist.tokens[0].lookup_word) <= 3): diff --git a/src/nominatim_api/search/token_assignment.py b/src/nominatim_api/search/token_assignment.py index 3ca9385c..8d25aa8f 100644 --- a/src/nominatim_api/search/token_assignment.py +++ b/src/nominatim_api/search/token_assignment.py @@ -269,10 +269,9 @@ class _TokenSequence: #
, should give preference to address search if base.postcode.start == 0: penalty = self.penalty - self.direction = -1 # name searches are only possible backwards else: penalty = self.penalty + 0.1 - self.direction = 1 # name searches are only possible forwards + penalty += 0.1 * max(0, len(base.address) - 1) yield dataclasses.replace(base, penalty=penalty) def _get_assignments_address_forward(self, base: TokenAssignment, @@ -282,6 +281,11 @@ class _TokenSequence: """ first = base.address[0] + # The postcode must come after the name. + if base.postcode and base.postcode < first: + log().var_dump('skip forward', (base.postcode, first)) + return + log().comment('first word = name') yield dataclasses.replace(base, penalty=self.penalty, name=first, address=base.address[1:]) @@ -317,7 +321,12 @@ class _TokenSequence: """ last = base.address[-1] - if self.direction == -1 or len(base.address) > 1: + # The postcode must come before the name for backward direction. + if base.postcode and base.postcode > last: + log().var_dump('skip backward', (base.postcode, last)) + return + + if self.direction == -1 or len(base.address) > 1 or base.postcode: log().comment('last word = name') yield dataclasses.replace(base, penalty=self.penalty, name=last, address=base.address[:-1]) diff --git a/src/nominatim_api/sql/sqlalchemy_types/json.py b/src/nominatim_api/sql/sqlalchemy_types/json.py index 1c8f9f7b..f3ea9c6e 100644 --- a/src/nominatim_api/sql/sqlalchemy_types/json.py +++ b/src/nominatim_api/sql/sqlalchemy_types/json.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Common json type for different dialects. @@ -24,6 +24,6 @@ class Json(sa.types.TypeDecorator[Any]): def load_dialect_impl(self, dialect: SaDialect) -> sa.types.TypeEngine[Any]: if dialect.name == 'postgresql': - return JSONB(none_as_null=True) # type: ignore[no-untyped-call] + return JSONB(none_as_null=True) return sqlite_json(none_as_null=True) diff --git a/src/nominatim_api/types.py b/src/nominatim_api/types.py index 66a3c553..e58df478 100644 --- a/src/nominatim_api/types.py +++ b/src/nominatim_api/types.py @@ -144,7 +144,7 @@ class Point(NamedTuple): except ValueError as exc: raise UsageError('Point parameter needs to be numbers.') from exc - if x < -180.0 or x > 180.0 or y < -90.0 or y > 90.0: + if not -180 <= x <= 180 or not -90 <= y <= 90.0: raise UsageError('Point coordinates invalid.') return Point(x, y) diff --git a/src/nominatim_api/v1/classtypes.py b/src/nominatim_api/v1/classtypes.py index 7198b412..c2fe1453 100644 --- a/src/nominatim_api/v1/classtypes.py +++ b/src/nominatim_api/v1/classtypes.py @@ -25,8 +25,8 @@ def get_label_tag(category: Tuple[str, str], extratags: Optional[Mapping[str, st elif rank < 26 and extratags and 'linked_place' in extratags: label = extratags['linked_place'] elif category == ('boundary', 'administrative'): - label = ADMIN_LABELS.get((country or '', int(rank/2)))\ - or ADMIN_LABELS.get(('', int(rank/2)))\ + label = ADMIN_LABELS.get((country or '', rank // 2))\ + or ADMIN_LABELS.get(('', rank // 2))\ or 'Administrative' elif category[1] == 'postal_code': label = 'postcode' diff --git a/src/nominatim_db/cli.py b/src/nominatim_db/cli.py index f5f74208..8d4585c6 100644 --- a/src/nominatim_db/cli.py +++ b/src/nominatim_db/cli.py @@ -2,16 +2,15 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Command-line interface to the Nominatim functions for import, update, database administration and querying. """ -from typing import Optional, Any +from typing import Optional, List, Mapping import importlib import logging -import os import sys import argparse import asyncio @@ -81,13 +80,14 @@ class CommandlineParser: parser.set_defaults(command=cmd) cmd.add_args(parser) - def run(self, **kwargs: Any) -> int: + def run(self, cli_args: Optional[List[str]], + environ: Optional[Mapping[str, str]]) -> int: """ Parse the command line arguments of the program and execute the appropriate subcommand. """ args = NominatimArgs() try: - self.parser.parse_args(args=kwargs.get('cli_args'), namespace=args) + self.parser.parse_args(args=cli_args, namespace=args) except SystemExit: return 1 @@ -101,23 +101,19 @@ class CommandlineParser: args.project_dir = Path(args.project_dir).resolve() - if 'cli_args' not in kwargs: + if cli_args is None: logging.basicConfig(stream=sys.stderr, format='%(asctime)s: %(message)s', datefmt='%Y-%m-%d %H:%M:%S', level=max(4 - args.verbose, 1) * 10) - args.config = Configuration(args.project_dir, - environ=kwargs.get('environ', os.environ)) - args.config.set_libdirs(osm2pgsql=kwargs['osm2pgsql_path']) + args.config = Configuration(args.project_dir, environ=environ) log = logging.getLogger() log.warning('Using project directory: %s', str(args.project_dir)) try: - ret = args.command.run(args) - - return ret + return args.command.run(args) except UsageError as exception: if log.isEnabledFor(logging.DEBUG): raise # use Python's exception printing @@ -233,9 +229,16 @@ def get_set_parser() -> CommandlineParser: return parser -def nominatim(**kwargs: Any) -> int: +def nominatim(cli_args: Optional[List[str]] = None, + environ: Optional[Mapping[str, str]] = None) -> int: """\ Command-line tools for importing, updating, administrating and querying the Nominatim database. + + 'cli_args' is a list of parameters for the command to run. If not given, + sys.args will be used. + + 'environ' is the dictionary of environment variables containing the + Nominatim configuration. When None, the os.environ is inherited. """ - return get_set_parser().run(**kwargs) + return get_set_parser().run(cli_args=cli_args, environ=environ) diff --git a/src/nominatim_db/clicmd/args.py b/src/nominatim_db/clicmd/args.py index a8ff210a..45df9b7c 100644 --- a/src/nominatim_db/clicmd/args.py +++ b/src/nominatim_db/clicmd/args.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Provides custom functions over command-line arguments. @@ -186,7 +186,7 @@ class NominatimArgs: from the command line arguments. The resulting dict can be further customized and then used in `run_osm2pgsql()`. """ - return dict(osm2pgsql=self.config.OSM2PGSQL_BINARY or self.config.lib_dir.osm2pgsql, + return dict(osm2pgsql=self.config.OSM2PGSQL_BINARY, osm2pgsql_cache=self.osm2pgsql_cache or default_cache, osm2pgsql_style=self.config.get_import_style_file(), osm2pgsql_style_path=self.config.lib_dir.lua, diff --git a/src/nominatim_db/config.py b/src/nominatim_db/config.py index ae59cfd3..ba804122 100644 --- a/src/nominatim_db/config.py +++ b/src/nominatim_db/config.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Nominatim configuration accessor. @@ -73,7 +73,6 @@ class Configuration: self.project_dir = None class _LibDirs: - osm2pgsql: Path sql = paths.SQLLIB_DIR lua = paths.LUALIB_DIR data = paths.DATA_DIR diff --git a/src/nominatim_db/db/connection.py b/src/nominatim_db/db/connection.py index e960a3fa..cc843ab6 100644 --- a/src/nominatim_db/db/connection.py +++ b/src/nominatim_db/db/connection.py @@ -102,10 +102,10 @@ def server_version_tuple(conn: Connection) -> Tuple[int, int]: Converts correctly for pre-10 and post-10 PostgreSQL versions. """ version = conn.info.server_version - if version < 100000: - return (int(version / 10000), int((version % 10000) / 100)) - - return (int(version / 10000), version % 10000) + major, minor = divmod(version, 10000) + if major < 10: + minor //= 100 + return major, minor def postgis_version_tuple(conn: Connection) -> Tuple[int, int]: diff --git a/src/nominatim_db/indexer/progress.py b/src/nominatim_db/indexer/progress.py index 66c35f06..b99296e5 100644 --- a/src/nominatim_db/indexer/progress.py +++ b/src/nominatim_db/indexer/progress.py @@ -50,8 +50,8 @@ class ProgressLogger: places_per_sec = self.done_places / done_time eta = (self.total_places - self.done_places) / places_per_sec - LOG.warning("Done %d in %d @ %.3f per second - %s ETA (seconds): %.2f", - self.done_places, int(done_time), + LOG.warning("Done %d in %.0f @ %.3f per second - %s ETA (seconds): %.2f", + self.done_places, done_time, places_per_sec, self.name, eta) self.next_info += int(places_per_sec) * self.log_interval @@ -68,8 +68,8 @@ class ProgressLogger: diff_seconds = (rank_end_time - self.rank_start_time).total_seconds() places_per_sec = self.done_places / diff_seconds - LOG.warning("Done %d/%d in %d @ %.3f per second - FINISHED %s\n", - self.done_places, self.total_places, int(diff_seconds), + LOG.warning("Done %d/%d in %.0f @ %.3f per second - FINISHED %s\n", + self.done_places, self.total_places, diff_seconds, places_per_sec, self.name) return self.done_places diff --git a/src/nominatim_db/tools/exec_utils.py b/src/nominatim_db/tools/exec_utils.py index 7629e2a2..2d048bcb 100644 --- a/src/nominatim_db/tools/exec_utils.py +++ b/src/nominatim_db/tools/exec_utils.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Helper functions for executing external programs. @@ -85,7 +85,7 @@ def _mk_tablespace_options(ttype: str, options: Mapping[str, Any]) -> List[str]: def _find_osm2pgsql_cmd(cmdline: Optional[str]) -> str: - if cmdline is not None: + if cmdline: return cmdline in_path = shutil.which('osm2pgsql') diff --git a/src/nominatim_db/tools/tiger_data.py b/src/nominatim_db/tools/tiger_data.py index 85110ae5..7b865570 100644 --- a/src/nominatim_db/tools/tiger_data.py +++ b/src/nominatim_db/tools/tiger_data.py @@ -108,8 +108,7 @@ async def add_tiger_data(data_dir: str, config: Configuration, threads: int, async with QueryPool(dsn, place_threads, autocommit=True) as pool: with tokenizer.name_analyzer() as analyzer: - lines = 0 - for row in tar: + for lineno, row in enumerate(tar, 1): try: address = dict(street=row['street'], postcode=row['postcode']) args = ('SRID=4326;' + row['geometry'], @@ -124,10 +123,8 @@ async def add_tiger_data(data_dir: str, config: Configuration, threads: int, %s::INT, %s::TEXT, %s::JSONB, %s::TEXT)""", args) - lines += 1 - if lines == 1000: + if not lineno % 1000: print('.', end='', flush=True) - lines = 0 print('', flush=True) diff --git a/src/nominatim_db/utils/centroid.py b/src/nominatim_db/utils/centroid.py index a45d958b..02512336 100644 --- a/src/nominatim_db/utils/centroid.py +++ b/src/nominatim_db/utils/centroid.py @@ -30,8 +30,8 @@ class PointsCentroid: if self.count == 0: raise ValueError("No points available for centroid.") - return (float(self.sum_x/self.count)/10000000, - float(self.sum_y/self.count)/10000000) + return (self.sum_x / self.count / 10_000_000, + self.sum_y / self.count / 10_000_000) def __len__(self) -> int: return self.count @@ -40,8 +40,8 @@ class PointsCentroid: if isinstance(other, Collection) and len(other) == 2: if all(isinstance(p, (float, int)) for p in other): x, y = other - self.sum_x += int(x * 10000000) - self.sum_y += int(y * 10000000) + self.sum_x += int(x * 10_000_000) + self.sum_y += int(y * 10_000_000) self.count += 1 return self diff --git a/test/bdd/environment.py b/test/bdd/environment.py index 7535c508..bedbe8d2 100644 --- a/test/bdd/environment.py +++ b/test/bdd/environment.py @@ -2,43 +2,45 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. from pathlib import Path import sys -from behave import * +from behave import * # noqa sys.path.insert(1, str(Path(__file__, '..', '..', '..', 'src').resolve())) -from steps.geometry_factory import GeometryFactory -from steps.nominatim_environment import NominatimEnvironment +from steps.geometry_factory import GeometryFactory # noqa: E402 +from steps.nominatim_environment import NominatimEnvironment # noqa: E402 TEST_BASE_DIR = Path(__file__, '..', '..').resolve() userconfig = { - 'REMOVE_TEMPLATE' : False, - 'KEEP_TEST_DB' : False, - 'DB_HOST' : None, - 'DB_PORT' : None, - 'DB_USER' : None, - 'DB_PASS' : None, - 'TEMPLATE_DB' : 'test_template_nominatim', - 'TEST_DB' : 'test_nominatim', - 'API_TEST_DB' : 'test_api_nominatim', - 'API_TEST_FILE' : TEST_BASE_DIR / 'testdb' / 'apidb-test-data.pbf', - 'TOKENIZER' : None, # Test with a custom tokenizer - 'STYLE' : 'extratags', + 'REMOVE_TEMPLATE': False, + 'KEEP_TEST_DB': False, + 'DB_HOST': None, + 'DB_PORT': None, + 'DB_USER': None, + 'DB_PASS': None, + 'TEMPLATE_DB': 'test_template_nominatim', + 'TEST_DB': 'test_nominatim', + 'API_TEST_DB': 'test_api_nominatim', + 'API_TEST_FILE': TEST_BASE_DIR / 'testdb' / 'apidb-test-data.pbf', + 'TOKENIZER': None, # Test with a custom tokenizer + 'STYLE': 'extratags', 'API_ENGINE': 'falcon' } -use_step_matcher("re") + +use_step_matcher("re") # noqa: F405 + def before_all(context): # logging setup context.config.setup_logging() # set up -D options - for k,v in userconfig.items(): + for k, v in userconfig.items(): context.config.userdata.setdefault(k, v) # Nominatim test setup context.nominatim = NominatimEnvironment(context.config.userdata) @@ -46,7 +48,7 @@ def before_all(context): def before_scenario(context, scenario): - if not 'SQLITE' in context.tags \ + if 'SQLITE' not in context.tags \ and context.config.userdata['API_TEST_DB'].startswith('sqlite:'): context.scenario.skip("Not usable with Sqlite database.") elif 'DB' in context.tags: @@ -56,6 +58,7 @@ def before_scenario(context, scenario): elif 'UNKNOWNDB' in context.tags: context.nominatim.setup_unknown_db() + def after_scenario(context, scenario): if 'DB' in context.tags: context.nominatim.teardown_db(context) diff --git a/test/bdd/steps/check_functions.py b/test/bdd/steps/check_functions.py index 49676896..df9e6f35 100644 --- a/test/bdd/steps/check_functions.py +++ b/test/bdd/steps/check_functions.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2023 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Collection of assertion functions used for the steps. @@ -11,20 +11,10 @@ import json import math import re -class Almost: - """ Compares a float value with a certain jitter. - """ - def __init__(self, value, offset=0.00001): - self.value = value - self.offset = offset - - def __eq__(self, other): - return abs(other - self.value) < self.offset - -OSM_TYPE = {'N' : 'node', 'W' : 'way', 'R' : 'relation', - 'n' : 'node', 'w' : 'way', 'r' : 'relation', - 'node' : 'n', 'way' : 'w', 'relation' : 'r'} +OSM_TYPE = {'N': 'node', 'W': 'way', 'R': 'relation', + 'n': 'node', 'w': 'way', 'r': 'relation', + 'node': 'n', 'way': 'w', 'relation': 'r'} class OsmType: @@ -34,11 +24,9 @@ class OsmType: def __init__(self, value): self.value = value - def __eq__(self, other): return other == self.value or other == OSM_TYPE[self.value] - def __str__(self): return f"{self.value} or {OSM_TYPE[self.value]}" @@ -92,7 +80,6 @@ class Bbox: return str(self.coord) - def check_for_attributes(obj, attrs, presence='present'): """ Check that the object has the given attributes. 'attrs' is a string with a comma-separated list of attributes. If 'presence' @@ -110,4 +97,3 @@ def check_for_attributes(obj, attrs, presence='present'): else: assert attr in obj, \ f"No attribute '{attr}'. Full response:\n{_dump_json()}" - diff --git a/test/bdd/steps/geometry_alias.py b/test/bdd/steps/geometry_alias.py index a9b4ec8c..dbec5201 100644 --- a/test/bdd/steps/geometry_alias.py +++ b/test/bdd/steps/geometry_alias.py @@ -2,261 +2,261 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2022 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Collection of aliases for various world coordinates. """ ALIASES = { -# Country aliases -'AD': (1.58972, 42.54241), -'AE': (54.61589, 24.82431), -'AF': (65.90264, 34.84708), -'AG': (-61.72430, 17.069), -'AI': (-63.10571, 18.25461), -'AL': (19.84941, 40.21232), -'AM': (44.64229, 40.37821), -'AO': (16.21924, -12.77014), -'AQ': (44.99999, -75.65695), -'AR': (-61.10759, -34.37615), -'AS': (-170.68470, -14.29307), -'AT': (14.25747, 47.36542), -'AU': (138.23155, -23.72068), -'AW': (-69.98255, 12.555), -'AX': (19.91839, 59.81682), -'AZ': (48.38555, 40.61639), -'BA': (17.18514, 44.25582), -'BB': (-59.53342, 13.19), -'BD': (89.75989, 24.34205), -'BE': (4.90078, 50.34682), -'BF': (-0.56743, 11.90471), -'BG': (24.80616, 43.09859), -'BH': (50.52032, 25.94685), -'BI': (29.54561, -2.99057), -'BJ': (2.70062, 10.02792), -'BL': (-62.79349, 17.907), -'BM': (-64.77406, 32.30199), -'BN': (114.52196, 4.28638), -'BO': (-62.02473, -17.77723), -'BQ': (-63.14322, 17.566), -'BR': (-45.77065, -9.58685), -'BS': (-77.60916, 23.8745), -'BT': (90.01350, 27.28137), -'BV': (3.35744, -54.4215), -'BW': (23.51505, -23.48391), -'BY': (26.77259, 53.15885), -'BZ': (-88.63489, 16.33951), -'CA': (-107.74817, 67.12612), -'CC': (96.84420, -12.01734), -'CD': (24.09544, -1.67713), -'CF': (22.58701, 5.98438), -'CG': (15.78875, 0.40388), -'CH': (7.65705, 46.57446), -'CI': (-6.31190, 6.62783), -'CK': (-159.77835, -21.23349), -'CL': (-70.41790, -53.77189), -'CM': (13.26022, 5.94519), -'CN': (96.44285, 38.04260), -'CO': (-72.52951, 2.45174), -'CR': (-83.83314, 9.93514), -'CU': (-80.81673, 21.88852), -'CV': (-24.50810, 14.929), -'CW': (-68.96409, 12.1845), -'CX': (105.62411, -10.48417), -'CY': (32.95922, 35.37010), -'CZ': (16.32098, 49.50692), -'DE': (9.30716, 50.21289), -'DJ': (42.96904, 11.41542), -'DK': (9.18490, 55.98916), -'DM': (-61.00358, 15.65470), -'DO': (-69.62855, 18.58841), -'DZ': (4.24749, 25.79721), -'EC': (-77.45831, -0.98284), -'EE': (23.94288, 58.43952), -'EG': (28.95293, 28.17718), -'EH': (-13.69031, 25.01241), -'ER': (39.01223, 14.96033), -'ES': (-2.59110, 38.79354), -'ET': (38.61697, 7.71399), -'FI': (26.89798, 63.56194), -'FJ': (177.91853, -17.74237), -'FK': (-58.99044, -51.34509), -'FM': (151.95358, 8.5045), -'FO': (-6.60483, 62.10000), -'FR': (0.28410, 47.51045), -'GA': (10.81070, -0.07429), -'GB': (-0.92823, 52.01618), -'GD': (-61.64524, 12.191), -'GE': (44.16664, 42.00385), -'GF': (-53.46524, 3.56188), -'GG': (-2.50580, 49.58543), -'GH': (-0.46348, 7.16051), -'GI': (-5.32053, 36.11066), -'GL': (-33.85511, 74.66355), -'GM': (-16.40960, 13.25), -'GN': (-13.83940, 10.96291), -'GP': (-61.68712, 16.23049), -'GQ': (10.23973, 1.43119), -'GR': (23.17850, 39.06206), -'GS': (-36.49430, -54.43067), -'GT': (-90.74368, 15.20428), -'GU': (144.73362, 13.44413), -'GW': (-14.83525, 11.92486), -'GY': (-58.45167, 5.73698), -'HK': (114.18577, 22.34923), -'HM': (73.68230, -53.22105), -'HN': (-86.95414, 15.23820), -'HR': (17.49966, 45.52689), -'HT': (-73.51925, 18.32492), -'HU': (20.35362, 47.51721), -'ID': (123.34505, -0.83791), -'IE': (-9.00520, 52.87725), -'IL': (35.46314, 32.86165), -'IM': (-4.86740, 54.023), -'IN': (88.67620, 27.86155), -'IO': (71.42743, -6.14349), -'IQ': (42.58109, 34.26103), -'IR': (56.09355, 30.46751), -'IS': (-17.51785, 64.71687), -'IT': (10.42639, 44.87904), -'JE': (-2.19261, 49.12458), -'JM': (-76.84020, 18.3935), -'JO': (36.55552, 30.75741), -'JP': (138.72531, 35.92099), -'KE': (36.90602, 1.08512), -'KG': (76.15571, 41.66497), -'KH': (104.31901, 12.95555), -'KI': (173.63353, 0.139), -'KM': (44.31474, -12.241), -'KN': (-62.69379, 17.2555), -'KP': (126.65575, 39.64575), -'KR': (127.27740, 36.41388), -'KW': (47.30684, 29.69180), -'KY': (-81.07455, 19.29949), -'KZ': (72.00811, 49.88855), -'LA': (102.44391, 19.81609), -'LB': (35.48464, 33.41766), -'LC': (-60.97894, 13.891), -'LI': (9.54693, 47.15934), -'LK': (80.38520, 8.41649), -'LR': (-11.16960, 4.04122), -'LS': (28.66984, -29.94538), -'LT': (24.51735, 55.49293), -'LU': (6.08649, 49.81533), -'LV': (23.51033, 56.67144), -'LY': (15.36841, 28.12177), -'MA': (-4.03061, 33.21696), -'MC': (7.47743, 43.62917), -'MD': (29.61725, 46.66517), -'ME': (19.72291, 43.02441), -'MF': (-63.06666, 18.08102), -'MG': (45.86378, -20.50245), -'MH': (171.94982, 5.983), -'MK': (21.42108, 41.08980), -'ML': (-1.93310, 16.46993), -'MM': (95.54624, 21.09620), -'MN': (99.81138, 48.18615), -'MO': (113.56441, 22.16209), -'MP': (145.21345, 14.14902), -'MQ': (-60.81128, 14.43706), -'MR': (-9.42324, 22.59251), -'MS': (-62.19455, 16.745), -'MT': (14.38363, 35.94467), -'MU': (57.55121, -20.41), -'MV': (73.39292, 4.19375), -'MW': (33.95722, -12.28218), -'MX': (-105.89221, 25.86826), -'MY': (112.71154, 2.10098), -'MZ': (37.58689, -13.72682), -'NA': (16.68569, -21.46572), -'NC': (164.95322, -20.38889), -'NE': (10.06041, 19.08273), -'NF': (167.95718, -29.0645), -'NG': (10.17781, 10.17804), -'NI': (-85.87974, 13.21715), -'NL': (-68.57062, 12.041), -'NO': (23.11556, 70.09934), -'NP': (83.36259, 28.13107), -'NR': (166.93479, -0.5275), -'NU': (-169.84873, -19.05305), -'NZ': (167.97209, -45.13056), -'OM': (56.86055, 20.47413), -'PA': (-79.40160, 8.80656), -'PE': (-78.66540, -7.54711), -'PF': (-145.05719, -16.70862), -'PG': (146.64600, -7.37427), -'PH': (121.48359, 15.09965), -'PK': (72.11347, 31.14629), -'PL': (17.88136, 52.77182), -'PM': (-56.19515, 46.78324), -'PN': (-130.10642, -25.06955), -'PR': (-65.88755, 18.37169), -'PS': (35.39801, 32.24773), -'PT': (-8.45743, 40.11154), -'PW': (134.49645, 7.3245), -'PY': (-59.51787, -22.41281), -'QA': (51.49903, 24.99816), -'RE': (55.77345, -21.36388), -'RO': (26.37632, 45.36120), -'RS': (20.40371, 44.56413), -'RU': (116.44060, 59.06780), -'RW': (29.57882, -1.62404), -'SA': (47.73169, 22.43790), -'SB': (164.63894, -10.23606), -'SC': (46.36566, -9.454), -'SD': (28.14720, 14.56423), -'SE': (15.68667, 60.35568), -'SG': (103.84187, 1.304), -'SH': (-12.28155, -37.11546), -'SI': (14.04738, 46.39085), -'SJ': (15.27552, 79.23365), -'SK': (20.41603, 48.86970), -'SL': (-11.47773, 8.78156), -'SM': (12.46062, 43.94279), -'SN': (-15.37111, 14.99477), -'SO': (46.93383, 9.34094), -'SR': (-55.42864, 4.56985), -'SS': (28.13573, 8.50933), -'ST': (6.61025, 0.2215), -'SV': (-89.36665, 13.43072), -'SX': (-63.15393, 17.9345), -'SY': (38.15513, 35.34221), -'SZ': (31.78263, -26.14244), -'TC': (-71.32554, 21.35), -'TD': (17.42092, 13.46223), -'TF': (137.5, -67.5), -'TG': (1.06983, 7.87677), -'TH': (102.00877, 16.42310), -'TJ': (71.91349, 39.01527), -'TK': (-171.82603, -9.20990), -'TL': (126.22520, -8.72636), -'TM': (57.71603, 39.92534), -'TN': (9.04958, 34.84199), -'TO': (-176.99320, -23.11104), -'TR': (32.82002, 39.86350), -'TT': (-60.70793, 11.1385), -'TV': (178.77499, -9.41685), -'TW': (120.30074, 23.17002), -'TZ': (33.53892, -5.01840), -'UA': (33.44335, 49.30619), -'UG': (32.96523, 2.08584), -'UM': (-169.50993, 16.74605), -'US': (-116.39535, 40.71379), -'UY': (-56.46505, -33.62658), -'UZ': (61.35529, 42.96107), -'VA': (12.33197, 42.04931), -'VC': (-61.09905, 13.316), -'VE': (-64.88323, 7.69849), -'VG': (-64.62479, 18.419), -'VI': (-64.88950, 18.32263), -'VN': (104.20179, 10.27644), -'VU': (167.31919, -15.88687), -'WF': (-176.20781, -13.28535), -'WS': (-172.10966, -13.85093), -'YE': (45.94562, 16.16338), -'YT': (44.93774, -12.60882), -'ZA': (23.19488, -30.43276), -'ZM': (26.38618, -14.39966), -'ZW': (30.12419, -19.86907) -} + # Country aliases + 'AD': (1.58972, 42.54241), + 'AE': (54.61589, 24.82431), + 'AF': (65.90264, 34.84708), + 'AG': (-61.72430, 17.069), + 'AI': (-63.10571, 18.25461), + 'AL': (19.84941, 40.21232), + 'AM': (44.64229, 40.37821), + 'AO': (16.21924, -12.77014), + 'AQ': (44.99999, -75.65695), + 'AR': (-61.10759, -34.37615), + 'AS': (-170.68470, -14.29307), + 'AT': (14.25747, 47.36542), + 'AU': (138.23155, -23.72068), + 'AW': (-69.98255, 12.555), + 'AX': (19.91839, 59.81682), + 'AZ': (48.38555, 40.61639), + 'BA': (17.18514, 44.25582), + 'BB': (-59.53342, 13.19), + 'BD': (89.75989, 24.34205), + 'BE': (4.90078, 50.34682), + 'BF': (-0.56743, 11.90471), + 'BG': (24.80616, 43.09859), + 'BH': (50.52032, 25.94685), + 'BI': (29.54561, -2.99057), + 'BJ': (2.70062, 10.02792), + 'BL': (-62.79349, 17.907), + 'BM': (-64.77406, 32.30199), + 'BN': (114.52196, 4.28638), + 'BO': (-62.02473, -17.77723), + 'BQ': (-63.14322, 17.566), + 'BR': (-45.77065, -9.58685), + 'BS': (-77.60916, 23.8745), + 'BT': (90.01350, 27.28137), + 'BV': (3.35744, -54.4215), + 'BW': (23.51505, -23.48391), + 'BY': (26.77259, 53.15885), + 'BZ': (-88.63489, 16.33951), + 'CA': (-107.74817, 67.12612), + 'CC': (96.84420, -12.01734), + 'CD': (24.09544, -1.67713), + 'CF': (22.58701, 5.98438), + 'CG': (15.78875, 0.40388), + 'CH': (7.65705, 46.57446), + 'CI': (-6.31190, 6.62783), + 'CK': (-159.77835, -21.23349), + 'CL': (-70.41790, -53.77189), + 'CM': (13.26022, 5.94519), + 'CN': (96.44285, 38.04260), + 'CO': (-72.52951, 2.45174), + 'CR': (-83.83314, 9.93514), + 'CU': (-80.81673, 21.88852), + 'CV': (-24.50810, 14.929), + 'CW': (-68.96409, 12.1845), + 'CX': (105.62411, -10.48417), + 'CY': (32.95922, 35.37010), + 'CZ': (16.32098, 49.50692), + 'DE': (9.30716, 50.21289), + 'DJ': (42.96904, 11.41542), + 'DK': (9.18490, 55.98916), + 'DM': (-61.00358, 15.65470), + 'DO': (-69.62855, 18.58841), + 'DZ': (4.24749, 25.79721), + 'EC': (-77.45831, -0.98284), + 'EE': (23.94288, 58.43952), + 'EG': (28.95293, 28.17718), + 'EH': (-13.69031, 25.01241), + 'ER': (39.01223, 14.96033), + 'ES': (-2.59110, 38.79354), + 'ET': (38.61697, 7.71399), + 'FI': (26.89798, 63.56194), + 'FJ': (177.91853, -17.74237), + 'FK': (-58.99044, -51.34509), + 'FM': (151.95358, 8.5045), + 'FO': (-6.60483, 62.10000), + 'FR': (0.28410, 47.51045), + 'GA': (10.81070, -0.07429), + 'GB': (-0.92823, 52.01618), + 'GD': (-61.64524, 12.191), + 'GE': (44.16664, 42.00385), + 'GF': (-53.46524, 3.56188), + 'GG': (-2.50580, 49.58543), + 'GH': (-0.46348, 7.16051), + 'GI': (-5.32053, 36.11066), + 'GL': (-33.85511, 74.66355), + 'GM': (-16.40960, 13.25), + 'GN': (-13.83940, 10.96291), + 'GP': (-61.68712, 16.23049), + 'GQ': (10.23973, 1.43119), + 'GR': (23.17850, 39.06206), + 'GS': (-36.49430, -54.43067), + 'GT': (-90.74368, 15.20428), + 'GU': (144.73362, 13.44413), + 'GW': (-14.83525, 11.92486), + 'GY': (-58.45167, 5.73698), + 'HK': (114.18577, 22.34923), + 'HM': (73.68230, -53.22105), + 'HN': (-86.95414, 15.23820), + 'HR': (17.49966, 45.52689), + 'HT': (-73.51925, 18.32492), + 'HU': (20.35362, 47.51721), + 'ID': (123.34505, -0.83791), + 'IE': (-9.00520, 52.87725), + 'IL': (35.46314, 32.86165), + 'IM': (-4.86740, 54.023), + 'IN': (88.67620, 27.86155), + 'IO': (71.42743, -6.14349), + 'IQ': (42.58109, 34.26103), + 'IR': (56.09355, 30.46751), + 'IS': (-17.51785, 64.71687), + 'IT': (10.42639, 44.87904), + 'JE': (-2.19261, 49.12458), + 'JM': (-76.84020, 18.3935), + 'JO': (36.55552, 30.75741), + 'JP': (138.72531, 35.92099), + 'KE': (36.90602, 1.08512), + 'KG': (76.15571, 41.66497), + 'KH': (104.31901, 12.95555), + 'KI': (173.63353, 0.139), + 'KM': (44.31474, -12.241), + 'KN': (-62.69379, 17.2555), + 'KP': (126.65575, 39.64575), + 'KR': (127.27740, 36.41388), + 'KW': (47.30684, 29.69180), + 'KY': (-81.07455, 19.29949), + 'KZ': (72.00811, 49.88855), + 'LA': (102.44391, 19.81609), + 'LB': (35.48464, 33.41766), + 'LC': (-60.97894, 13.891), + 'LI': (9.54693, 47.15934), + 'LK': (80.38520, 8.41649), + 'LR': (-11.16960, 4.04122), + 'LS': (28.66984, -29.94538), + 'LT': (24.51735, 55.49293), + 'LU': (6.08649, 49.81533), + 'LV': (23.51033, 56.67144), + 'LY': (15.36841, 28.12177), + 'MA': (-4.03061, 33.21696), + 'MC': (7.47743, 43.62917), + 'MD': (29.61725, 46.66517), + 'ME': (19.72291, 43.02441), + 'MF': (-63.06666, 18.08102), + 'MG': (45.86378, -20.50245), + 'MH': (171.94982, 5.983), + 'MK': (21.42108, 41.08980), + 'ML': (-1.93310, 16.46993), + 'MM': (95.54624, 21.09620), + 'MN': (99.81138, 48.18615), + 'MO': (113.56441, 22.16209), + 'MP': (145.21345, 14.14902), + 'MQ': (-60.81128, 14.43706), + 'MR': (-9.42324, 22.59251), + 'MS': (-62.19455, 16.745), + 'MT': (14.38363, 35.94467), + 'MU': (57.55121, -20.41), + 'MV': (73.39292, 4.19375), + 'MW': (33.95722, -12.28218), + 'MX': (-105.89221, 25.86826), + 'MY': (112.71154, 2.10098), + 'MZ': (37.58689, -13.72682), + 'NA': (16.68569, -21.46572), + 'NC': (164.95322, -20.38889), + 'NE': (10.06041, 19.08273), + 'NF': (167.95718, -29.0645), + 'NG': (10.17781, 10.17804), + 'NI': (-85.87974, 13.21715), + 'NL': (-68.57062, 12.041), + 'NO': (23.11556, 70.09934), + 'NP': (83.36259, 28.13107), + 'NR': (166.93479, -0.5275), + 'NU': (-169.84873, -19.05305), + 'NZ': (167.97209, -45.13056), + 'OM': (56.86055, 20.47413), + 'PA': (-79.40160, 8.80656), + 'PE': (-78.66540, -7.54711), + 'PF': (-145.05719, -16.70862), + 'PG': (146.64600, -7.37427), + 'PH': (121.48359, 15.09965), + 'PK': (72.11347, 31.14629), + 'PL': (17.88136, 52.77182), + 'PM': (-56.19515, 46.78324), + 'PN': (-130.10642, -25.06955), + 'PR': (-65.88755, 18.37169), + 'PS': (35.39801, 32.24773), + 'PT': (-8.45743, 40.11154), + 'PW': (134.49645, 7.3245), + 'PY': (-59.51787, -22.41281), + 'QA': (51.49903, 24.99816), + 'RE': (55.77345, -21.36388), + 'RO': (26.37632, 45.36120), + 'RS': (20.40371, 44.56413), + 'RU': (116.44060, 59.06780), + 'RW': (29.57882, -1.62404), + 'SA': (47.73169, 22.43790), + 'SB': (164.63894, -10.23606), + 'SC': (46.36566, -9.454), + 'SD': (28.14720, 14.56423), + 'SE': (15.68667, 60.35568), + 'SG': (103.84187, 1.304), + 'SH': (-12.28155, -37.11546), + 'SI': (14.04738, 46.39085), + 'SJ': (15.27552, 79.23365), + 'SK': (20.41603, 48.86970), + 'SL': (-11.47773, 8.78156), + 'SM': (12.46062, 43.94279), + 'SN': (-15.37111, 14.99477), + 'SO': (46.93383, 9.34094), + 'SR': (-55.42864, 4.56985), + 'SS': (28.13573, 8.50933), + 'ST': (6.61025, 0.2215), + 'SV': (-89.36665, 13.43072), + 'SX': (-63.15393, 17.9345), + 'SY': (38.15513, 35.34221), + 'SZ': (31.78263, -26.14244), + 'TC': (-71.32554, 21.35), + 'TD': (17.42092, 13.46223), + 'TF': (137.5, -67.5), + 'TG': (1.06983, 7.87677), + 'TH': (102.00877, 16.42310), + 'TJ': (71.91349, 39.01527), + 'TK': (-171.82603, -9.20990), + 'TL': (126.22520, -8.72636), + 'TM': (57.71603, 39.92534), + 'TN': (9.04958, 34.84199), + 'TO': (-176.99320, -23.11104), + 'TR': (32.82002, 39.86350), + 'TT': (-60.70793, 11.1385), + 'TV': (178.77499, -9.41685), + 'TW': (120.30074, 23.17002), + 'TZ': (33.53892, -5.01840), + 'UA': (33.44335, 49.30619), + 'UG': (32.96523, 2.08584), + 'UM': (-169.50993, 16.74605), + 'US': (-116.39535, 40.71379), + 'UY': (-56.46505, -33.62658), + 'UZ': (61.35529, 42.96107), + 'VA': (12.33197, 42.04931), + 'VC': (-61.09905, 13.316), + 'VE': (-64.88323, 7.69849), + 'VG': (-64.62479, 18.419), + 'VI': (-64.88950, 18.32263), + 'VN': (104.20179, 10.27644), + 'VU': (167.31919, -15.88687), + 'WF': (-176.20781, -13.28535), + 'WS': (-172.10966, -13.85093), + 'YE': (45.94562, 16.16338), + 'YT': (44.93774, -12.60882), + 'ZA': (23.19488, -30.43276), + 'ZM': (26.38618, -14.39966), + 'ZW': (30.12419, -19.86907) + } diff --git a/test/bdd/steps/geometry_factory.py b/test/bdd/steps/geometry_factory.py index 19c0406c..504227b3 100644 --- a/test/bdd/steps/geometry_factory.py +++ b/test/bdd/steps/geometry_factory.py @@ -2,13 +2,11 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2022 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. -from pathlib import Path -import os - from steps.geometry_alias import ALIASES + class GeometryFactory: """ Provides functions to create geometries from coordinates and data grids. """ @@ -47,7 +45,6 @@ class GeometryFactory: return "ST_SetSRID('{}'::geometry, 4326)".format(out) - def mk_wkt_point(self, point): """ Parse a point description. The point may either consist of 'x y' coordinates or a number @@ -65,7 +62,6 @@ class GeometryFactory: assert pt is not None, "Scenario error: Point '{}' not found in grid".format(geom) return "{} {}".format(*pt) - def mk_wkt_points(self, geom): """ Parse a list of points. The list must be a comma-separated list of points. Points @@ -73,7 +69,6 @@ class GeometryFactory: """ return ','.join([self.mk_wkt_point(x) for x in geom.split(',')]) - def set_grid(self, lines, grid_step, origin=(0.0, 0.0)): """ Replace the grid with one from the given lines. """ @@ -87,7 +82,6 @@ class GeometryFactory: x += grid_step y += grid_step - def grid_node(self, nodeid): """ Get the coordinates for the given grid node. """ diff --git a/test/bdd/steps/http_responses.py b/test/bdd/steps/http_responses.py index 2e24ed50..f803a45f 100644 --- a/test/bdd/steps/http_responses.py +++ b/test/bdd/steps/http_responses.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2023 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Classes wrapping HTTP responses from the Nominatim API. @@ -11,7 +11,7 @@ import re import json import xml.etree.ElementTree as ET -from check_functions import Almost, OsmType, Field, check_for_attributes +from check_functions import OsmType, Field, check_for_attributes class GenericResponse: @@ -45,7 +45,6 @@ class GenericResponse: else: self.result = [self.result] - def _parse_geojson(self): self._parse_json() if self.result: @@ -76,7 +75,6 @@ class GenericResponse: new['__' + k] = v self.result.append(new) - def _parse_geocodejson(self): self._parse_geojson() if self.result: @@ -87,7 +85,6 @@ class GenericResponse: inner = r.pop('geocoding') r.update(inner) - def assert_address_field(self, idx, field, value): """ Check that result rows`idx` has a field `field` with value `value` in its address. If idx is None, then all results are checked. @@ -103,7 +100,6 @@ class GenericResponse: address = self.result[idx]['address'] self.check_row_field(idx, field, value, base=address) - def match_row(self, row, context=None, field=None): """ Match the result fields against the given behave table row. """ @@ -139,7 +135,6 @@ class GenericResponse: else: self.check_row_field(i, name, Field(value), base=subdict) - def check_row(self, idx, check, msg): """ Assert for the condition 'check' and print 'msg' on fail together with the contents of the failing result. @@ -154,7 +149,6 @@ class GenericResponse: assert check, _RowError(self.result[idx]) - def check_row_field(self, idx, field, expected, base=None): """ Check field 'field' of result 'idx' for the expected value and print a meaningful error if the condition fails. @@ -172,7 +166,6 @@ class GenericResponse: f"\nBad value for field '{field}'. Expected: {expected}, got: {value}") - class SearchResponse(GenericResponse): """ Specialised class for search and lookup responses. Transforms the xml response in a format similar to json. @@ -240,7 +233,8 @@ class ReverseResponse(GenericResponse): assert 'namedetails' not in self.result[0], "More than one namedetails in result" self.result[0]['namedetails'] = {} for tag in child: - assert len(tag) == 0, f"Namedetails element '{tag.attrib['desc']}' has subelements" + assert len(tag) == 0, \ + f"Namedetails element '{tag.attrib['desc']}' has subelements" self.result[0]['namedetails'][tag.attrib['desc']] = tag.text elif child.tag == 'geokml': assert 'geokml' not in self.result[0], "More than one geokml in result" diff --git a/test/bdd/steps/nominatim_environment.py b/test/bdd/steps/nominatim_environment.py index ba19bb48..22908480 100644 --- a/test/bdd/steps/nominatim_environment.py +++ b/test/bdd/steps/nominatim_environment.py @@ -2,10 +2,9 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. from pathlib import Path -import importlib import tempfile import psycopg @@ -13,10 +12,9 @@ from psycopg import sql as pysql from nominatim_db import cli from nominatim_db.config import Configuration -from nominatim_db.db.connection import Connection, register_hstore, execute_scalar -from nominatim_db.tools import refresh +from nominatim_db.db.connection import register_hstore, execute_scalar from nominatim_db.tokenizer import factory as tokenizer_factory -from steps.utils import run_script + class NominatimEnvironment: """ Collects all functions for the execution of Nominatim functions. @@ -62,7 +60,6 @@ class NominatimEnvironment: dbargs['password'] = self.db_pass return psycopg.connect(**dbargs) - def write_nominatim_config(self, dbname): """ Set up a custom test configuration that connects to the given database. This sets up the environment variables so that they can @@ -101,7 +98,6 @@ class NominatimEnvironment: self.website_dir = tempfile.TemporaryDirectory() - def get_test_config(self): cfg = Configuration(Path(self.website_dir.name), environ=self.test_env) return cfg @@ -122,14 +118,13 @@ class NominatimEnvironment: return dsn - def db_drop_database(self, name): """ Drop the database with the given name. """ with self.connect_database('postgres') as conn: conn.autocommit = True conn.execute(pysql.SQL('DROP DATABASE IF EXISTS') - + pysql.Identifier(name)) + + pysql.Identifier(name)) def setup_template_db(self): """ Setup a template database that already contains common test data. @@ -153,13 +148,12 @@ class NominatimEnvironment: '--osm2pgsql-cache', '1', '--ignore-errors', '--offline', '--index-noanalyse') - except: + except: # noqa: E722 self.db_drop_database(self.template_db) raise self.run_nominatim('refresh', '--functions') - def setup_api_db(self): """ Setup a test against the API test database. """ @@ -184,13 +178,12 @@ class NominatimEnvironment: csv_path = str(testdata / 'full_en_phrases_test.csv') self.run_nominatim('special-phrases', '--import-from-csv', csv_path) - except: + except: # noqa: E722 self.db_drop_database(self.api_test_db) raise tokenizer_factory.get_tokenizer_for_db(self.get_test_config()) - def setup_unknown_db(self): """ Setup a test against a non-existing database. """ @@ -213,7 +206,7 @@ class NominatimEnvironment: with self.connect_database(self.template_db) as conn: conn.autocommit = True conn.execute(pysql.SQL('DROP DATABASE IF EXISTS') - + pysql.Identifier(self.test_db)) + + pysql.Identifier(self.test_db)) conn.execute(pysql.SQL('CREATE DATABASE {} TEMPLATE = {}').format( pysql.Identifier(self.test_db), pysql.Identifier(self.template_db))) @@ -250,7 +243,6 @@ class NominatimEnvironment: return False - def reindex_placex(self, db): """ Run the indexing step until all data in the placex has been processed. Indexing during updates can produce more data @@ -259,18 +251,15 @@ class NominatimEnvironment: """ self.run_nominatim('index') - def run_nominatim(self, *cmdline): """ Run the nominatim command-line tool via the library. """ if self.website_dir is not None: cmdline = list(cmdline) + ['--project-dir', self.website_dir.name] - cli.nominatim(osm2pgsql_path=None, - cli_args=cmdline, + cli.nominatim(cli_args=cmdline, environ=self.test_env) - def copy_from_place(self, db): """ Copy data from place to the placex and location_property_osmline tables invoking the appropriate triggers. @@ -293,7 +282,6 @@ class NominatimEnvironment: and osm_type='W' and ST_GeometryType(geometry) = 'ST_LineString'""") - def create_api_request_func_starlette(self): import nominatim_api.server.starlette.server from asgi_lifespan import LifespanManager @@ -311,7 +299,6 @@ class NominatimEnvironment: return _request - def create_api_request_func_falcon(self): import nominatim_api.server.falcon.server import falcon.testing @@ -326,6 +313,3 @@ class NominatimEnvironment: return response.text, response.status_code return _request - - - diff --git a/test/bdd/steps/place_inserter.py b/test/bdd/steps/place_inserter.py index c033ac17..dcd2baec 100644 --- a/test/bdd/steps/place_inserter.py +++ b/test/bdd/steps/place_inserter.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2022 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Helper classes for filling the place table. @@ -10,12 +10,13 @@ Helper classes for filling the place table. import random import string + class PlaceColumn: """ Helper class to collect contents from a behave table row and insert it into the place table. """ def __init__(self, context): - self.columns = {'admin_level' : 15} + self.columns = {'admin_level': 15} self.context = context self.geometry = None @@ -28,9 +29,11 @@ class PlaceColumn: assert 'osm_type' in self.columns, "osm column missing" if force_name and 'name' not in self.columns: - self._add_hstore('name', 'name', - ''.join(random.choice(string.printable) - for _ in range(int(random.random()*30)))) + self._add_hstore( + 'name', + 'name', + ''.join(random.choices(string.printable, k=random.randrange(30))), + ) return self @@ -96,7 +99,7 @@ class PlaceColumn: """ Issue a delete for the given OSM object. """ cursor.execute('DELETE FROM place WHERE osm_type = %s and osm_id = %s', - (self.columns['osm_type'] , self.columns['osm_id'])) + (self.columns['osm_type'], self.columns['osm_id'])) def db_insert(self, cursor): """ Insert the collected data into the database. @@ -104,7 +107,7 @@ class PlaceColumn: if self.columns['osm_type'] == 'N' and self.geometry is None: pt = self.context.osm.grid_node(self.columns['osm_id']) if pt is None: - pt = (random.random()*360 - 180, random.random()*180 - 90) + pt = (random.uniform(-180, 180), random.uniform(-90, 90)) self.geometry = "ST_SetSRID(ST_Point(%f, %f), 4326)" % pt else: diff --git a/test/bdd/steps/steps_api_queries.py b/test/bdd/steps/steps_api_queries.py index 4d15381d..de38549e 100644 --- a/test/bdd/steps/steps_api_queries.py +++ b/test/bdd/steps/steps_api_queries.py @@ -2,20 +2,16 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Steps that run queries against the API. """ from pathlib import Path -import json -import os import re import logging import asyncio import xml.etree.ElementTree as ET -from urllib.parse import urlencode -from utils import run_script from http_responses import GenericResponse, SearchResponse, ReverseResponse, StatusResponse from check_functions import Bbox, check_for_attributes from table_compare import NominatimID @@ -68,7 +64,7 @@ def send_api_query(endpoint, params, fmt, context): getattr(context, 'http_headers', {}))) -@given(u'the HTTP header') +@given('the HTTP header') def add_http_header(context): if not hasattr(context, 'http_headers'): context.http_headers = {} @@ -77,7 +73,7 @@ def add_http_header(context): context.http_headers[h] = context.table[0][h] -@when(u'sending (?P\S+ )?search query "(?P.*)"(?P with address)?') +@when(r'sending (?P\S+ )?search query "(?P.*)"(?P with address)?') def website_search_request(context, fmt, query, addr): params = {} if query: @@ -90,7 +86,7 @@ def website_search_request(context, fmt, query, addr): context.response = SearchResponse(outp, fmt or 'json', status) -@when('sending v1/reverse at (?P[\d.-]*),(?P[\d.-]*)(?: with format (?P.+))?') +@when(r'sending v1/reverse at (?P[\d.-]*),(?P[\d.-]*)(?: with format (?P.+))?') def api_endpoint_v1_reverse(context, lat, lon, fmt): params = {} if lat is not None: @@ -106,7 +102,7 @@ def api_endpoint_v1_reverse(context, lat, lon, fmt): context.response = ReverseResponse(outp, fmt or 'xml', status) -@when('sending v1/reverse N(?P\d+)(?: with format (?P.+))?') +@when(r'sending v1/reverse N(?P\d+)(?: with format (?P.+))?') def api_endpoint_v1_reverse_from_node(context, nodeid, fmt): params = {} params['lon'], params['lat'] = (f'{c:f}' for c in context.osm.grid_node(int(nodeid))) @@ -115,7 +111,7 @@ def api_endpoint_v1_reverse_from_node(context, nodeid, fmt): context.response = ReverseResponse(outp, fmt or 'xml', status) -@when(u'sending (?P\S+ )?details query for (?P.*)') +@when(r'sending (?P\S+ )?details query for (?P.*)') def website_details_request(context, fmt, query): params = {} if query[0] in 'NWR': @@ -130,38 +126,45 @@ def website_details_request(context, fmt, query): context.response = GenericResponse(outp, fmt or 'json', status) -@when(u'sending (?P\S+ )?lookup query for (?P.*)') + +@when(r'sending (?P\S+ )?lookup query for (?P.*)') def website_lookup_request(context, fmt, query): - params = { 'osm_ids' : query } + params = {'osm_ids': query} outp, status = send_api_query('lookup', params, fmt, context) context.response = SearchResponse(outp, fmt or 'xml', status) -@when(u'sending (?P\S+ )?status query') + +@when(r'sending (?P\S+ )?status query') def website_status_request(context, fmt): params = {} outp, status = send_api_query('status', params, fmt, context) context.response = StatusResponse(outp, fmt or 'text', status) -@step(u'(?Pless than|more than|exactly|at least|at most) (?P\d+) results? (?:is|are) returned') + +@step(r'(?Pless than|more than|exactly|at least|at most) ' + r'(?P\d+) results? (?:is|are) returned') def validate_result_number(context, operator, number): context.execute_steps("Then a HTTP 200 is returned") numres = len(context.response.result) assert compare(operator, numres, int(number)), \ f"Bad number of results: expected {operator} {number}, got {numres}." -@then(u'a HTTP (?P\d+) is returned') + +@then(r'a HTTP (?P\d+) is returned') def check_http_return_status(context, status): assert context.response.errorcode == int(status), \ f"Return HTTP status is {context.response.errorcode}."\ f" Full response:\n{context.response.page}" -@then(u'the page contents equals "(?P.+)"') + +@then(r'the page contents equals "(?P.+)"') def check_page_content_equals(context, text): assert context.response.page == text -@then(u'the result is valid (?P\w+)') + +@then(r'the result is valid (?P\w+)') def step_impl(context, fmt): context.execute_steps("Then a HTTP 200 is returned") if fmt.strip() == 'html': @@ -178,7 +181,7 @@ def step_impl(context, fmt): assert context.response.format == fmt -@then(u'a (?P\w+) user error is returned') +@then(r'a (?P\w+) user error is returned') def check_page_error(context, fmt): context.execute_steps("Then a HTTP 400 is returned") assert context.response.format == fmt @@ -188,32 +191,34 @@ def check_page_error(context, fmt): else: assert re.search(r'({"error":)', context.response.page, re.DOTALL) is not None -@then(u'result header contains') + +@then('result header contains') def check_header_attr(context): context.execute_steps("Then a HTTP 200 is returned") for line in context.table: assert line['attr'] in context.response.header, \ - f"Field '{line['attr']}' missing in header. Full header:\n{context.response.header}" + f"Field '{line['attr']}' missing in header. " \ + f"Full header:\n{context.response.header}" value = context.response.header[line['attr']] assert re.fullmatch(line['value'], value) is not None, \ f"Attribute '{line['attr']}': expected: '{line['value']}', got '{value}'" -@then(u'result header has (?Pnot )?attributes (?P.*)') +@then('result header has (?Pnot )?attributes (?P.*)') def check_header_no_attr(context, neg, attrs): check_for_attributes(context.response.header, attrs, 'absent' if neg else 'present') -@then(u'results contain(?: in field (?P.*))?') -def step_impl(context, field): +@then(r'results contain(?: in field (?P.*))?') +def results_contain_in_field(context, field): context.execute_steps("then at least 1 result is returned") for line in context.table: context.response.match_row(line, context=context, field=field) -@then(u'result (?P\d+ )?has (?Pnot )?attributes (?P.*)') +@then(r'result (?P\d+ )?has (?Pnot )?attributes (?P.*)') def validate_attributes(context, lid, neg, attrs): for i in make_todo_list(context, lid): check_for_attributes(context.response.result[i], attrs, @@ -221,7 +226,7 @@ def validate_attributes(context, lid, neg, attrs): @then(u'result addresses contain') -def step_impl(context): +def result_addresses_contain(context): context.execute_steps("then at least 1 result is returned") for line in context.table: @@ -231,8 +236,9 @@ def step_impl(context): if name != 'ID': context.response.assert_address_field(idx, name, value) -@then(u'address of result (?P\d+) has(?P no)? types (?P.*)') -def check_address(context, lid, neg, attrs): + +@then(r'address of result (?P\d+) has(?P no)? types (?P.*)') +def check_address_has_types(context, lid, neg, attrs): context.execute_steps(f"then more than {lid} results are returned") addr_parts = context.response.result[int(lid)]['address'] @@ -243,7 +249,8 @@ def check_address(context, lid, neg, attrs): else: assert attr in addr_parts -@then(u'address of result (?P\d+) (?Pis|contains)') + +@then(r'address of result (?P\d+) (?Pis|contains)') def check_address(context, lid, complete): context.execute_steps(f"then more than {lid} results are returned") @@ -258,7 +265,7 @@ def check_address(context, lid, complete): assert len(addr_parts) == 0, f"Additional address parts found: {addr_parts!s}" -@then(u'result (?P\d+ )?has bounding box in (?P[\d,.-]+)') +@then(r'result (?P\d+ )?has bounding box in (?P[\d,.-]+)') def check_bounding_box_in_area(context, lid, coords): expected = Bbox(coords) @@ -269,7 +276,7 @@ def check_bounding_box_in_area(context, lid, coords): f"Bbox is not contained in {expected}") -@then(u'result (?P\d+ )?has centroid in (?P[\d,.-]+)') +@then(r'result (?P\d+ )?has centroid in (?P[\d,.-]+)') def check_centroid_in_area(context, lid, coords): expected = Bbox(coords) @@ -280,7 +287,7 @@ def check_centroid_in_area(context, lid, coords): f"Centroid is not inside {expected}") -@then(u'there are(?P no)? duplicates') +@then('there are(?P no)? duplicates') def check_for_duplicates(context, neg): context.execute_steps("then at least 1 result is returned") @@ -298,4 +305,3 @@ def check_for_duplicates(context, neg): assert not has_dupe, f"Found duplicate for {dup}" else: assert has_dupe, "No duplicates found" - diff --git a/test/bdd/steps/steps_db_ops.py b/test/bdd/steps/steps_db_ops.py index fb8431d5..8b62cbc6 100644 --- a/test/bdd/steps/steps_db_ops.py +++ b/test/bdd/steps/steps_db_ops.py @@ -2,9 +2,8 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. -import logging from itertools import chain import psycopg @@ -13,9 +12,9 @@ from psycopg import sql as pysql from place_inserter import PlaceColumn from table_compare import NominatimID, DBRow -from nominatim_db.indexer import indexer from nominatim_db.tokenizer import factory as tokenizer_factory + def check_database_integrity(context): """ Check some generic constraints on the tables. """ @@ -31,10 +30,9 @@ def check_database_integrity(context): cur.execute("SELECT count(*) FROM word WHERE word_token = ''") assert cur.fetchone()[0] == 0, "Empty word tokens found in word table" +# GIVEN ################################## -################################ GIVEN ################################## - @given("the (?Pnamed )?places") def add_data_to_place_table(context, named): """ Add entries into the place table. 'named places' makes sure that @@ -46,6 +44,7 @@ def add_data_to_place_table(context, named): PlaceColumn(context).add_row(row, named is not None).db_insert(cur) cur.execute('ALTER TABLE place ENABLE TRIGGER place_before_insert') + @given("the relations") def add_data_to_planet_relations(context): """ Add entries into the osm2pgsql relation middle table. This is needed @@ -77,9 +76,11 @@ def add_data_to_planet_relations(context): else: members = None - tags = chain.from_iterable([(h[5:], r[h]) for h in r.headings if h.startswith("tags+")]) + tags = chain.from_iterable([(h[5:], r[h]) for h in r.headings + if h.startswith("tags+")]) - cur.execute("""INSERT INTO planet_osm_rels (id, way_off, rel_off, parts, members, tags) + cur.execute("""INSERT INTO planet_osm_rels (id, way_off, rel_off, + parts, members, tags) VALUES (%s, %s, %s, %s, %s, %s)""", (r['id'], last_node, last_way, parts, members, list(tags))) else: @@ -99,6 +100,7 @@ def add_data_to_planet_relations(context): (r['id'], psycopg.types.json.Json(tags), psycopg.types.json.Json(members))) + @given("the ways") def add_data_to_planet_ways(context): """ Add entries into the osm2pgsql way middle table. This is necessary for @@ -110,16 +112,18 @@ def add_data_to_planet_ways(context): json_tags = row is not None and row['value'] != '1' for r in context.table: if json_tags: - tags = psycopg.types.json.Json({h[5:]: r[h] for h in r.headings if h.startswith("tags+")}) + tags = psycopg.types.json.Json({h[5:]: r[h] for h in r.headings + if h.startswith("tags+")}) else: tags = list(chain.from_iterable([(h[5:], r[h]) for h in r.headings if h.startswith("tags+")])) - nodes = [ int(x.strip()) for x in r['nodes'].split(',') ] + nodes = [int(x.strip()) for x in r['nodes'].split(',')] cur.execute("INSERT INTO planet_osm_ways (id, nodes, tags) VALUES (%s, %s, %s)", (r['id'], nodes, tags)) -################################ WHEN ################################## +# WHEN ################################## + @when("importing") def import_and_index_data_from_place_table(context): @@ -136,6 +140,7 @@ def import_and_index_data_from_place_table(context): # itself. context.log_capture.buffer.clear() + @when("updating places") def update_place_table(context): """ Update the place table with the given data. Also runs all triggers @@ -164,6 +169,7 @@ def update_postcodes(context): """ context.nominatim.run_nominatim('refresh', '--postcodes') + @when("marking for delete (?P.*)") def delete_places(context, oids): """ Remove entries from the place table. Multiple ids may be given @@ -184,7 +190,8 @@ def delete_places(context, oids): # itself. context.log_capture.buffer.clear() -################################ THEN ################################## +# THEN ################################## + @then("(?Pplacex|place) contains(?P exactly)?") def check_place_contents(context, table, exact): @@ -201,7 +208,8 @@ def check_place_contents(context, table, exact): expected_content = set() for row in context.table: nid = NominatimID(row['object']) - query = 'SELECT *, ST_AsText(geometry) as geomtxt, ST_GeometryType(geometry) as geometrytype' + query = """SELECT *, ST_AsText(geometry) as geomtxt, + ST_GeometryType(geometry) as geometrytype """ if table == 'placex': query += ' ,ST_X(centroid) as cx, ST_Y(centroid) as cy' query += " FROM %s WHERE {}" % (table, ) @@ -261,17 +269,18 @@ def check_search_name_contents(context, exclude): if not exclude: assert len(tokens) >= len(items), \ - "No word entry found for {}. Entries found: {!s}".format(value, len(tokens)) + f"No word entry found for {value}. Entries found: {len(tokens)}" for word, token, wid in tokens: if exclude: assert wid not in res[name], \ - "Found term for {}/{}: {}".format(nid, name, wid) + "Found term for {}/{}: {}".format(nid, name, wid) else: assert wid in res[name], \ - "Missing term for {}/{}: {}".format(nid, name, wid) + "Missing term for {}/{}: {}".format(nid, name, wid) elif name != 'object': assert db_row.contains(name, value), db_row.assert_msg(name, value) + @then("search_name has no entry for (?P.*)") def check_search_name_has_entry(context, oid): """ Check that there is noentry in the search_name table for the given @@ -283,6 +292,7 @@ def check_search_name_has_entry(context, oid): assert cur.rowcount == 0, \ "Found {} entries for ID {}".format(cur.rowcount, oid) + @then("location_postcode contains exactly") def check_location_postcode(context): """ Check full contents for location_postcode table. Each row represents a table row @@ -294,21 +304,22 @@ def check_location_postcode(context): with context.db.cursor() as cur: cur.execute("SELECT *, ST_AsText(geometry) as geomtxt FROM location_postcode") assert cur.rowcount == len(list(context.table)), \ - "Postcode table has {} rows, expected {}.".format(cur.rowcount, len(list(context.table))) + "Postcode table has {cur.rowcount} rows, expected {len(list(context.table))}." results = {} for row in cur: key = (row['country_code'], row['postcode']) assert key not in results, "Postcode table has duplicate entry: {}".format(row) - results[key] = DBRow((row['country_code'],row['postcode']), row, context) + results[key] = DBRow((row['country_code'], row['postcode']), row, context) for row in context.table: - db_row = results.get((row['country'],row['postcode'])) + db_row = results.get((row['country'], row['postcode'])) assert db_row is not None, \ f"Missing row for country '{row['country']}' postcode '{row['postcode']}'." db_row.assert_row(row, ('country', 'postcode')) + @then("there are(?P no)? word tokens for postcodes (?P.*)") def check_word_table_for_postcodes(context, exclude, postcodes): """ Check that the tokenizer produces postcode tokens for the given @@ -333,7 +344,8 @@ def check_word_table_for_postcodes(context, exclude, postcodes): assert len(found) == 0, f"Unexpected postcodes: {found}" else: assert set(found) == set(plist), \ - f"Missing postcodes {set(plist) - set(found)}. Found: {found}" + f"Missing postcodes {set(plist) - set(found)}. Found: {found}" + @then("place_addressline contains") def check_place_addressline(context): @@ -352,11 +364,12 @@ def check_place_addressline(context): WHERE place_id = %s AND address_place_id = %s""", (pid, apid)) assert cur.rowcount > 0, \ - "No rows found for place %s and address %s" % (row['object'], row['address']) + f"No rows found for place {row['object']} and address {row['address']}." for res in cur: DBRow(nid, res, context).assert_row(row, ('address', 'object')) + @then("place_addressline doesn't contain") def check_place_addressline_exclude(context): """ Check that the place_addressline doesn't contain any entries for the @@ -371,9 +384,10 @@ def check_place_addressline_exclude(context): WHERE place_id = %s AND address_place_id = %s""", (pid, apid)) assert cur.rowcount == 0, \ - "Row found for place %s and address %s" % (row['object'], row['address']) + f"Row found for place {row['object']} and address {row['address']}." + -@then("W(?P\d+) expands to(?P no)? interpolation") +@then(r"W(?P\d+) expands to(?P no)? interpolation") def check_location_property_osmline(context, oid, neg): """ Check that the given way is present in the interpolation table. """ @@ -392,7 +406,7 @@ def check_location_property_osmline(context, oid, neg): for i in todo: row = context.table[i] if (int(row['start']) == res['startnumber'] - and int(row['end']) == res['endnumber']): + and int(row['end']) == res['endnumber']): todo.remove(i) break else: @@ -402,8 +416,9 @@ def check_location_property_osmline(context, oid, neg): assert not todo, f"Unmatched lines in table: {list(context.table[i] for i in todo)}" + @then("location_property_osmline contains(?P exactly)?") -def check_place_contents(context, exact): +def check_osmline_contents(context, exact): """ Check contents of the interpolation table. Each row represents a table row and all data must match. Data not present in the expected table, may be arbitrary. The rows are identified via the 'object' column which must @@ -447,4 +462,3 @@ def check_place_contents(context, exact): assert expected_content == actual, \ f"Missing entries: {expected_content - actual}\n" \ f"Not expected in table: {actual - expected_content}" - diff --git a/test/bdd/steps/steps_osm_data.py b/test/bdd/steps/steps_osm_data.py index 70cf1515..69f71994 100644 --- a/test/bdd/steps/steps_osm_data.py +++ b/test/bdd/steps/steps_osm_data.py @@ -14,6 +14,7 @@ from nominatim_db.tools.replication import run_osm2pgsql_updates from geometry_alias import ALIASES + def get_osm2pgsql_options(nominatim_env, fname, append): return dict(import_file=fname, osm2pgsql='osm2pgsql', @@ -25,8 +26,7 @@ def get_osm2pgsql_options(nominatim_env, fname, append): flatnode_file='', tablespaces=dict(slim_data='', slim_index='', main_data='', main_index=''), - append=append - ) + append=append) def write_opl_file(opl, grid): @@ -41,14 +41,14 @@ def write_opl_file(opl, grid): if line.startswith('n') and line.find(' x') < 0: coord = grid.grid_node(int(line[1:].split(' ')[0])) if coord is None: - coord = (random.random() * 360 - 180, - random.random() * 180 - 90) + coord = (random.uniform(-180, 180), random.uniform(-90, 90)) line += " x%f y%f" % coord fd.write(line.encode('utf-8')) fd.write(b'\n') return fd.name + @given('the lua style file') def lua_style_file(context): """ Define a custom style file to use for the import. @@ -91,7 +91,7 @@ def define_node_grid(context, grid_step, origin): @when(u'loading osm data') def load_osm_file(context): """ - Load the given data into a freshly created test data using osm2pgsql. + Load the given data into a freshly created test database using osm2pgsql. No further indexing is done. The data is expected as attached text in OPL format. @@ -103,13 +103,14 @@ def load_osm_file(context): finally: os.remove(fname) - ### reintroduce the triggers/indexes we've lost by having osm2pgsql set up place again + # reintroduce the triggers/indexes we've lost by having osm2pgsql set up place again cur = context.db.cursor() cur.execute("""CREATE TRIGGER place_before_delete BEFORE DELETE ON place FOR EACH ROW EXECUTE PROCEDURE place_delete()""") cur.execute("""CREATE TRIGGER place_before_insert BEFORE INSERT ON place FOR EACH ROW EXECUTE PROCEDURE place_insert()""") - cur.execute("""CREATE UNIQUE INDEX idx_place_osm_unique on place using btree(osm_id,osm_type,class,type)""") + cur.execute("""CREATE UNIQUE INDEX idx_place_osm_unique ON place + USING btree(osm_id,osm_type,class,type)""") context.db.commit() @@ -133,6 +134,7 @@ def update_from_osm_file(context): finally: os.remove(fname) + @when('indexing') def index_database(context): """ diff --git a/test/bdd/steps/table_compare.py b/test/bdd/steps/table_compare.py index 4284fad9..79c9186b 100644 --- a/test/bdd/steps/table_compare.py +++ b/test/bdd/steps/table_compare.py @@ -2,21 +2,21 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2022 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Functions to facilitate accessing and comparing the content of DB tables. """ +import math import re import json import psycopg from psycopg import sql as pysql -from steps.check_functions import Almost - ID_REGEX = re.compile(r"(?P[NRW])(?P\d+)(:(?P\w+))?") + class NominatimID: """ Splits a unique identifier for places into its components. As place_ids cannot be used for testing, we use a unique @@ -147,10 +147,10 @@ class DBRow: return str(actual) == expected def _compare_place_id(self, actual, expected): - if expected == '0': + if expected == '0': return actual == 0 - with self.context.db.cursor() as cur: + with self.context.db.cursor() as cur: return NominatimID(expected).get_place_id(cur) == actual def _has_centroid(self, expected): @@ -166,13 +166,15 @@ class DBRow: else: x, y = self.context.osm.grid_node(int(expected)) - return Almost(float(x)) == self.db_row['cx'] and Almost(float(y)) == self.db_row['cy'] + return math.isclose(float(x), self.db_row['cx']) \ + and math.isclose(float(y), self.db_row['cy']) def _has_geometry(self, expected): geom = self.context.osm.parse_geometry(expected) with self.context.db.cursor(row_factory=psycopg.rows.tuple_row) as cur: - cur.execute(pysql.SQL("""SELECT ST_Equals(ST_SnapToGrid({}, 0.00001, 0.00001), - ST_SnapToGrid(ST_SetSRID({}::geometry, 4326), 0.00001, 0.00001))""") + cur.execute(pysql.SQL(""" + SELECT ST_Equals(ST_SnapToGrid({}, 0.00001, 0.00001), + ST_SnapToGrid(ST_SetSRID({}::geometry, 4326), 0.00001, 0.00001))""") .format(pysql.SQL(geom), pysql.Literal(self.db_row['geomtxt']))) return cur.fetchone()[0] @@ -187,7 +189,8 @@ class DBRow: else: msg += " No such column." - return msg + "\nFull DB row: {}".format(json.dumps(dict(self.db_row), indent=4, default=str)) + return msg + "\nFull DB row: {}".format(json.dumps(dict(self.db_row), + indent=4, default=str)) def _get_actual(self, name): if '+' in name: diff --git a/test/bdd/steps/utils.py b/test/bdd/steps/utils.py deleted file mode 100644 index e789deff..00000000 --- a/test/bdd/steps/utils.py +++ /dev/null @@ -1,28 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-only -# -# This file is part of Nominatim. (https://nominatim.org) -# -# Copyright (C) 2022 by the Nominatim developer community. -# For a full list of authors see the git log. -""" -Various smaller helps for step execution. -""" -import logging -import subprocess - -LOG = logging.getLogger(__name__) - -def run_script(cmd, **kwargs): - """ Run the given command, check that it is successful and output - when necessary. - """ - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - **kwargs) - (outp, outerr) = proc.communicate() - outp = outp.decode('utf-8') - outerr = outerr.decode('utf-8').replace('\\n', '\n') - LOG.debug("Run command: %s\n%s\n%s", cmd, outp, outerr) - - assert proc.returncode == 0, "Script '{}' failed:\n{}\n{}\n".format(cmd[0], outp, outerr) - - return outp, outerr diff --git a/test/python/api/conftest.py b/test/python/api/conftest.py index 3ca0720b..bde0afc4 100644 --- a/test/python/api/conftest.py +++ b/test/python/api/conftest.py @@ -2,14 +2,13 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Helper fixtures for API call tests. """ import pytest import pytest_asyncio -import time import datetime as dt import sqlalchemy as sa @@ -20,27 +19,25 @@ from nominatim_api.search.query_analyzer_factory import make_query_analyzer from nominatim_db.tools import convert_sqlite import nominatim_api.logging as loglib + class APITester: def __init__(self): self.api = napi.NominatimAPI() self.async_to_sync(self.api._async_api.setup_database()) - def async_to_sync(self, func): """ Run an asynchronous function until completion using the internal loop of the API. """ return self.api._loop.run_until_complete(func) - def add_data(self, table, data): """ Insert data into the given table. """ sql = getattr(self.api._async_api._tables, table).insert() self.async_to_sync(self.exec_async(sql, data)) - def add_placex(self, **kw): name = kw.get('name') if isinstance(name, str): @@ -50,30 +47,29 @@ class APITester: geometry = kw.get('geometry', 'POINT(%f %f)' % centroid) self.add_data('placex', - {'place_id': kw.get('place_id', 1000), - 'osm_type': kw.get('osm_type', 'W'), - 'osm_id': kw.get('osm_id', 4), - 'class_': kw.get('class_', 'highway'), - 'type': kw.get('type', 'residential'), - 'name': name, - 'address': kw.get('address'), - 'extratags': kw.get('extratags'), - 'parent_place_id': kw.get('parent_place_id'), - 'linked_place_id': kw.get('linked_place_id'), - 'admin_level': kw.get('admin_level', 15), - 'country_code': kw.get('country_code'), - 'housenumber': kw.get('housenumber'), - 'postcode': kw.get('postcode'), - 'wikipedia': kw.get('wikipedia'), - 'rank_search': kw.get('rank_search', 30), - 'rank_address': kw.get('rank_address', 30), - 'importance': kw.get('importance'), - 'centroid': 'POINT(%f %f)' % centroid, - 'indexed_status': kw.get('indexed_status', 0), - 'indexed_date': kw.get('indexed_date', - dt.datetime(2022, 12, 7, 14, 14, 46, 0)), - 'geometry': geometry}) - + {'place_id': kw.get('place_id', 1000), + 'osm_type': kw.get('osm_type', 'W'), + 'osm_id': kw.get('osm_id', 4), + 'class_': kw.get('class_', 'highway'), + 'type': kw.get('type', 'residential'), + 'name': name, + 'address': kw.get('address'), + 'extratags': kw.get('extratags'), + 'parent_place_id': kw.get('parent_place_id'), + 'linked_place_id': kw.get('linked_place_id'), + 'admin_level': kw.get('admin_level', 15), + 'country_code': kw.get('country_code'), + 'housenumber': kw.get('housenumber'), + 'postcode': kw.get('postcode'), + 'wikipedia': kw.get('wikipedia'), + 'rank_search': kw.get('rank_search', 30), + 'rank_address': kw.get('rank_address', 30), + 'importance': kw.get('importance'), + 'centroid': 'POINT(%f %f)' % centroid, + 'indexed_status': kw.get('indexed_status', 0), + 'indexed_date': kw.get('indexed_date', + dt.datetime(2022, 12, 7, 14, 14, 46, 0)), + 'geometry': geometry}) def add_address_placex(self, object_id, **kw): self.add_placex(**kw) @@ -85,46 +81,42 @@ class APITester: 'fromarea': kw.get('fromarea', False), 'isaddress': kw.get('isaddress', True)}) - def add_osmline(self, **kw): self.add_data('osmline', - {'place_id': kw.get('place_id', 10000), - 'osm_id': kw.get('osm_id', 4004), - 'parent_place_id': kw.get('parent_place_id'), - 'indexed_date': kw.get('indexed_date', - dt.datetime(2022, 12, 7, 14, 14, 46, 0)), - 'startnumber': kw.get('startnumber', 2), - 'endnumber': kw.get('endnumber', 6), - 'step': kw.get('step', 2), - 'address': kw.get('address'), - 'postcode': kw.get('postcode'), - 'country_code': kw.get('country_code'), - 'linegeo': kw.get('geometry', 'LINESTRING(1.1 -0.2, 1.09 -0.22)')}) - + {'place_id': kw.get('place_id', 10000), + 'osm_id': kw.get('osm_id', 4004), + 'parent_place_id': kw.get('parent_place_id'), + 'indexed_date': kw.get('indexed_date', + dt.datetime(2022, 12, 7, 14, 14, 46, 0)), + 'startnumber': kw.get('startnumber', 2), + 'endnumber': kw.get('endnumber', 6), + 'step': kw.get('step', 2), + 'address': kw.get('address'), + 'postcode': kw.get('postcode'), + 'country_code': kw.get('country_code'), + 'linegeo': kw.get('geometry', 'LINESTRING(1.1 -0.2, 1.09 -0.22)')}) def add_tiger(self, **kw): self.add_data('tiger', - {'place_id': kw.get('place_id', 30000), - 'parent_place_id': kw.get('parent_place_id'), - 'startnumber': kw.get('startnumber', 2), - 'endnumber': kw.get('endnumber', 6), - 'step': kw.get('step', 2), - 'postcode': kw.get('postcode'), - 'linegeo': kw.get('geometry', 'LINESTRING(1.1 -0.2, 1.09 -0.22)')}) - + {'place_id': kw.get('place_id', 30000), + 'parent_place_id': kw.get('parent_place_id'), + 'startnumber': kw.get('startnumber', 2), + 'endnumber': kw.get('endnumber', 6), + 'step': kw.get('step', 2), + 'postcode': kw.get('postcode'), + 'linegeo': kw.get('geometry', 'LINESTRING(1.1 -0.2, 1.09 -0.22)')}) def add_postcode(self, **kw): self.add_data('postcode', - {'place_id': kw.get('place_id', 1000), - 'parent_place_id': kw.get('parent_place_id'), - 'country_code': kw.get('country_code'), - 'postcode': kw.get('postcode'), - 'rank_search': kw.get('rank_search', 20), - 'rank_address': kw.get('rank_address', 22), - 'indexed_date': kw.get('indexed_date', - dt.datetime(2022, 12, 7, 14, 14, 46, 0)), - 'geometry': kw.get('geometry', 'POINT(23 34)')}) - + {'place_id': kw.get('place_id', 1000), + 'parent_place_id': kw.get('parent_place_id'), + 'country_code': kw.get('country_code'), + 'postcode': kw.get('postcode'), + 'rank_search': kw.get('rank_search', 20), + 'rank_address': kw.get('rank_address', 22), + 'indexed_date': kw.get('indexed_date', + dt.datetime(2022, 12, 7, 14, 14, 46, 0)), + 'geometry': kw.get('geometry', 'POINT(23 34)')}) def add_country(self, country_code, geometry): self.add_data('country_grid', @@ -132,14 +124,12 @@ class APITester: 'area': 0.1, 'geometry': geometry}) - def add_country_name(self, country_code, names, partition=0): self.add_data('country_name', {'country_code': country_code, 'name': names, 'partition': partition}) - def add_search_name(self, place_id, **kw): centroid = kw.get('centroid', (23.0, 34.0)) self.add_data('search_name', @@ -152,7 +142,6 @@ class APITester: 'country_code': kw.get('country_code', 'xx'), 'centroid': 'POINT(%f %f)' % centroid}) - def add_class_type_table(self, cls, typ): self.async_to_sync( self.exec_async(sa.text(f"""CREATE TABLE place_classtype_{cls}_{typ} @@ -160,7 +149,6 @@ class APITester: WHERE class = '{cls}' AND type = '{typ}') """))) - def add_word_table(self, content): data = [dict(zip(['word_id', 'word_token', 'type', 'word', 'info'], c)) for c in content] @@ -176,12 +164,10 @@ class APITester: self.async_to_sync(_do_sql()) - async def exec_async(self, sql, *args, **kwargs): async with self.api._async_api.begin() as conn: return await conn.execute(sql, *args, **kwargs) - async def create_tables(self): async with self.api._async_api._engine.begin() as conn: await conn.run_sync(self.api._async_api._tables.meta.create_all) @@ -212,11 +198,12 @@ def frontend(request, event_loop, tmp_path): db = str(tmp_path / 'test_nominatim_python_unittest.sqlite') def mkapi(apiobj, options={'reverse'}): - apiobj.add_data('properties', - [{'property': 'tokenizer', 'value': 'icu'}, - {'property': 'tokenizer_import_normalisation', 'value': ':: lower();'}, - {'property': 'tokenizer_import_transliteration', 'value': "'1' > '/1/'; 'ä' > 'ä '"}, - ]) + apiobj.add_data( + 'properties', + [{'property': 'tokenizer', 'value': 'icu'}, + {'property': 'tokenizer_import_normalisation', 'value': ':: lower();'}, + {'property': 'tokenizer_import_transliteration', + 'value': "'1' > '/1/'; 'ä' > 'ä '"}]) async def _do_sql(): async with apiobj.api._async_api.begin() as conn: diff --git a/test/python/api/fake_adaptor.py b/test/python/api/fake_adaptor.py index 4b64c17d..a3a3bcf9 100644 --- a/test/python/api/fake_adaptor.py +++ b/test/python/api/fake_adaptor.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Provides dummy implementations of ASGIAdaptor for testing. @@ -13,6 +13,7 @@ import nominatim_api.v1.server_glue as glue from nominatim_api.v1.format import dispatch as formatting from nominatim_api.config import Configuration + class FakeError(BaseException): def __init__(self, msg, status): @@ -22,8 +23,10 @@ class FakeError(BaseException): def __str__(self): return f'{self.status} -- {self.msg}' + FakeResponse = namedtuple('FakeResponse', ['status', 'output', 'content_type']) + class FakeAdaptor(glue.ASGIAdaptor): def __init__(self, params=None, headers=None, config=None): @@ -31,23 +34,18 @@ class FakeAdaptor(glue.ASGIAdaptor): self.headers = headers or {} self._config = config or Configuration(None) - def get(self, name, default=None): return self.params.get(name, default) - def get_header(self, name, default=None): return self.headers.get(name, default) - def error(self, msg, status=400): return FakeError(msg, status) - def create_response(self, status, output, num_results): return FakeResponse(status, output, self.content_type) - def base_uri(self): return 'http://test' @@ -56,5 +54,3 @@ class FakeAdaptor(glue.ASGIAdaptor): def formatting(self): return formatting - - diff --git a/test/python/api/query_processing/test_normalize.py b/test/python/api/query_processing/test_normalize.py index 12a8de2a..35f5fcd7 100644 --- a/test/python/api/query_processing/test_normalize.py +++ b/test/python/api/query_processing/test_normalize.py @@ -2,21 +2,18 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for normalizing search queries. """ -from pathlib import Path - -import pytest - from icu import Transliterator import nominatim_api.search.query as qmod from nominatim_api.query_preprocessing.config import QueryConfig from nominatim_api.query_preprocessing import normalize + def run_preprocessor_on(query, norm): normalizer = Transliterator.createFromRules("normalization", norm) proc = normalize.create(QueryConfig().set_normalizer(normalizer)) diff --git a/test/python/api/query_processing/test_split_japanese_phrases.py b/test/python/api/query_processing/test_split_japanese_phrases.py index 51d592e3..30f22e7b 100644 --- a/test/python/api/query_processing/test_split_japanese_phrases.py +++ b/test/python/api/query_processing/test_split_japanese_phrases.py @@ -7,16 +7,13 @@ """ Tests for japanese phrase splitting. """ -from pathlib import Path - import pytest -from icu import Transliterator - import nominatim_api.search.query as qmod from nominatim_api.query_preprocessing.config import QueryConfig from nominatim_api.query_preprocessing import split_japanese_phrases + def run_preprocessor_on(query): proc = split_japanese_phrases.create(QueryConfig().set_normalizer(None)) diff --git a/test/python/api/search/test_api_search_query.py b/test/python/api/search/test_api_search_query.py index 08a1f7aa..c9e8de93 100644 --- a/test/python/api/search/test_api_search_query.py +++ b/test/python/api/search/test_api_search_query.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for tokenized query data structures. @@ -11,6 +11,7 @@ import pytest from nominatim_api.search import query + class MyToken(query.Token): def get_category(self): @@ -21,9 +22,11 @@ def mktoken(tid: int): return MyToken(penalty=3.0, token=tid, count=1, addr_count=1, lookup_word='foo') + @pytest.fixture def qnode(): - return query.QueryNode(query.BREAK_PHRASE, query.PHRASE_ANY, 0.0 ,'', '') + return query.QueryNode(query.BREAK_PHRASE, query.PHRASE_ANY, 0.0, '', '') + @pytest.mark.parametrize('ptype,ttype', [(query.PHRASE_ANY, 'W'), (query.PHRASE_AMENITY, 'Q'), @@ -132,4 +135,3 @@ def test_query_struct_amenity_two_words(): assert len(q.get_tokens(query.TokenRange(1, 2), query.TOKEN_PARTIAL)) == 1 assert len(q.get_tokens(query.TokenRange(1, 2), query.TOKEN_NEAR_ITEM)) == 0 assert len(q.get_tokens(query.TokenRange(1, 2), query.TOKEN_QUALIFIER)) == 1 - diff --git a/test/python/api/search/test_db_search_builder.py b/test/python/api/search/test_db_search_builder.py index 49d5f303..be34fbea 100644 --- a/test/python/api/search/test_db_search_builder.py +++ b/test/python/api/search/test_db_search_builder.py @@ -16,6 +16,7 @@ from nominatim_api.search.token_assignment import TokenAssignment from nominatim_api.types import SearchDetails import nominatim_api.search.db_searches as dbs + class MyToken(Token): def get_category(self): return 'this', 'that' @@ -36,7 +37,6 @@ def make_query(*args): token=tid, count=1, addr_count=1, lookup_word=word)) - return q @@ -241,8 +241,7 @@ def test_name_and_address(): [(2, qmod.TOKEN_PARTIAL, [(2, 'b')]), (2, qmod.TOKEN_WORD, [(101, 'b')])], [(3, qmod.TOKEN_PARTIAL, [(3, 'c')]), - (3, qmod.TOKEN_WORD, [(102, 'c')])] - ) + (3, qmod.TOKEN_WORD, [(102, 'c')])]) builder = SearchBuilder(q, SearchDetails()) searches = list(builder.build(TokenAssignment(name=TokenRange(0, 1), @@ -267,8 +266,7 @@ def test_name_and_complex_address(): (3, qmod.TOKEN_WORD, [(101, 'bc')])], [(3, qmod.TOKEN_PARTIAL, [(3, 'c')])], [(4, qmod.TOKEN_PARTIAL, [(4, 'd')]), - (4, qmod.TOKEN_WORD, [(103, 'd')])] - ) + (4, qmod.TOKEN_WORD, [(103, 'd')])]) builder = SearchBuilder(q, SearchDetails()) searches = list(builder.build(TokenAssignment(name=TokenRange(0, 1), @@ -423,8 +421,8 @@ def test_infrequent_partials_in_name(): assert len(search.lookups) == 2 assert len(search.rankings) == 2 - assert set((l.column, l.lookup_type.__name__) for l in search.lookups) == \ - {('name_vector', 'LookupAll'), ('nameaddress_vector', 'Restrict')} + assert set((s.column, s.lookup_type.__name__) for s in search.lookups) == \ + {('name_vector', 'LookupAll'), ('nameaddress_vector', 'Restrict')} def test_frequent_partials_in_name_and_address(): @@ -435,10 +433,10 @@ def test_frequent_partials_in_name_and_address(): assert all(isinstance(s, dbs.PlaceSearch) for s in searches) searches.sort(key=lambda s: s.penalty) - assert set((l.column, l.lookup_type.__name__) for l in searches[0].lookups) == \ - {('name_vector', 'LookupAny'), ('nameaddress_vector', 'Restrict')} - assert set((l.column, l.lookup_type.__name__) for l in searches[1].lookups) == \ - {('nameaddress_vector', 'LookupAll'), ('name_vector', 'LookupAll')} + assert set((s.column, s.lookup_type.__name__) for s in searches[0].lookups) == \ + {('name_vector', 'LookupAny'), ('nameaddress_vector', 'Restrict')} + assert set((s.column, s.lookup_type.__name__) for s in searches[1].lookups) == \ + {('nameaddress_vector', 'LookupAll'), ('name_vector', 'LookupAll')} def test_too_frequent_partials_in_name_and_address(): @@ -449,5 +447,5 @@ def test_too_frequent_partials_in_name_and_address(): assert all(isinstance(s, dbs.PlaceSearch) for s in searches) searches.sort(key=lambda s: s.penalty) - assert set((l.column, l.lookup_type.__name__) for l in searches[0].lookups) == \ - {('name_vector', 'LookupAny'), ('nameaddress_vector', 'Restrict')} + assert set((s.column, s.lookup_type.__name__) for s in searches[0].lookups) == \ + {('name_vector', 'LookupAny'), ('nameaddress_vector', 'Restrict')} diff --git a/test/python/api/search/test_icu_query_analyzer.py b/test/python/api/search/test_icu_query_analyzer.py index fc200bca..72535026 100644 --- a/test/python/api/search/test_icu_query_analyzer.py +++ b/test/python/api/search/test_icu_query_analyzer.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for query analyzer for ICU tokenizer. @@ -16,7 +16,8 @@ import nominatim_api.search.query as qmod import nominatim_api.search.icu_tokenizer as tok from nominatim_api.logging import set_log_output, get_and_disable -async def add_word(conn, word_id, word_token, wtype, word, info = None): + +async def add_word(conn, word_id, word_token, wtype, word, info=None): t = conn.t.meta.tables['word'] await conn.execute(t.insert(), {'word_id': word_id, 'word_token': word_token, @@ -28,6 +29,7 @@ async def add_word(conn, word_id, word_token, wtype, word, info = None): def make_phrase(query): return [Phrase(qmod.PHRASE_ANY, s) for s in query.split(',')] + @pytest_asyncio.fixture async def conn(table_factory): """ Create an asynchronous SQLAlchemy engine for the test DB. @@ -102,8 +104,7 @@ async def test_splitting_in_transliteration(conn): @pytest.mark.asyncio @pytest.mark.parametrize('term,order', [('23456', ['P', 'H', 'W', 'w']), - ('3', ['H', 'W', 'w']) - ]) + ('3', ['H', 'W', 'w'])]) async def test_penalty_postcodes_and_housenumbers(conn, term, order): ana = await tok.create_query_analyzer(conn) @@ -120,6 +121,7 @@ async def test_penalty_postcodes_and_housenumbers(conn, term, order): assert [t[1] for t in torder] == order + @pytest.mark.asyncio async def test_category_words_only_at_beginning(conn): ana = await tok.create_query_analyzer(conn) diff --git a/test/python/api/search/test_postcode_parser.py b/test/python/api/search/test_postcode_parser.py index 284aba5b..38638e07 100644 --- a/test/python/api/search/test_postcode_parser.py +++ b/test/python/api/search/test_postcode_parser.py @@ -16,6 +16,7 @@ import pytest from nominatim_api.search.postcode_parser import PostcodeParser from nominatim_api.search.query import QueryStruct, PHRASE_ANY, PHRASE_POSTCODE, PHRASE_STREET + @pytest.fixture def pc_config(project_env): country_file = project_env.project_dir / 'country_settings.yaml' @@ -55,6 +56,7 @@ ky: return project_env + def mk_query(inp): query = QueryStruct([]) phrase_split = re.split(r"([ ,:'-])", inp) @@ -80,6 +82,7 @@ def test_simple_postcode(pc_config, query, pos): assert result == {(pos, pos + 1, '45325'), (pos, pos + 1, '453 25')} + def test_contained_postcode(pc_config): parser = PostcodeParser(pc_config) @@ -87,7 +90,6 @@ def test_contained_postcode(pc_config): (0, 2, '12345 DX')} - @pytest.mark.parametrize('query,frm,to', [('345987', 0, 1), ('345 987', 0, 2), ('Aina 345 987', 1, 3), ('Aina 23 345 987 ff', 2, 4)]) @@ -98,6 +100,7 @@ def test_postcode_with_space(pc_config, query, frm, to): assert result == {(frm, to, '345987')} + def test_overlapping_postcode(pc_config): parser = PostcodeParser(pc_config) @@ -131,6 +134,7 @@ def test_postcode_with_non_matching_country_prefix(pc_config): assert not parser.parse(mk_query('ky12233')) + def test_postcode_inside_postcode_phrase(pc_config): parser = PostcodeParser(pc_config) diff --git a/test/python/api/search/test_query.py b/test/python/api/search/test_query.py index bfed38df..09f25f8e 100644 --- a/test/python/api/search/test_query.py +++ b/test/python/api/search/test_query.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Test data types for search queries. @@ -11,14 +11,15 @@ import pytest import nominatim_api.search.query as nq + def test_token_range_equal(): assert nq.TokenRange(2, 3) == nq.TokenRange(2, 3) assert not (nq.TokenRange(2, 3) != nq.TokenRange(2, 3)) @pytest.mark.parametrize('lop,rop', [((1, 2), (3, 4)), - ((3, 4), (3, 5)), - ((10, 12), (11, 12))]) + ((3, 4), (3, 5)), + ((10, 12), (11, 12))]) def test_token_range_unequal(lop, rop): assert not (nq.TokenRange(*lop) == nq.TokenRange(*rop)) assert nq.TokenRange(*lop) != nq.TokenRange(*rop) @@ -28,17 +29,17 @@ def test_token_range_lt(): assert nq.TokenRange(1, 3) < nq.TokenRange(10, 12) assert nq.TokenRange(5, 6) < nq.TokenRange(7, 8) assert nq.TokenRange(1, 4) < nq.TokenRange(4, 5) - assert not(nq.TokenRange(5, 6) < nq.TokenRange(5, 6)) - assert not(nq.TokenRange(10, 11) < nq.TokenRange(4, 5)) + assert not (nq.TokenRange(5, 6) < nq.TokenRange(5, 6)) + assert not (nq.TokenRange(10, 11) < nq.TokenRange(4, 5)) def test_token_rankge_gt(): assert nq.TokenRange(3, 4) > nq.TokenRange(1, 2) assert nq.TokenRange(100, 200) > nq.TokenRange(10, 11) assert nq.TokenRange(10, 11) > nq.TokenRange(4, 10) - assert not(nq.TokenRange(5, 6) > nq.TokenRange(5, 6)) - assert not(nq.TokenRange(1, 2) > nq.TokenRange(3, 4)) - assert not(nq.TokenRange(4, 10) > nq.TokenRange(3, 5)) + assert not (nq.TokenRange(5, 6) > nq.TokenRange(5, 6)) + assert not (nq.TokenRange(1, 2) > nq.TokenRange(3, 4)) + assert not (nq.TokenRange(4, 10) > nq.TokenRange(3, 5)) def test_token_range_unimplemented_ops(): @@ -58,8 +59,7 @@ def test_query_extract_words(): words = q.extract_words(base_penalty=1.0) assert set(words.keys()) \ - == {'12', 'ab', 'hallo', '12 ab', 'ab 12', '12 ab 12'} + == {'12', 'ab', 'hallo', '12 ab', 'ab 12', '12 ab 12'} assert sorted(words['12']) == [nq.TokenRange(0, 1, 1.0), nq.TokenRange(2, 3, 1.0)] assert words['12 ab'] == [nq.TokenRange(0, 2, 1.1)] assert words['hallo'] == [nq.TokenRange(3, 4, 1.0)] - diff --git a/test/python/api/search/test_query_analyzer_factory.py b/test/python/api/search/test_query_analyzer_factory.py index 42220b55..933bdd1f 100644 --- a/test/python/api/search/test_query_analyzer_factory.py +++ b/test/python/api/search/test_query_analyzer_factory.py @@ -2,18 +2,17 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for query analyzer creation. """ -from pathlib import Path - import pytest from nominatim_api.search.query_analyzer_factory import make_query_analyzer from nominatim_api.search.icu_tokenizer import ICUQueryAnalyzer + @pytest.mark.asyncio async def test_import_icu_tokenizer(table_factory, api): table_factory('nominatim_properties', diff --git a/test/python/api/search/test_search_country.py b/test/python/api/search/test_search_country.py index 2109ecb0..46875a2c 100644 --- a/test/python/api/search/test_search_country.py +++ b/test/python/api/search/test_search_country.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for running the country searcher. @@ -48,6 +48,7 @@ def test_find_from_placex(apiobj, frontend): assert results[0].place_id == 55 assert results[0].accuracy == 0.8 + def test_find_from_fallback_countries(apiobj, frontend): apiobj.add_country('ro', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))') apiobj.add_country_name('ro', {'name': 'România'}) @@ -87,7 +88,6 @@ class TestCountryParameters: apiobj.add_country('ro', 'POLYGON((0 0, 0 1, 1 1, 1 0, 0 0))') apiobj.add_country_name('ro', {'name': 'România'}) - @pytest.mark.parametrize('geom', [napi.GeometryFormat.GEOJSON, napi.GeometryFormat.KML, napi.GeometryFormat.SVG, @@ -100,7 +100,6 @@ class TestCountryParameters: assert len(results) == 1 assert geom.name.lower() in results[0].geometry - @pytest.mark.parametrize('pid,rids', [(76, [55]), (55, [])]) def test_exclude_place_id(self, apiobj, frontend, pid, rids): results = run_search(apiobj, frontend, 0.5, ['yw', 'ro'], @@ -108,7 +107,6 @@ class TestCountryParameters: assert [r.place_id for r in results] == rids - @pytest.mark.parametrize('viewbox,rids', [((9, 9, 11, 11), [55]), ((-10, -10, -3, -3), [])]) def test_bounded_viewbox_in_placex(self, apiobj, frontend, viewbox, rids): @@ -118,9 +116,8 @@ class TestCountryParameters: assert [r.place_id for r in results] == rids - @pytest.mark.parametrize('viewbox,numres', [((0, 0, 1, 1), 1), - ((-10, -10, -3, -3), 0)]) + ((-10, -10, -3, -3), 0)]) def test_bounded_viewbox_in_fallback(self, apiobj, frontend, viewbox, numres): results = run_search(apiobj, frontend, 0.5, ['ro'], details=SearchDetails.from_kwargs({'viewbox': viewbox, diff --git a/test/python/api/search/test_search_near.py b/test/python/api/search/test_search_near.py index 43098ddd..e9650168 100644 --- a/test/python/api/search/test_search_near.py +++ b/test/python/api/search/test_search_near.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for running the near searcher. @@ -12,8 +12,8 @@ import pytest import nominatim_api as napi from nominatim_api.types import SearchDetails from nominatim_api.search.db_searches import NearSearch, PlaceSearch -from nominatim_api.search.db_search_fields import WeightedStrings, WeightedCategories,\ - FieldLookup, FieldRanking, RankedTokens +from nominatim_api.search.db_search_fields import WeightedStrings, WeightedCategories, \ + FieldLookup from nominatim_api.search.db_search_lookups import LookupAll @@ -80,7 +80,6 @@ class TestNearSearch: apiobj.add_search_name(101, names=[56], country_code='mx', centroid=(-10.3, 56.9)) - def test_near_in_placex(self, apiobj, frontend): apiobj.add_placex(place_id=22, class_='amenity', type='bank', centroid=(5.6001, 4.2994)) @@ -91,7 +90,6 @@ class TestNearSearch: assert [r.place_id for r in results] == [22] - def test_multiple_types_near_in_placex(self, apiobj, frontend): apiobj.add_placex(place_id=22, class_='amenity', type='bank', importance=0.002, @@ -105,7 +103,6 @@ class TestNearSearch: assert [r.place_id for r in results] == [22, 23] - def test_near_in_classtype(self, apiobj, frontend): apiobj.add_placex(place_id=22, class_='amenity', type='bank', centroid=(5.6, 4.34)) @@ -118,7 +115,6 @@ class TestNearSearch: assert [r.place_id for r in results] == [22] - @pytest.mark.parametrize('cc,rid', [('us', 22), ('mx', 23)]) def test_restrict_by_country(self, apiobj, frontend, cc, rid): apiobj.add_placex(place_id=22, class_='amenity', type='bank', @@ -138,7 +134,6 @@ class TestNearSearch: assert [r.place_id for r in results] == [rid] - @pytest.mark.parametrize('excluded,rid', [(22, 122), (122, 22)]) def test_exclude_place_by_id(self, apiobj, frontend, excluded, rid): apiobj.add_placex(place_id=22, class_='amenity', type='bank', @@ -148,13 +143,11 @@ class TestNearSearch: centroid=(5.6001, 4.2994), country_code='us') - results = run_search(apiobj, frontend, 0.1, [('amenity', 'bank')], details=SearchDetails(excluded=[excluded])) assert [r.place_id for r in results] == [rid] - @pytest.mark.parametrize('layer,rids', [(napi.DataLayer.POI, [22]), (napi.DataLayer.MANMADE, [])]) def test_with_layer(self, apiobj, frontend, layer, rids): diff --git a/test/python/api/search/test_search_places.py b/test/python/api/search/test_search_places.py index c6ff16b8..ed0722c3 100644 --- a/test/python/api/search/test_search_places.py +++ b/test/python/api/search/test_search_places.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for running the generic place searcher. @@ -14,12 +14,13 @@ import pytest import nominatim_api as napi from nominatim_api.types import SearchDetails from nominatim_api.search.db_searches import PlaceSearch -from nominatim_api.search.db_search_fields import WeightedStrings, WeightedCategories,\ +from nominatim_api.search.db_search_fields import WeightedStrings, WeightedCategories, \ FieldLookup, FieldRanking, RankedTokens from nominatim_api.search.db_search_lookups import LookupAll, LookupAny, Restrict APIOPTIONS = ['search'] + def run_search(apiobj, frontend, global_penalty, lookup, ranking, count=2, hnrs=[], pcs=[], ccodes=[], quals=[], details=SearchDetails()): @@ -55,29 +56,27 @@ class TestNameOnlySearches: def fill_database(self, apiobj): apiobj.add_placex(place_id=100, country_code='us', centroid=(5.6, 4.3)) - apiobj.add_search_name(100, names=[1,2,10,11], country_code='us', + apiobj.add_search_name(100, names=[1, 2, 10, 11], country_code='us', centroid=(5.6, 4.3)) apiobj.add_placex(place_id=101, country_code='mx', centroid=(-10.3, 56.9)) - apiobj.add_search_name(101, names=[1,2,20,21], country_code='mx', + apiobj.add_search_name(101, names=[1, 2, 20, 21], country_code='mx', centroid=(-10.3, 56.9)) - @pytest.mark.parametrize('lookup_type', [LookupAll, Restrict]) @pytest.mark.parametrize('rank,res', [([10], [100, 101]), ([20], [101, 100])]) def test_lookup_all_match(self, apiobj, frontend, lookup_type, rank, res): - lookup = FieldLookup('name_vector', [1,2], lookup_type) + lookup = FieldLookup('name_vector', [1, 2], lookup_type) ranking = FieldRanking('name_vector', 0.4, [RankedTokens(0.0, rank)]) results = run_search(apiobj, frontend, 0.1, [lookup], [ranking]) assert [r.place_id for r in results] == res - @pytest.mark.parametrize('lookup_type', [LookupAll, Restrict]) def test_lookup_all_partial_match(self, apiobj, frontend, lookup_type): - lookup = FieldLookup('name_vector', [1,20], lookup_type) + lookup = FieldLookup('name_vector', [1, 20], lookup_type) ranking = FieldRanking('name_vector', 0.4, [RankedTokens(0.0, [21])]) results = run_search(apiobj, frontend, 0.1, [lookup], [ranking]) @@ -88,14 +87,13 @@ class TestNameOnlySearches: @pytest.mark.parametrize('rank,res', [([10], [100, 101]), ([20], [101, 100])]) def test_lookup_any_match(self, apiobj, frontend, rank, res): - lookup = FieldLookup('name_vector', [11,21], LookupAny) + lookup = FieldLookup('name_vector', [11, 21], LookupAny) ranking = FieldRanking('name_vector', 0.4, [RankedTokens(0.0, rank)]) results = run_search(apiobj, frontend, 0.1, [lookup], [ranking]) assert [r.place_id for r in results] == res - def test_lookup_any_partial_match(self, apiobj, frontend): lookup = FieldLookup('name_vector', [20], LookupAll) ranking = FieldRanking('name_vector', 0.4, [RankedTokens(0.0, [21])]) @@ -105,19 +103,17 @@ class TestNameOnlySearches: assert len(results) == 1 assert results[0].place_id == 101 - @pytest.mark.parametrize('cc,res', [('us', 100), ('mx', 101)]) def test_lookup_restrict_country(self, apiobj, frontend, cc, res): - lookup = FieldLookup('name_vector', [1,2], LookupAll) + lookup = FieldLookup('name_vector', [1, 2], LookupAll) ranking = FieldRanking('name_vector', 0.4, [RankedTokens(0.0, [10])]) results = run_search(apiobj, frontend, 0.1, [lookup], [ranking], ccodes=[cc]) assert [r.place_id for r in results] == [res] - def test_lookup_restrict_placeid(self, apiobj, frontend): - lookup = FieldLookup('name_vector', [1,2], LookupAll) + lookup = FieldLookup('name_vector', [1, 2], LookupAll) ranking = FieldRanking('name_vector', 0.4, [RankedTokens(0.0, [10])]) results = run_search(apiobj, frontend, 0.1, [lookup], [ranking], @@ -125,7 +121,6 @@ class TestNameOnlySearches: assert [r.place_id for r in results] == [100] - @pytest.mark.parametrize('geom', [napi.GeometryFormat.GEOJSON, napi.GeometryFormat.KML, napi.GeometryFormat.SVG, @@ -139,7 +134,6 @@ class TestNameOnlySearches: assert geom.name.lower() in results[0].geometry - @pytest.mark.parametrize('factor,npoints', [(0.0, 3), (1.0, 2)]) def test_return_simplified_geometry(self, apiobj, frontend, factor, npoints): apiobj.add_placex(place_id=333, country_code='us', @@ -162,7 +156,6 @@ class TestNameOnlySearches: assert result.place_id == 333 assert len(geom['coordinates']) == npoints - @pytest.mark.parametrize('viewbox', ['5.0,4.0,6.0,5.0', '5.7,4.0,6.0,5.0']) @pytest.mark.parametrize('wcount,rids', [(2, [100, 101]), (20000, [100])]) def test_prefer_viewbox(self, apiobj, frontend, viewbox, wcount, rids): @@ -177,18 +170,16 @@ class TestNameOnlySearches: details=SearchDetails.from_kwargs({'viewbox': viewbox})) assert [r.place_id for r in results] == rids - @pytest.mark.parametrize('viewbox', ['5.0,4.0,6.0,5.0', '5.55,4.27,5.62,4.31']) def test_force_viewbox(self, apiobj, frontend, viewbox): lookup = FieldLookup('name_vector', [1, 2], LookupAll) - details=SearchDetails.from_kwargs({'viewbox': viewbox, - 'bounded_viewbox': True}) + details = SearchDetails.from_kwargs({'viewbox': viewbox, + 'bounded_viewbox': True}) results = run_search(apiobj, frontend, 0.1, [lookup], [], details=details) assert [r.place_id for r in results] == [100] - def test_prefer_near(self, apiobj, frontend): lookup = FieldLookup('name_vector', [1, 2], LookupAll) ranking = FieldRanking('name_vector', 0.4, [RankedTokens(0.0, [21])]) @@ -202,13 +193,12 @@ class TestNameOnlySearches: results.sort(key=lambda r: -r.importance) assert [r.place_id for r in results] == [100, 101] - @pytest.mark.parametrize('radius', [0.09, 0.11]) def test_force_near(self, apiobj, frontend, radius): lookup = FieldLookup('name_vector', [1, 2], LookupAll) - details=SearchDetails.from_kwargs({'near': '5.6,4.3', - 'near_radius': radius}) + details = SearchDetails.from_kwargs({'near': '5.6,4.3', + 'near_radius': radius}) results = run_search(apiobj, frontend, 0.1, [lookup], [], details=details) @@ -228,7 +218,7 @@ class TestStreetWithHousenumber: apiobj.add_placex(place_id=1000, class_='highway', type='residential', rank_search=26, rank_address=26, country_code='es') - apiobj.add_search_name(1000, names=[1,2,10,11], + apiobj.add_search_name(1000, names=[1, 2, 10, 11], search_rank=26, address_rank=26, country_code='es') apiobj.add_placex(place_id=91, class_='place', type='house', @@ -243,26 +233,24 @@ class TestStreetWithHousenumber: apiobj.add_placex(place_id=2000, class_='highway', type='residential', rank_search=26, rank_address=26, country_code='pt') - apiobj.add_search_name(2000, names=[1,2,20,21], + apiobj.add_search_name(2000, names=[1, 2, 20, 21], search_rank=26, address_rank=26, country_code='pt') - @pytest.mark.parametrize('hnr,res', [('20', [91, 1]), ('20 a', [1]), ('21', [2]), ('22', [2, 92]), ('24', [93]), ('25', [])]) def test_lookup_by_single_housenumber(self, apiobj, frontend, hnr, res): - lookup = FieldLookup('name_vector', [1,2], LookupAll) + lookup = FieldLookup('name_vector', [1, 2], LookupAll) ranking = FieldRanking('name_vector', 0.3, [RankedTokens(0.0, [10])]) results = run_search(apiobj, frontend, 0.1, [lookup], [ranking], hnrs=[hnr]) assert [r.place_id for r in results] == res + [1000, 2000] - @pytest.mark.parametrize('cc,res', [('es', [2, 1000]), ('pt', [92, 2000])]) def test_lookup_with_country_restriction(self, apiobj, frontend, cc, res): - lookup = FieldLookup('name_vector', [1,2], LookupAll) + lookup = FieldLookup('name_vector', [1, 2], LookupAll) ranking = FieldRanking('name_vector', 0.3, [RankedTokens(0.0, [10])]) results = run_search(apiobj, frontend, 0.1, [lookup], [ranking], hnrs=['22'], @@ -270,9 +258,8 @@ class TestStreetWithHousenumber: assert [r.place_id for r in results] == res - def test_lookup_exclude_housenumber_placeid(self, apiobj, frontend): - lookup = FieldLookup('name_vector', [1,2], LookupAll) + lookup = FieldLookup('name_vector', [1, 2], LookupAll) ranking = FieldRanking('name_vector', 0.3, [RankedTokens(0.0, [10])]) results = run_search(apiobj, frontend, 0.1, [lookup], [ranking], hnrs=['22'], @@ -280,9 +267,8 @@ class TestStreetWithHousenumber: assert [r.place_id for r in results] == [2, 1000, 2000] - def test_lookup_exclude_street_placeid(self, apiobj, frontend): - lookup = FieldLookup('name_vector', [1,2], LookupAll) + lookup = FieldLookup('name_vector', [1, 2], LookupAll) ranking = FieldRanking('name_vector', 0.3, [RankedTokens(0.0, [10])]) results = run_search(apiobj, frontend, 0.1, [lookup], [ranking], hnrs=['22'], @@ -290,9 +276,8 @@ class TestStreetWithHousenumber: assert [r.place_id for r in results] == [2, 92, 2000] - def test_lookup_only_house_qualifier(self, apiobj, frontend): - lookup = FieldLookup('name_vector', [1,2], LookupAll) + lookup = FieldLookup('name_vector', [1, 2], LookupAll) ranking = FieldRanking('name_vector', 0.3, [RankedTokens(0.0, [10])]) results = run_search(apiobj, frontend, 0.1, [lookup], [ranking], hnrs=['22'], @@ -300,9 +285,8 @@ class TestStreetWithHousenumber: assert [r.place_id for r in results] == [2, 92] - def test_lookup_only_street_qualifier(self, apiobj, frontend): - lookup = FieldLookup('name_vector', [1,2], LookupAll) + lookup = FieldLookup('name_vector', [1, 2], LookupAll) ranking = FieldRanking('name_vector', 0.3, [RankedTokens(0.0, [10])]) results = run_search(apiobj, frontend, 0.1, [lookup], [ranking], hnrs=['22'], @@ -310,10 +294,9 @@ class TestStreetWithHousenumber: assert [r.place_id for r in results] == [1000, 2000] - @pytest.mark.parametrize('rank,found', [(26, True), (27, False), (30, False)]) def test_lookup_min_rank(self, apiobj, frontend, rank, found): - lookup = FieldLookup('name_vector', [1,2], LookupAll) + lookup = FieldLookup('name_vector', [1, 2], LookupAll) ranking = FieldRanking('name_vector', 0.3, [RankedTokens(0.0, [10])]) results = run_search(apiobj, frontend, 0.1, [lookup], [ranking], hnrs=['22'], @@ -321,7 +304,6 @@ class TestStreetWithHousenumber: assert [r.place_id for r in results] == ([2, 92, 1000, 2000] if found else [2, 92]) - @pytest.mark.parametrize('geom', [napi.GeometryFormat.GEOJSON, napi.GeometryFormat.KML, napi.GeometryFormat.SVG, @@ -343,7 +325,7 @@ def test_very_large_housenumber(apiobj, frontend): apiobj.add_placex(place_id=2000, class_='highway', type='residential', rank_search=26, rank_address=26, country_code='pt') - apiobj.add_search_name(2000, names=[1,2], + apiobj.add_search_name(2000, names=[1, 2], search_rank=26, address_rank=26, country_code='pt') @@ -405,7 +387,6 @@ class TestInterpolations: centroid=(10.0, 10.00001), geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)') - @pytest.mark.parametrize('hnr,res', [('21', [992]), ('22', []), ('23', [991])]) def test_lookup_housenumber(self, apiobj, frontend, hnr, res): lookup = FieldLookup('name_vector', [111], LookupAll) @@ -414,7 +395,6 @@ class TestInterpolations: assert [r.place_id for r in results] == res + [990] - @pytest.mark.parametrize('geom', [napi.GeometryFormat.GEOJSON, napi.GeometryFormat.KML, napi.GeometryFormat.SVG, @@ -429,7 +409,6 @@ class TestInterpolations: assert geom.name.lower() in results[0].geometry - class TestTiger: @pytest.fixture(autouse=True) @@ -453,7 +432,6 @@ class TestTiger: centroid=(10.0, 10.00001), geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)') - @pytest.mark.parametrize('hnr,res', [('21', [992]), ('22', []), ('23', [991])]) def test_lookup_housenumber(self, apiobj, frontend, hnr, res): lookup = FieldLookup('name_vector', [111], LookupAll) @@ -462,7 +440,6 @@ class TestTiger: assert [r.place_id for r in results] == res + [990] - @pytest.mark.parametrize('geom', [napi.GeometryFormat.GEOJSON, napi.GeometryFormat.KML, napi.GeometryFormat.SVG, @@ -513,15 +490,15 @@ class TestLayersRank30: importance=0.0005, address_rank=0, search_rank=30) - - @pytest.mark.parametrize('layer,res', [(napi.DataLayer.ADDRESS, [223]), - (napi.DataLayer.POI, [224]), - (napi.DataLayer.ADDRESS | napi.DataLayer.POI, [223, 224]), - (napi.DataLayer.MANMADE, [225]), - (napi.DataLayer.RAILWAY, [226]), - (napi.DataLayer.NATURAL, [227]), - (napi.DataLayer.MANMADE | napi.DataLayer.NATURAL, [225, 227]), - (napi.DataLayer.MANMADE | napi.DataLayer.RAILWAY, [225, 226])]) + @pytest.mark.parametrize('layer,res', + [(napi.DataLayer.ADDRESS, [223]), + (napi.DataLayer.POI, [224]), + (napi.DataLayer.ADDRESS | napi.DataLayer.POI, [223, 224]), + (napi.DataLayer.MANMADE, [225]), + (napi.DataLayer.RAILWAY, [226]), + (napi.DataLayer.NATURAL, [227]), + (napi.DataLayer.MANMADE | napi.DataLayer.NATURAL, [225, 227]), + (napi.DataLayer.MANMADE | napi.DataLayer.RAILWAY, [225, 226])]) def test_layers_rank30(self, apiobj, frontend, layer, res): lookup = FieldLookup('name_vector', [34], LookupAny) diff --git a/test/python/api/search/test_search_poi.py b/test/python/api/search/test_search_poi.py index d4319a57..9387385e 100644 --- a/test/python/api/search/test_search_poi.py +++ b/test/python/api/search/test_search_poi.py @@ -2,14 +2,13 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for running the POI searcher. """ import pytest -import nominatim_api as napi from nominatim_api.types import SearchDetails from nominatim_api.search.db_searches import PoiSearch from nominatim_api.search.db_search_fields import WeightedStrings, WeightedCategories @@ -84,14 +83,12 @@ class TestPoiSearchWithRestrictions: else: self.args = {'near': '34.3, 56.100021', 'near_radius': 0.001} - def test_unrestricted(self, apiobj, frontend): results = run_search(apiobj, frontend, 0.1, [('highway', 'bus_stop')], [0.5], details=SearchDetails.from_kwargs(self.args)) assert [r.place_id for r in results] == [1, 2] - def test_restict_country(self, apiobj, frontend): results = run_search(apiobj, frontend, 0.1, [('highway', 'bus_stop')], [0.5], ccodes=['de', 'nz'], @@ -99,7 +96,6 @@ class TestPoiSearchWithRestrictions: assert [r.place_id for r in results] == [2] - def test_restrict_by_viewbox(self, apiobj, frontend): args = {'bounded_viewbox': True, 'viewbox': '34.299,56.0,34.3001,56.10001'} args.update(self.args) diff --git a/test/python/api/search/test_search_postcode.py b/test/python/api/search/test_search_postcode.py index 369e1504..529fb409 100644 --- a/test/python/api/search/test_search_postcode.py +++ b/test/python/api/search/test_search_postcode.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for running the postcode searcher. @@ -15,6 +15,7 @@ from nominatim_api.search.db_searches import PostcodeSearch from nominatim_api.search.db_search_fields import WeightedStrings, FieldLookup, \ FieldRanking, RankedTokens + def run_search(apiobj, frontend, global_penalty, pcs, pc_penalties=None, ccodes=[], lookup=[], ranking=[], details=SearchDetails()): if pc_penalties is None: @@ -85,26 +86,24 @@ class TestPostcodeSearchWithAddress: apiobj.add_placex(place_id=1000, class_='place', type='village', rank_search=22, rank_address=22, country_code='ch') - apiobj.add_search_name(1000, names=[1,2,10,11], + apiobj.add_search_name(1000, names=[1, 2, 10, 11], search_rank=22, address_rank=22, country_code='ch') apiobj.add_placex(place_id=2000, class_='place', type='village', rank_search=22, rank_address=22, country_code='pl') - apiobj.add_search_name(2000, names=[1,2,20,21], + apiobj.add_search_name(2000, names=[1, 2, 20, 21], search_rank=22, address_rank=22, country_code='pl') - def test_lookup_both(self, apiobj, frontend): - lookup = FieldLookup('name_vector', [1,2], 'restrict') + lookup = FieldLookup('name_vector', [1, 2], 'restrict') ranking = FieldRanking('name_vector', 0.3, [RankedTokens(0.0, [10])]) results = run_search(apiobj, frontend, 0.1, ['12345'], lookup=[lookup], ranking=[ranking]) assert [r.place_id for r in results] == [100, 101] - def test_restrict_by_name(self, apiobj, frontend): lookup = FieldLookup('name_vector', [10], 'restrict') @@ -112,11 +111,10 @@ class TestPostcodeSearchWithAddress: assert [r.place_id for r in results] == [100] - @pytest.mark.parametrize('coord,place_id', [((16.5, 5), 100), ((-45.1, 7.004), 101)]) def test_lookup_near(self, apiobj, frontend, coord, place_id): - lookup = FieldLookup('name_vector', [1,2], 'restrict') + lookup = FieldLookup('name_vector', [1, 2], 'restrict') ranking = FieldRanking('name_vector', 0.3, [RankedTokens(0.0, [10])]) results = run_search(apiobj, frontend, 0.1, ['12345'], @@ -126,7 +124,6 @@ class TestPostcodeSearchWithAddress: assert [r.place_id for r in results] == [place_id] - @pytest.mark.parametrize('geom', [napi.GeometryFormat.GEOJSON, napi.GeometryFormat.KML, napi.GeometryFormat.SVG, @@ -138,18 +135,16 @@ class TestPostcodeSearchWithAddress: assert results assert all(geom.name.lower() in r.geometry for r in results) - - @pytest.mark.parametrize('viewbox, rids', [('-46,6,-44,8', [101,100]), - ('16,4,18,6', [100,101])]) + @pytest.mark.parametrize('viewbox, rids', [('-46,6,-44,8', [101, 100]), + ('16,4,18,6', [100, 101])]) def test_prefer_viewbox(self, apiobj, frontend, viewbox, rids): results = run_search(apiobj, frontend, 0.1, ['12345'], details=SearchDetails.from_kwargs({'viewbox': viewbox})) assert [r.place_id for r in results] == rids - @pytest.mark.parametrize('viewbox, rid', [('-46,6,-44,8', 101), - ('16,4,18,6', 100)]) + ('16,4,18,6', 100)]) def test_restrict_to_viewbox(self, apiobj, frontend, viewbox, rid): results = run_search(apiobj, frontend, 0.1, ['12345'], details=SearchDetails.from_kwargs({'viewbox': viewbox, @@ -157,7 +152,6 @@ class TestPostcodeSearchWithAddress: assert [r.place_id for r in results] == [rid] - @pytest.mark.parametrize('coord,rids', [((17.05, 5), [100, 101]), ((-45, 7.1), [101, 100])]) def test_prefer_near(self, apiobj, frontend, coord, rids): @@ -166,7 +160,6 @@ class TestPostcodeSearchWithAddress: assert [r.place_id for r in results] == rids - @pytest.mark.parametrize('pid,rid', [(100, 101), (101, 100)]) def test_exclude(self, apiobj, frontend, pid, rid): results = run_search(apiobj, frontend, 0.1, ['12345'], diff --git a/test/python/api/search/test_token_assignment.py b/test/python/api/search/test_token_assignment.py index fff8d471..2ffba335 100644 --- a/test/python/api/search/test_token_assignment.py +++ b/test/python/api/search/test_token_assignment.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Test for creation of token assignments from tokenized queries. @@ -11,7 +11,10 @@ import pytest from nominatim_api.search.query import QueryStruct, Phrase, TokenRange, Token import nominatim_api.search.query as qmod -from nominatim_api.search.token_assignment import yield_token_assignments, TokenAssignment, PENALTY_TOKENCHANGE +from nominatim_api.search.token_assignment import (yield_token_assignments, + TokenAssignment, + PENALTY_TOKENCHANGE) + class MyToken(Token): def get_category(self): @@ -102,8 +105,7 @@ def test_multiple_simple_words(btype): TokenAssignment(penalty=penalty, name=TokenRange(1, 3), address=[TokenRange(0, 1)]), TokenAssignment(penalty=penalty, name=TokenRange(2, 3), - address=[TokenRange(0, 2)]) - ) + address=[TokenRange(0, 2)])) def test_multiple_words_respect_phrase_break(): @@ -156,6 +158,7 @@ def test_housenumber_and_postcode(): address=[TokenRange(0, 1), TokenRange(2, 3)], postcode=TokenRange(3, 4))) + def test_postcode_and_housenumber(): q = make_query((qmod.BREAK_START, qmod.PHRASE_ANY, [(1, qmod.TOKEN_PARTIAL)]), (qmod.BREAK_WORD, qmod.PHRASE_ANY, [(2, qmod.TOKEN_POSTCODE)]), @@ -211,11 +214,11 @@ def test_housenumber_many_phrases(): check_assignments(yield_token_assignments(q), TokenAssignment(penalty=0.1, name=TokenRange(4, 5), - housenumber=TokenRange(3, 4),\ + housenumber=TokenRange(3, 4), address=[TokenRange(0, 1), TokenRange(1, 2), TokenRange(2, 3)]), TokenAssignment(penalty=0.1, - housenumber=TokenRange(3, 4),\ + housenumber=TokenRange(3, 4), address=[TokenRange(0, 1), TokenRange(1, 2), TokenRange(2, 3), TokenRange(4, 5)])) @@ -299,7 +302,6 @@ def test_qualifier_at_beginning(): (qmod.BREAK_WORD, qmod.PHRASE_ANY, [(2, qmod.TOKEN_PARTIAL)]), (qmod.BREAK_WORD, qmod.PHRASE_ANY, [(3, qmod.TOKEN_PARTIAL)])) - check_assignments(yield_token_assignments(q), TokenAssignment(penalty=0.1, name=TokenRange(1, 3), qualifier=TokenRange(0, 1)), @@ -315,7 +317,6 @@ def test_qualifier_after_name(): (qmod.BREAK_WORD, qmod.PHRASE_ANY, [(4, qmod.TOKEN_PARTIAL)]), (qmod.BREAK_WORD, qmod.PHRASE_ANY, [(5, qmod.TOKEN_PARTIAL)])) - check_assignments(yield_token_assignments(q), TokenAssignment(penalty=0.2, name=TokenRange(0, 2), qualifier=TokenRange(2, 3), @@ -349,4 +350,3 @@ def test_qualifier_in_middle_of_phrase(): (qmod.BREAK_PHRASE, qmod.PHRASE_ANY, [(5, qmod.TOKEN_PARTIAL)])) check_assignments(yield_token_assignments(q)) - diff --git a/test/python/api/test_api_connection.py b/test/python/api/test_api_connection.py index f62b0d9e..9b29411a 100644 --- a/test/python/api/test_api_connection.py +++ b/test/python/api/test_api_connection.py @@ -2,12 +2,11 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for enhanced connection class for API functions. """ -from pathlib import Path import pytest import sqlalchemy as sa @@ -76,7 +75,7 @@ async def test_get_db_property_existing(api): @pytest.mark.asyncio -async def test_get_db_property_existing(api): +async def test_get_db_property_bad_name(api): async with api.begin() as conn: with pytest.raises(ValueError): await conn.get_db_property('dfkgjd.rijg') diff --git a/test/python/api/test_api_deletable_v1.py b/test/python/api/test_api_deletable_v1.py index 9e113886..8ea4c9cd 100644 --- a/test/python/api/test_api_deletable_v1.py +++ b/test/python/api/test_api_deletable_v1.py @@ -2,20 +2,20 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for the deletable v1 API call. """ import json -from pathlib import Path import pytest -from fake_adaptor import FakeAdaptor, FakeError, FakeResponse +from fake_adaptor import FakeAdaptor import nominatim_api.v1.server_glue as glue + class TestDeletableEndPoint: @pytest.fixture(autouse=True) @@ -25,14 +25,13 @@ class TestDeletableEndPoint: content=[(345, 'N', 'boundary', 'administrative'), (781, 'R', 'landuse', 'wood'), (781, 'R', 'landcover', 'grass')]) - table_factory('placex', - definition="""place_id bigint, osm_id bigint, osm_type char(1), - class text, type text, name HSTORE, country_code char(2)""", - content=[(1, 345, 'N', 'boundary', 'administrative', {'old_name': 'Former'}, 'ab'), - (2, 781, 'R', 'landuse', 'wood', {'name': 'Wood'}, 'cd'), - (3, 781, 'R', 'landcover', 'grass', None, 'cd')]) - - + table_factory( + 'placex', + definition="""place_id bigint, osm_id bigint, osm_type char(1), + class text, type text, name HSTORE, country_code char(2)""", + content=[(1, 345, 'N', 'boundary', 'administrative', {'old_name': 'Former'}, 'ab'), + (2, 781, 'R', 'landuse', 'wood', {'name': 'Wood'}, 'cd'), + (3, 781, 'R', 'landcover', 'grass', None, 'cd')]) @pytest.mark.asyncio async def test_deletable(self, api): diff --git a/test/python/api/test_api_details.py b/test/python/api/test_api_details.py index 7f405728..4f6dd92b 100644 --- a/test/python/api/test_api_details.py +++ b/test/python/api/test_api_details.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for details API call. @@ -13,23 +13,24 @@ import pytest import nominatim_api as napi + @pytest.mark.parametrize('idobj', (napi.PlaceID(332), napi.OsmID('W', 4), napi.OsmID('W', 4, 'highway'))) def test_lookup_in_placex(apiobj, frontend, idobj): import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0) apiobj.add_placex(place_id=332, osm_type='W', osm_id=4, - class_='highway', type='residential', - name={'name': 'Road'}, address={'city': 'Barrow'}, - extratags={'surface': 'paved'}, - parent_place_id=34, linked_place_id=55, - admin_level=15, country_code='gb', - housenumber='4', - postcode='34425', wikipedia='en:Faa', - rank_search=27, rank_address=26, - importance=0.01, - centroid=(23, 34), - indexed_date=import_date, - geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)') + class_='highway', type='residential', + name={'name': 'Road'}, address={'city': 'Barrow'}, + extratags={'surface': 'paved'}, + parent_place_id=34, linked_place_id=55, + admin_level=15, country_code='gb', + housenumber='4', + postcode='34425', wikipedia='en:Faa', + rank_search=27, rank_address=26, + importance=0.01, + centroid=(23, 34), + indexed_date=import_date, + geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)') api = frontend(apiobj, options={'details'}) result = api.details(idobj) @@ -73,12 +74,12 @@ def test_lookup_in_placex(apiobj, frontend, idobj): def test_lookup_in_placex_minimal_info(apiobj, frontend): import_date = dt.datetime(2022, 12, 7, 14, 14, 46, 0) apiobj.add_placex(place_id=332, osm_type='W', osm_id=4, - class_='highway', type='residential', - admin_level=15, - rank_search=27, rank_address=26, - centroid=(23, 34), - indexed_date=import_date, - geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)') + class_='highway', type='residential', + admin_level=15, + rank_search=27, rank_address=26, + centroid=(23, 34), + indexed_date=import_date, + geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)') api = frontend(apiobj, options={'details'}) result = api.details(napi.PlaceID(332)) @@ -131,9 +132,9 @@ def test_lookup_in_placex_with_geometry(apiobj, frontend): def test_lookup_placex_with_address_details(apiobj, frontend): apiobj.add_placex(place_id=332, osm_type='W', osm_id=4, - class_='highway', type='residential', name='Street', - country_code='pl', - rank_search=27, rank_address=26) + class_='highway', type='residential', name='Street', + country_code='pl', + rank_search=27, rank_address=26) apiobj.add_address_placex(332, fromarea=False, isaddress=False, distance=0.0034, place_id=1000, osm_type='N', osm_id=3333, @@ -178,9 +179,9 @@ def test_lookup_placex_with_address_details(apiobj, frontend): def test_lookup_place_with_linked_places_none_existing(apiobj, frontend): apiobj.add_placex(place_id=332, osm_type='W', osm_id=4, - class_='highway', type='residential', name='Street', - country_code='pl', linked_place_id=45, - rank_search=27, rank_address=26) + class_='highway', type='residential', name='Street', + country_code='pl', linked_place_id=45, + rank_search=27, rank_address=26) api = frontend(apiobj, options={'details'}) result = api.details(napi.PlaceID(332), linked_places=True) @@ -190,17 +191,17 @@ def test_lookup_place_with_linked_places_none_existing(apiobj, frontend): def test_lookup_place_with_linked_places_existing(apiobj, frontend): apiobj.add_placex(place_id=332, osm_type='W', osm_id=4, - class_='highway', type='residential', name='Street', - country_code='pl', linked_place_id=45, - rank_search=27, rank_address=26) + class_='highway', type='residential', name='Street', + country_code='pl', linked_place_id=45, + rank_search=27, rank_address=26) apiobj.add_placex(place_id=1001, osm_type='W', osm_id=5, - class_='highway', type='residential', name='Street', - country_code='pl', linked_place_id=332, - rank_search=27, rank_address=26) + class_='highway', type='residential', name='Street', + country_code='pl', linked_place_id=332, + rank_search=27, rank_address=26) apiobj.add_placex(place_id=1002, osm_type='W', osm_id=6, - class_='highway', type='residential', name='Street', - country_code='pl', linked_place_id=332, - rank_search=27, rank_address=26) + class_='highway', type='residential', name='Street', + country_code='pl', linked_place_id=332, + rank_search=27, rank_address=26) api = frontend(apiobj, options={'details'}) result = api.details(napi.PlaceID(332), linked_places=True) @@ -221,9 +222,9 @@ def test_lookup_place_with_linked_places_existing(apiobj, frontend): def test_lookup_place_with_parented_places_not_existing(apiobj, frontend): apiobj.add_placex(place_id=332, osm_type='W', osm_id=4, - class_='highway', type='residential', name='Street', - country_code='pl', parent_place_id=45, - rank_search=27, rank_address=26) + class_='highway', type='residential', name='Street', + country_code='pl', parent_place_id=45, + rank_search=27, rank_address=26) api = frontend(apiobj, options={'details'}) result = api.details(napi.PlaceID(332), parented_places=True) @@ -233,17 +234,17 @@ def test_lookup_place_with_parented_places_not_existing(apiobj, frontend): def test_lookup_place_with_parented_places_existing(apiobj, frontend): apiobj.add_placex(place_id=332, osm_type='W', osm_id=4, - class_='highway', type='residential', name='Street', - country_code='pl', parent_place_id=45, - rank_search=27, rank_address=26) + class_='highway', type='residential', name='Street', + country_code='pl', parent_place_id=45, + rank_search=27, rank_address=26) apiobj.add_placex(place_id=1001, osm_type='N', osm_id=5, - class_='place', type='house', housenumber='23', - country_code='pl', parent_place_id=332, - rank_search=30, rank_address=30) + class_='place', type='house', housenumber='23', + country_code='pl', parent_place_id=332, + rank_search=30, rank_address=30) apiobj.add_placex(place_id=1002, osm_type='W', osm_id=6, - class_='highway', type='residential', name='Street', - country_code='pl', parent_place_id=332, - rank_search=27, rank_address=26) + class_='highway', type='residential', name='Street', + country_code='pl', parent_place_id=332, + rank_search=27, rank_address=26) api = frontend(apiobj, options={'details'}) result = api.details(napi.PlaceID(332), parented_places=True) @@ -332,9 +333,9 @@ def test_lookup_osmline_with_address_details(apiobj, frontend): startnumber=2, endnumber=4, step=1, parent_place_id=332) apiobj.add_placex(place_id=332, osm_type='W', osm_id=4, - class_='highway', type='residential', name='Street', - country_code='pl', - rank_search=27, rank_address=26) + class_='highway', type='residential', name='Street', + country_code='pl', + rank_search=27, rank_address=26) apiobj.add_address_placex(332, fromarea=False, isaddress=False, distance=0.0034, place_id=1000, osm_type='N', osm_id=3333, @@ -432,9 +433,9 @@ def test_lookup_tiger_with_address_details(apiobj, frontend): startnumber=2, endnumber=4, step=1, parent_place_id=332) apiobj.add_placex(place_id=332, osm_type='W', osm_id=4, - class_='highway', type='residential', name='Street', - country_code='us', - rank_search=27, rank_address=26) + class_='highway', type='residential', name='Street', + country_code='us', + rank_search=27, rank_address=26) apiobj.add_address_placex(332, fromarea=False, isaddress=False, distance=0.0034, place_id=1000, osm_type='N', osm_id=3333, @@ -571,6 +572,7 @@ def test_lookup_postcode_with_address_details(apiobj, frontend): rank_address=4, distance=0.0) ] + @pytest.mark.parametrize('objid', [napi.PlaceID(1736), napi.OsmID('W', 55), napi.OsmID('N', 55, 'amenity')]) @@ -583,8 +585,8 @@ def test_lookup_missing_object(apiobj, frontend, objid): @pytest.mark.parametrize('gtype', (napi.GeometryFormat.KML, - napi.GeometryFormat.SVG, - napi.GeometryFormat.TEXT)) + napi.GeometryFormat.SVG, + napi.GeometryFormat.TEXT)) def test_lookup_unsupported_geometry(apiobj, frontend, gtype): apiobj.add_placex(place_id=332) diff --git a/test/python/api/test_api_lookup.py b/test/python/api/test_api_lookup.py index 4281cd6c..a2660f51 100644 --- a/test/python/api/test_api_lookup.py +++ b/test/python/api/test_api_lookup.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for lookup API call. @@ -13,6 +13,7 @@ import pytest import nominatim_api as napi + def test_lookup_empty_list(apiobj, frontend): api = frontend(apiobj, options={'details'}) assert api.lookup([]) == [] @@ -28,17 +29,17 @@ def test_lookup_non_existing(apiobj, frontend): napi.OsmID('W', 4, 'highway'))) def test_lookup_single_placex(apiobj, frontend, idobj): apiobj.add_placex(place_id=332, osm_type='W', osm_id=4, - class_='highway', type='residential', - name={'name': 'Road'}, address={'city': 'Barrow'}, - extratags={'surface': 'paved'}, - parent_place_id=34, linked_place_id=55, - admin_level=15, country_code='gb', - housenumber='4', - postcode='34425', wikipedia='en:Faa', - rank_search=27, rank_address=26, - importance=0.01, - centroid=(23, 34), - geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)') + class_='highway', type='residential', + name={'name': 'Road'}, address={'city': 'Barrow'}, + extratags={'surface': 'paved'}, + parent_place_id=34, linked_place_id=55, + admin_level=15, country_code='gb', + housenumber='4', + postcode='34425', wikipedia='en:Faa', + rank_search=27, rank_address=26, + importance=0.01, + centroid=(23, 34), + geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)') api = frontend(apiobj, options={'details'}) result = api.lookup([idobj]) @@ -79,17 +80,17 @@ def test_lookup_single_placex(apiobj, frontend, idobj): def test_lookup_multiple_places(apiobj, frontend): apiobj.add_placex(place_id=332, osm_type='W', osm_id=4, - class_='highway', type='residential', - name={'name': 'Road'}, address={'city': 'Barrow'}, - extratags={'surface': 'paved'}, - parent_place_id=34, linked_place_id=55, - admin_level=15, country_code='gb', - housenumber='4', - postcode='34425', wikipedia='en:Faa', - rank_search=27, rank_address=26, - importance=0.01, - centroid=(23, 34), - geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)') + class_='highway', type='residential', + name={'name': 'Road'}, address={'city': 'Barrow'}, + extratags={'surface': 'paved'}, + parent_place_id=34, linked_place_id=55, + admin_level=15, country_code='gb', + housenumber='4', + postcode='34425', wikipedia='en:Faa', + rank_search=27, rank_address=26, + importance=0.01, + centroid=(23, 34), + geometry='LINESTRING(23 34, 23.1 34, 23.1 34.1, 23 34)') apiobj.add_osmline(place_id=4924, osm_id=9928, parent_place_id=12, startnumber=1, endnumber=4, step=1, @@ -97,7 +98,6 @@ def test_lookup_multiple_places(apiobj, frontend): address={'city': 'Big'}, geometry='LINESTRING(23 34, 23 35)') - api = frontend(apiobj, options={'details'}) result = api.lookup((napi.OsmID('W', 1), napi.OsmID('W', 4), @@ -111,17 +111,17 @@ def test_lookup_multiple_places(apiobj, frontend): @pytest.mark.parametrize('gtype', list(napi.GeometryFormat)) def test_simple_place_with_geometry(apiobj, frontend, gtype): apiobj.add_placex(place_id=332, osm_type='W', osm_id=4, - class_='highway', type='residential', - name={'name': 'Road'}, address={'city': 'Barrow'}, - extratags={'surface': 'paved'}, - parent_place_id=34, linked_place_id=55, - admin_level=15, country_code='gb', - housenumber='4', - postcode='34425', wikipedia='en:Faa', - rank_search=27, rank_address=26, - importance=0.01, - centroid=(23, 34), - geometry='POLYGON((23 34, 23.1 34, 23.1 34.1, 23 34))') + class_='highway', type='residential', + name={'name': 'Road'}, address={'city': 'Barrow'}, + extratags={'surface': 'paved'}, + parent_place_id=34, linked_place_id=55, + admin_level=15, country_code='gb', + housenumber='4', + postcode='34425', wikipedia='en:Faa', + rank_search=27, rank_address=26, + importance=0.01, + centroid=(23, 34), + geometry='POLYGON((23 34, 23.1 34, 23.1 34.1, 23 34))') api = frontend(apiobj, options={'details'}) result = api.lookup([napi.OsmID('W', 4)], geometry_output=gtype) @@ -137,17 +137,17 @@ def test_simple_place_with_geometry(apiobj, frontend, gtype): def test_simple_place_with_geometry_simplified(apiobj, frontend): apiobj.add_placex(place_id=332, osm_type='W', osm_id=4, - class_='highway', type='residential', - name={'name': 'Road'}, address={'city': 'Barrow'}, - extratags={'surface': 'paved'}, - parent_place_id=34, linked_place_id=55, - admin_level=15, country_code='gb', - housenumber='4', - postcode='34425', wikipedia='en:Faa', - rank_search=27, rank_address=26, - importance=0.01, - centroid=(23, 34), - geometry='POLYGON((23 34, 22.999 34, 23.1 34, 23.1 34.1, 23 34))') + class_='highway', type='residential', + name={'name': 'Road'}, address={'city': 'Barrow'}, + extratags={'surface': 'paved'}, + parent_place_id=34, linked_place_id=55, + admin_level=15, country_code='gb', + housenumber='4', + postcode='34425', wikipedia='en:Faa', + rank_search=27, rank_address=26, + importance=0.01, + centroid=(23, 34), + geometry='POLYGON((23 34, 22.999 34, 23.1 34, 23.1 34.1, 23 34))') api = frontend(apiobj, options={'details'}) result = api.lookup([napi.OsmID('W', 4)], @@ -159,5 +159,5 @@ def test_simple_place_with_geometry_simplified(apiobj, frontend): geom = json.loads(result[0].geometry['geojson']) - assert geom['type'] == 'Polygon' + assert geom['type'] == 'Polygon' assert geom['coordinates'] == [[[23, 34], [23.1, 34], [23.1, 34.1], [23, 34]]] diff --git a/test/python/api/test_api_polygons_v1.py b/test/python/api/test_api_polygons_v1.py index ac2b4cb9..e4700a95 100644 --- a/test/python/api/test_api_polygons_v1.py +++ b/test/python/api/test_api_polygons_v1.py @@ -2,21 +2,21 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for the deletable v1 API call. """ import json import datetime as dt -from pathlib import Path import pytest -from fake_adaptor import FakeAdaptor, FakeError, FakeResponse +from fake_adaptor import FakeAdaptor import nominatim_api.v1.server_glue as glue + class TestPolygonsEndPoint: @pytest.fixture(autouse=True) @@ -35,13 +35,12 @@ class TestPolygonsEndPoint: errormessage text, prevgeometry geometry(Geometry,4326), newgeometry geometry(Geometry,4326)""", - content=[(345, 'N', 'boundary', 'administrative', - {'name': 'Foo'}, 'xx', self.recent, - 'some text', None, None), - (781, 'R', 'landuse', 'wood', - None, 'ds', self.now, - 'Area reduced by lots', None, None)]) - + content=[(345, 'N', 'boundary', 'administrative', + {'name': 'Foo'}, 'xx', self.recent, + 'some text', None, None), + (781, 'R', 'landuse', 'wood', + None, 'ds', self.now, + 'Area reduced by lots', None, None)]) @pytest.mark.asyncio async def test_polygons_simple(self, api): @@ -63,7 +62,6 @@ class TestPolygonsEndPoint: 'errormessage': 'Area reduced by lots', 'updated': self.now.isoformat(sep=' ', timespec='seconds')}] - @pytest.mark.asyncio async def test_polygons_days(self, api): a = FakeAdaptor() @@ -74,7 +72,6 @@ class TestPolygonsEndPoint: assert [r['osm_id'] for r in results] == [781] - @pytest.mark.asyncio async def test_polygons_class(self, api): a = FakeAdaptor() @@ -85,8 +82,6 @@ class TestPolygonsEndPoint: assert [r['osm_id'] for r in results] == [781] - - @pytest.mark.asyncio async def test_polygons_reduced(self, api): a = FakeAdaptor() diff --git a/test/python/api/test_api_reverse.py b/test/python/api/test_api_reverse.py index ff7f402b..91074ecb 100644 --- a/test/python/api/test_api_reverse.py +++ b/test/python/api/test_api_reverse.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for reverse API call. @@ -18,6 +18,7 @@ import nominatim_api as napi API_OPTIONS = {'reverse'} + def test_reverse_rank_30(apiobj, frontend): apiobj.add_placex(place_id=223, class_='place', type='house', housenumber='1', @@ -35,7 +36,7 @@ def test_reverse_rank_30(apiobj, frontend): def test_reverse_street(apiobj, frontend, country): apiobj.add_placex(place_id=990, class_='highway', type='service', rank_search=27, rank_address=27, - name = {'name': 'My Street'}, + name={'name': 'My Street'}, centroid=(10.0, 10.0), country_code=country, geometry='LINESTRING(9.995 10, 10.005 10)') @@ -57,16 +58,17 @@ def test_reverse_ignore_unindexed(apiobj, frontend): assert result is None -@pytest.mark.parametrize('y,layer,place_id', [(0.7, napi.DataLayer.ADDRESS, 223), - (0.70001, napi.DataLayer.POI, 224), - (0.7, napi.DataLayer.ADDRESS | napi.DataLayer.POI, 224), - (0.70001, napi.DataLayer.ADDRESS | napi.DataLayer.POI, 223), - (0.7, napi.DataLayer.MANMADE, 225), - (0.7, napi.DataLayer.RAILWAY, 226), - (0.7, napi.DataLayer.NATURAL, 227), - (0.70003, napi.DataLayer.MANMADE | napi.DataLayer.RAILWAY, 225), - (0.70003, napi.DataLayer.MANMADE | napi.DataLayer.NATURAL, 225), - (5, napi.DataLayer.ADDRESS, 229)]) +@pytest.mark.parametrize('y,layer,place_id', + [(0.7, napi.DataLayer.ADDRESS, 223), + (0.70001, napi.DataLayer.POI, 224), + (0.7, napi.DataLayer.ADDRESS | napi.DataLayer.POI, 224), + (0.70001, napi.DataLayer.ADDRESS | napi.DataLayer.POI, 223), + (0.7, napi.DataLayer.MANMADE, 225), + (0.7, napi.DataLayer.RAILWAY, 226), + (0.7, napi.DataLayer.NATURAL, 227), + (0.70003, napi.DataLayer.MANMADE | napi.DataLayer.RAILWAY, 225), + (0.70003, napi.DataLayer.MANMADE | napi.DataLayer.NATURAL, 225), + (5, napi.DataLayer.ADDRESS, 229)]) def test_reverse_rank_30_layers(apiobj, frontend, y, layer, place_id): apiobj.add_placex(place_id=223, osm_type='N', class_='place', type='house', housenumber='1', @@ -108,14 +110,14 @@ def test_reverse_poi_layer_with_no_pois(apiobj, frontend): api = frontend(apiobj, options=API_OPTIONS) assert api.reverse((1.3, 0.70001), max_rank=29, - layers=napi.DataLayer.POI) is None + layers=napi.DataLayer.POI) is None @pytest.mark.parametrize('with_geom', [True, False]) def test_reverse_housenumber_on_street(apiobj, frontend, with_geom): apiobj.add_placex(place_id=990, class_='highway', type='service', rank_search=27, rank_address=27, - name = {'name': 'My Street'}, + name={'name': 'My Street'}, centroid=(10.0, 10.0), geometry='LINESTRING(9.995 10, 10.005 10)') apiobj.add_placex(place_id=991, class_='place', type='house', @@ -125,7 +127,7 @@ def test_reverse_housenumber_on_street(apiobj, frontend, with_geom): centroid=(10.0, 10.00001)) apiobj.add_placex(place_id=1990, class_='highway', type='service', rank_search=27, rank_address=27, - name = {'name': 'Other Street'}, + name={'name': 'Other Street'}, centroid=(10.0, 1.0), geometry='LINESTRING(9.995 1, 10.005 1)') apiobj.add_placex(place_id=1991, class_='place', type='house', @@ -147,7 +149,7 @@ def test_reverse_housenumber_on_street(apiobj, frontend, with_geom): def test_reverse_housenumber_interpolation(apiobj, frontend, with_geom): apiobj.add_placex(place_id=990, class_='highway', type='service', rank_search=27, rank_address=27, - name = {'name': 'My Street'}, + name={'name': 'My Street'}, centroid=(10.0, 10.0), geometry='LINESTRING(9.995 10, 10.005 10)') apiobj.add_placex(place_id=991, class_='place', type='house', @@ -162,7 +164,7 @@ def test_reverse_housenumber_interpolation(apiobj, frontend, with_geom): geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)') apiobj.add_placex(place_id=1990, class_='highway', type='service', rank_search=27, rank_address=27, - name = {'name': 'Other Street'}, + name={'name': 'Other Street'}, centroid=(10.0, 20.0), geometry='LINESTRING(9.995 20, 10.005 20)') apiobj.add_osmline(place_id=1992, @@ -181,7 +183,7 @@ def test_reverse_housenumber_interpolation(apiobj, frontend, with_geom): def test_reverse_housenumber_point_interpolation(apiobj, frontend): apiobj.add_placex(place_id=990, class_='highway', type='service', rank_search=27, rank_address=27, - name = {'name': 'My Street'}, + name={'name': 'My Street'}, centroid=(10.0, 10.0), geometry='LINESTRING(9.995 10, 10.005 10)') apiobj.add_osmline(place_id=992, @@ -199,7 +201,7 @@ def test_reverse_housenumber_point_interpolation(apiobj, frontend): def test_reverse_tiger_number(apiobj, frontend): apiobj.add_placex(place_id=990, class_='highway', type='service', rank_search=27, rank_address=27, - name = {'name': 'My Street'}, + name={'name': 'My Street'}, centroid=(10.0, 10.0), country_code='us', geometry='LINESTRING(9.995 10, 10.005 10)') @@ -217,7 +219,7 @@ def test_reverse_tiger_number(apiobj, frontend): def test_reverse_point_tiger(apiobj, frontend): apiobj.add_placex(place_id=990, class_='highway', type='service', rank_search=27, rank_address=27, - name = {'name': 'My Street'}, + name={'name': 'My Street'}, centroid=(10.0, 10.0), country_code='us', geometry='LINESTRING(9.995 10, 10.005 10)') @@ -393,14 +395,15 @@ def test_reverse_interpolation_geometry(apiobj, frontend): geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)') api = frontend(apiobj, options=API_OPTIONS) - assert api.reverse((10.0, 10.0), geometry_output=napi.GeometryFormat.TEXT)\ - .geometry['text'] == 'POINT(10 10.00001)' + result = api.reverse((10.0, 10.0), geometry_output=napi.GeometryFormat.TEXT) + + assert result.geometry['text'] == 'POINT(10 10.00001)' def test_reverse_tiger_geometry(apiobj, frontend): apiobj.add_placex(place_id=990, class_='highway', type='service', rank_search=27, rank_address=27, - name = {'name': 'My Street'}, + name={'name': 'My Street'}, centroid=(10.0, 10.0), country_code='us', geometry='LINESTRING(9.995 10, 10.005 10)') @@ -411,7 +414,7 @@ def test_reverse_tiger_geometry(apiobj, frontend): geometry='LINESTRING(9.995 10.00001, 10.005 10.00001)') apiobj.add_placex(place_id=1000, class_='highway', type='service', rank_search=27, rank_address=27, - name = {'name': 'My Street'}, + name={'name': 'My Street'}, centroid=(11.0, 11.0), country_code='us', geometry='LINESTRING(10.995 11, 11.005 11)') @@ -426,8 +429,9 @@ def test_reverse_tiger_geometry(apiobj, frontend): params = {'geometry_output': napi.GeometryFormat.GEOJSON} output = api.reverse((10.0, 10.0), **params) - assert json.loads(output.geometry['geojson']) == {'coordinates': [10, 10.00001], 'type': 'Point'} + assert json.loads(output.geometry['geojson']) \ + == {'coordinates': [10, 10.00001], 'type': 'Point'} output = api.reverse((11.0, 11.0), **params) - assert json.loads(output.geometry['geojson']) == {'coordinates': [11, 11.00001], 'type': 'Point'} - + assert json.loads(output.geometry['geojson']) \ + == {'coordinates': [11, 11.00001], 'type': 'Point'} diff --git a/test/python/api/test_api_search.py b/test/python/api/test_api_search.py index 54138e24..59a83aa9 100644 --- a/test/python/api/test_api_search.py +++ b/test/python/api/test_api_search.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for search API calls. @@ -10,17 +10,13 @@ Tests for search API calls. These tests make sure that all Python code is correct and executable. Functional tests can be found in the BDD test suite. """ -import json - import pytest -import sqlalchemy as sa - -import nominatim_api as napi import nominatim_api.logging as loglib API_OPTIONS = {'search'} + @pytest.fixture(autouse=True) def setup_icu_tokenizer(apiobj): """ Setup the properties needed for using the ICU tokenizer. @@ -28,8 +24,9 @@ def setup_icu_tokenizer(apiobj): apiobj.add_data('properties', [{'property': 'tokenizer', 'value': 'icu'}, {'property': 'tokenizer_import_normalisation', 'value': ':: lower();'}, - {'property': 'tokenizer_import_transliteration', 'value': "'1' > '/1/'; 'ä' > 'ä '"}, - ]) + {'property': 'tokenizer_import_transliteration', + 'value': "'1' > '/1/'; 'ä' > 'ä '"}, + ]) def test_search_no_content(apiobj, frontend): @@ -64,7 +61,7 @@ def test_search_with_debug(apiobj, frontend, logtype): api = frontend(apiobj, options=API_OPTIONS) loglib.set_log_output(logtype) - results = api.search('TEST') + api.search('TEST') assert loglib.get_and_disable() diff --git a/test/python/api/test_api_status.py b/test/python/api/test_api_status.py index 9341b527..a80c8710 100644 --- a/test/python/api/test_api_status.py +++ b/test/python/api/test_api_status.py @@ -2,18 +2,17 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for the status API call. """ import datetime as dt -import pytest -from nominatim_db.version import NominatimVersion from nominatim_api.version import NOMINATIM_API_VERSION import nominatim_api as napi + def test_status_no_extra_info(apiobj, frontend): api = frontend(apiobj) result = api.status() diff --git a/test/python/api/test_api_types.py b/test/python/api/test_api_types.py index fbb9b682..898b884d 100644 --- a/test/python/api/test_api_types.py +++ b/test/python/api/test_api_types.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for loading of parameter dataclasses. @@ -12,6 +12,7 @@ import pytest from nominatim_api.errors import UsageError import nominatim_api.types as typ + def test_no_params_defaults(): params = typ.LookupDetails.from_kwargs({}) @@ -24,7 +25,7 @@ def test_no_params_defaults(): ('geometry_simplification', 'NaN')]) def test_bad_format_reverse(k, v): with pytest.raises(UsageError): - params = typ.ReverseDetails.from_kwargs({k: v}) + typ.ReverseDetails.from_kwargs({k: v}) @pytest.mark.parametrize('rin,rout', [(-23, 0), (0, 0), (1, 1), diff --git a/test/python/api/test_export.py b/test/python/api/test_export.py index b0da52ce..7a4c6883 100644 --- a/test/python/api/test_export.py +++ b/test/python/api/test_export.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for export CLI function. @@ -11,12 +11,12 @@ import pytest import nominatim_db.cli + @pytest.fixture def run_export(tmp_path, capsys): def _exec(args): - assert 0 == nominatim_db.cli.nominatim(osm2pgsql_path='OSM2PGSQL NOT AVAILABLE', - cli_args=['export', '--project-dir', str(tmp_path)] - + args) + cli_args = ['export', '--project-dir', str(tmp_path)] + args + assert 0 == nominatim_db.cli.nominatim(cli_args=cli_args) return capsys.readouterr().out.split('\r\n') return _exec @@ -25,9 +25,9 @@ def run_export(tmp_path, capsys): @pytest.fixture(autouse=True) def setup_database_with_context(apiobj): apiobj.add_placex(place_id=332, osm_type='W', osm_id=4, - class_='highway', type='residential', name='Street', - country_code='pl', postcode='55674', - rank_search=27, rank_address=26) + class_='highway', type='residential', name='Street', + country_code='pl', postcode='55674', + rank_search=27, rank_address=26) apiobj.add_address_placex(332, fromarea=False, isaddress=False, distance=0.0034, place_id=1000, osm_type='N', osm_id=3333, diff --git a/test/python/api/test_helpers_v1.py b/test/python/api/test_helpers_v1.py index 3a6a9a0b..10f0921b 100644 --- a/test/python/api/test_helpers_v1.py +++ b/test/python/api/test_helpers_v1.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for the helper functions for v1 API. @@ -11,6 +11,7 @@ import pytest import nominatim_api.v1.helpers as helper + @pytest.mark.parametrize('inp', ['', 'abc', '12 23', @@ -35,40 +36,42 @@ def test_extract_coords_with_text_before(): def test_extract_coords_with_text_after(): assert ('abc', 12.456, -78.90) == helper.extract_coords_from_query('-78.90, 12.456 abc') + @pytest.mark.parametrize('inp', [' [12.456,-78.90] ', ' 12.456,-78.90 ']) def test_extract_coords_with_spaces(inp): assert ('', -78.90, 12.456) == helper.extract_coords_from_query(inp) + @pytest.mark.parametrize('inp', ['40 26.767 N 79 58.933 W', - '40° 26.767′ N 79° 58.933′ W', - "40° 26.767' N 79° 58.933' W", - "40° 26.767'\n" - " N 79° 58.933' W", - 'N 40 26.767, W 79 58.933', - 'N 40°26.767′, W 79°58.933′', - ' N 40°26.767′, W 79°58.933′', - "N 40°26.767', W 79°58.933'", - - '40 26 46 N 79 58 56 W', - '40° 26′ 46″ N 79° 58′ 56″ W', - '40° 26′ 46.00″ N 79° 58′ 56.00″ W', - '40°26′46″N 79°58′56″W', - 'N 40 26 46 W 79 58 56', - 'N 40° 26′ 46″, W 79° 58′ 56″', - 'N 40° 26\' 46", W 79° 58\' 56"', - 'N 40° 26\' 46", W 79° 58\' 56"', - - '40.446 -79.982', - '40.446,-79.982', - '40.446° N 79.982° W', - 'N 40.446° W 79.982°', - - '[40.446 -79.982]', - '[40.446, -79.982]', - ' 40.446 , -79.982 ', - ' 40.446 , -79.982 ', - ' 40.446 , -79.982 ', - ' 40.446 , -79.982 ']) + '40° 26.767′ N 79° 58.933′ W', + "40° 26.767' N 79° 58.933' W", + "40° 26.767'\n" + " N 79° 58.933' W", + 'N 40 26.767, W 79 58.933', + 'N 40°26.767′, W 79°58.933′', + ' N 40°26.767′, W 79°58.933′', + "N 40°26.767', W 79°58.933'", + + '40 26 46 N 79 58 56 W', + '40° 26′ 46″ N 79° 58′ 56″ W', + '40° 26′ 46.00″ N 79° 58′ 56.00″ W', + '40°26′46″N 79°58′56″W', + 'N 40 26 46 W 79 58 56', + 'N 40° 26′ 46″, W 79° 58′ 56″', + 'N 40° 26\' 46", W 79° 58\' 56"', + 'N 40° 26\' 46", W 79° 58\' 56"', + + '40.446 -79.982', + '40.446,-79.982', + '40.446° N 79.982° W', + 'N 40.446° W 79.982°', + + '[40.446 -79.982]', + '[40.446, -79.982]', + ' 40.446 , -79.982 ', + ' 40.446 , -79.982 ', + ' 40.446 , -79.982 ', + ' 40.446 , -79.982 ']) def test_extract_coords_formats(inp): query, x, y = helper.extract_coords_from_query(inp) @@ -108,9 +111,11 @@ def test_extract_category_good(inp): assert cls == 'shop' assert typ == 'fish' + def test_extract_category_only(): assert helper.extract_category_from_query('[shop=market]') == ('', 'shop', 'market') + @pytest.mark.parametrize('inp', ['house []', 'nothing', '[352]']) -def test_extract_category_no_match(inp): +def test_extract_category_no_match(inp): assert helper.extract_category_from_query(inp) == (inp, None, None) diff --git a/test/python/api/test_localization.py b/test/python/api/test_localization.py index 21fa72c8..0a30cdc1 100644 --- a/test/python/api/test_localization.py +++ b/test/python/api/test_localization.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Test functions for adapting results to the user's locale. @@ -11,34 +11,36 @@ import pytest from nominatim_api import Locales + def test_display_name_empty_names(): - l = Locales(['en', 'de']) + loc = Locales(['en', 'de']) + + assert loc.display_name(None) == '' + assert loc.display_name({}) == '' - assert l.display_name(None) == '' - assert l.display_name({}) == '' def test_display_name_none_localized(): - l = Locales() + loc = Locales() - assert l.display_name({}) == '' - assert l.display_name({'name:de': 'DE', 'name': 'ALL'}) == 'ALL' - assert l.display_name({'ref': '34', 'name:de': 'DE'}) == '34' + assert loc.display_name({}) == '' + assert loc.display_name({'name:de': 'DE', 'name': 'ALL'}) == 'ALL' + assert loc.display_name({'ref': '34', 'name:de': 'DE'}) == '34' def test_display_name_localized(): - l = Locales(['en', 'de']) + loc = Locales(['en', 'de']) - assert l.display_name({}) == '' - assert l.display_name({'name:de': 'DE', 'name': 'ALL'}) == 'DE' - assert l.display_name({'ref': '34', 'name:de': 'DE'}) == 'DE' + assert loc.display_name({}) == '' + assert loc.display_name({'name:de': 'DE', 'name': 'ALL'}) == 'DE' + assert loc.display_name({'ref': '34', 'name:de': 'DE'}) == 'DE' def test_display_name_preference(): - l = Locales(['en', 'de']) + loc = Locales(['en', 'de']) - assert l.display_name({}) == '' - assert l.display_name({'name:de': 'DE', 'name:en': 'EN'}) == 'EN' - assert l.display_name({'official_name:en': 'EN', 'name:de': 'DE'}) == 'DE' + assert loc.display_name({}) == '' + assert loc.display_name({'name:de': 'DE', 'name:en': 'EN'}) == 'EN' + assert loc.display_name({'official_name:en': 'EN', 'name:de': 'DE'}) == 'DE' @pytest.mark.parametrize('langstr,langlist', diff --git a/test/python/api/test_result_formatting_v1.py b/test/python/api/test_result_formatting_v1.py index aaecab45..406c7654 100644 --- a/test/python/api/test_result_formatting_v1.py +++ b/test/python/api/test_result_formatting_v1.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for formatting results for the V1 API. @@ -22,6 +22,7 @@ STATUS_FORMATS = {'text', 'json'} # StatusResult + def test_status_format_list(): assert set(v1_format.list_formats(napi.StatusResult)) == STATUS_FORMATS @@ -36,11 +37,13 @@ def test_status_unsupported(): def test_status_format_text(): - assert v1_format.format_result(napi.StatusResult(0, 'message here'), 'text', {}) == 'OK' + assert v1_format.format_result(napi.StatusResult(0, 'message here'), 'text', {}) \ + == 'OK' -def test_status_format_text(): - assert v1_format.format_result(napi.StatusResult(500, 'message here'), 'text', {}) == 'ERROR: message here' +def test_status_format_error_text(): + assert v1_format.format_result(napi.StatusResult(500, 'message here'), 'text', {}) \ + == 'ERROR: message here' def test_status_format_json_minimal(): @@ -48,8 +51,9 @@ def test_status_format_json_minimal(): result = v1_format.format_result(status, 'json', {}) - assert result == \ - f'{{"status":700,"message":"Bad format.","software_version":"{napi.__version__}"}}' + assert json.loads(result) == {'status': 700, + 'message': 'Bad format.', + 'software_version': napi.__version__} def test_status_format_json_full(): @@ -59,8 +63,11 @@ def test_status_format_json_full(): result = v1_format.format_result(status, 'json', {}) - assert result == \ - f'{{"status":0,"message":"OK","data_updated":"2010-02-07T20:20:03+00:00","software_version":"{napi.__version__}","database_version":"5.6"}}' + assert json.loads(result) == {'status': 0, + 'message': 'OK', + 'data_updated': '2010-02-07T20:20:03+00:00', + 'software_version': napi.__version__, + 'database_version': '5.6'} # DetailedResult @@ -86,7 +93,7 @@ def test_search_details_minimal(): 'extratags': {}, 'centroid': {'type': 'Point', 'coordinates': [1.0, 2.0]}, 'geometry': {'type': 'Point', 'coordinates': [1.0, 2.0]}, - } + } def test_search_details_full(): @@ -110,7 +117,7 @@ def test_search_details_full(): rank_search=28, importance=0.0443, country_code='ll', - indexed_date = import_date + indexed_date=import_date ) search.localize(napi.Locales()) @@ -140,7 +147,7 @@ def test_search_details_full(): 'isarea': False, 'centroid': {'type': 'Point', 'coordinates': [56.947, -87.44]}, 'geometry': {'type': 'Point', 'coordinates': [56.947, -87.44]}, - } + } @pytest.mark.parametrize('gtype,isarea', [('ST_Point', False), @@ -149,9 +156,9 @@ def test_search_details_full(): ('ST_MultiPolygon', True)]) def test_search_details_no_geometry(gtype, isarea): search = napi.DetailedResult(napi.SourceTable.PLACEX, - ('place', 'thing'), - napi.Point(1.0, 2.0), - geometry={'type': gtype}) + ('place', 'thing'), + napi.Point(1.0, 2.0), + geometry={'type': gtype}) result = v1_format.format_result(search, 'json', {}) js = json.loads(result) @@ -161,16 +168,17 @@ def test_search_details_no_geometry(gtype, isarea): def test_search_details_with_geometry(): - search = napi.DetailedResult(napi.SourceTable.PLACEX, - ('place', 'thing'), - napi.Point(1.0, 2.0), - geometry={'geojson': '{"type":"Point","coordinates":[56.947,-87.44]}'}) + search = napi.DetailedResult( + napi.SourceTable.PLACEX, + ('place', 'thing'), + napi.Point(1.0, 2.0), + geometry={'geojson': '{"type":"Point","coordinates":[56.947,-87.44]}'}) result = v1_format.format_result(search, 'json', {}) js = json.loads(result) assert js['geometry'] == {'type': 'Point', 'coordinates': [56.947, -87.44]} - assert js['isarea'] == False + assert js['isarea'] is False def test_search_details_with_icon_available(): @@ -226,7 +234,7 @@ def test_search_details_with_address_minimal(): @pytest.mark.parametrize('field,outfield', [('address_rows', 'address'), ('linked_rows', 'linked_places'), ('parented_rows', 'hierarchy') - ]) + ]) def test_search_details_with_further_infos(field, outfield): search = napi.DetailedResult(napi.SourceTable.PLACEX, ('place', 'thing'), @@ -249,50 +257,49 @@ def test_search_details_with_further_infos(field, outfield): js = json.loads(result) assert js[outfield] == [{'localname': 'Trespass', - 'place_id': 3498, - 'osm_id': 442, - 'osm_type': 'R', - 'place_type': 'spec', - 'class': 'bnd', - 'type': 'note', - 'admin_level': 4, - 'rank_address': 10, - 'distance': 0.034, - 'isaddress': True}] + 'place_id': 3498, + 'osm_id': 442, + 'osm_type': 'R', + 'place_type': 'spec', + 'class': 'bnd', + 'type': 'note', + 'admin_level': 4, + 'rank_address': 10, + 'distance': 0.034, + 'isaddress': True}] def test_search_details_grouped_hierarchy(): search = napi.DetailedResult(napi.SourceTable.PLACEX, ('place', 'thing'), napi.Point(1.0, 2.0), - parented_rows = - [napi.AddressLine(place_id=3498, - osm_object=('R', 442), - category=('bnd', 'note'), - names={'name': 'Trespass'}, - extratags={'access': 'no', - 'place_type': 'spec'}, - admin_level=4, - fromarea=True, - isaddress=True, - rank_address=10, - distance=0.034) - ]) + parented_rows=[napi.AddressLine( + place_id=3498, + osm_object=('R', 442), + category=('bnd', 'note'), + names={'name': 'Trespass'}, + extratags={'access': 'no', + 'place_type': 'spec'}, + admin_level=4, + fromarea=True, + isaddress=True, + rank_address=10, + distance=0.034)]) result = v1_format.format_result(search, 'json', {'group_hierarchy': True}) js = json.loads(result) assert js['hierarchy'] == {'note': [{'localname': 'Trespass', - 'place_id': 3498, - 'osm_id': 442, - 'osm_type': 'R', - 'place_type': 'spec', - 'class': 'bnd', - 'type': 'note', - 'admin_level': 4, - 'rank_address': 10, - 'distance': 0.034, - 'isaddress': True}]} + 'place_id': 3498, + 'osm_id': 442, + 'osm_type': 'R', + 'place_type': 'spec', + 'class': 'bnd', + 'type': 'note', + 'admin_level': 4, + 'rank_address': 10, + 'distance': 0.034, + 'isaddress': True}]} def test_search_details_keywords_name(): @@ -307,7 +314,7 @@ def test_search_details_keywords_name(): js = json.loads(result) assert js['keywords'] == {'name': [{'id': 23, 'token': 'foo'}, - {'id': 24, 'token': 'foo'}], + {'id': 24, 'token': 'foo'}], 'address': []} @@ -323,6 +330,5 @@ def test_search_details_keywords_address(): js = json.loads(result) assert js['keywords'] == {'address': [{'id': 23, 'token': 'foo'}, - {'id': 24, 'token': 'foo'}], + {'id': 24, 'token': 'foo'}], 'name': []} - diff --git a/test/python/api/test_result_formatting_v1_reverse.py b/test/python/api/test_result_formatting_v1_reverse.py index 2c036a65..902f0e79 100644 --- a/test/python/api/test_result_formatting_v1_reverse.py +++ b/test/python/api/test_result_formatting_v1_reverse.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for formatting reverse results for the V1 API. @@ -20,6 +20,7 @@ import nominatim_api as napi FORMATS = ['json', 'jsonv2', 'geojson', 'geocodejson', 'xml'] + @pytest.mark.parametrize('fmt', FORMATS) def test_format_reverse_minimal(fmt): reverse = napi.ReverseResult(napi.SourceTable.PLACEX, @@ -104,8 +105,7 @@ def test_format_reverse_with_address(fmt): reverse.localize(napi.Locales()) raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt, - {'addressdetails': True}) - + {'addressdetails': True}) if fmt == 'xml': root = ET.fromstring(raw) @@ -168,7 +168,7 @@ def test_format_reverse_geocodejson_special_parts(): reverse.localize(napi.Locales()) raw = v1_format.format_result(napi.ReverseResults([reverse]), 'geocodejson', - {'addressdetails': True}) + {'addressdetails': True}) props = json.loads(raw)['features'][0]['properties']['geocoding'] assert props['housenumber'] == '1' @@ -184,8 +184,7 @@ def test_format_reverse_with_address_none(fmt): address_rows=napi.AddressLines()) raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt, - {'addressdetails': True}) - + {'addressdetails': True}) if fmt == 'xml': root = ET.fromstring(raw) @@ -211,10 +210,10 @@ def test_format_reverse_with_extratags(fmt): reverse = napi.ReverseResult(napi.SourceTable.PLACEX, ('place', 'thing'), napi.Point(1.0, 2.0), - extratags={'one': 'A', 'two':'B'}) + extratags={'one': 'A', 'two': 'B'}) raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt, - {'extratags': True}) + {'extratags': True}) if fmt == 'xml': root = ET.fromstring(raw) @@ -226,7 +225,7 @@ def test_format_reverse_with_extratags(fmt): else: extra = result['extratags'] - assert extra == {'one': 'A', 'two':'B'} + assert extra == {'one': 'A', 'two': 'B'} @pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml']) @@ -236,7 +235,7 @@ def test_format_reverse_with_extratags_none(fmt): napi.Point(1.0, 2.0)) raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt, - {'extratags': True}) + {'extratags': True}) if fmt == 'xml': root = ET.fromstring(raw) @@ -256,10 +255,10 @@ def test_format_reverse_with_namedetails_with_name(fmt): reverse = napi.ReverseResult(napi.SourceTable.PLACEX, ('place', 'thing'), napi.Point(1.0, 2.0), - names={'name': 'A', 'ref':'1'}) + names={'name': 'A', 'ref': '1'}) raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt, - {'namedetails': True}) + {'namedetails': True}) if fmt == 'xml': root = ET.fromstring(raw) @@ -271,7 +270,7 @@ def test_format_reverse_with_namedetails_with_name(fmt): else: extra = result['namedetails'] - assert extra == {'name': 'A', 'ref':'1'} + assert extra == {'name': 'A', 'ref': '1'} @pytest.mark.parametrize('fmt', ['json', 'jsonv2', 'geojson', 'xml']) @@ -281,7 +280,7 @@ def test_format_reverse_with_namedetails_without_name(fmt): napi.Point(1.0, 2.0)) raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt, - {'namedetails': True}) + {'namedetails': True}) if fmt == 'xml': root = ET.fromstring(raw) @@ -303,7 +302,7 @@ def test_search_details_with_icon_available(fmt): napi.Point(1.0, 2.0)) result = v1_format.format_result(napi.ReverseResults([reverse]), fmt, - {'icon_base_url': 'foo'}) + {'icon_base_url': 'foo'}) js = json.loads(result) @@ -317,7 +316,6 @@ def test_search_details_with_icon_not_available(fmt): napi.Point(1.0, 2.0)) result = v1_format.format_result(napi.ReverseResults([reverse]), fmt, - {'icon_base_url': 'foo'}) + {'icon_base_url': 'foo'}) assert 'icon' not in json.loads(result) - diff --git a/test/python/api/test_results.py b/test/python/api/test_results.py index f0bfa163..8e9fbf68 100644 --- a/test/python/api/test_results.py +++ b/test/python/api/test_results.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for result datatype helper functions. @@ -11,16 +11,15 @@ import struct from binascii import hexlify import pytest -import pytest_asyncio -import sqlalchemy as sa - from nominatim_api import SourceTable, DetailedResult, Point import nominatim_api.results as nresults + def mkpoint(x, y): return hexlify(struct.pack("=biidd", 1, 0x20000001, 4326, x, y)).decode('utf-8') + class FakeRow: def __init__(self, **kwargs): if 'parent_place_id' not in kwargs: @@ -39,6 +38,7 @@ def test_minimal_detailed_result(): assert res.lat == 0.5 assert res.calculated_importance() == pytest.approx(0.00001) + def test_detailed_result_custom_importance(): res = DetailedResult(SourceTable.PLACEX, ('amenity', 'post_box'), diff --git a/test/python/api/test_server_glue_v1.py b/test/python/api/test_server_glue_v1.py index 6ea790c0..8d9f0940 100644 --- a/test/python/api/test_server_glue_v1.py +++ b/test/python/api/test_server_glue_v1.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for the Python web frameworks adaptor, v1 API. @@ -121,7 +121,6 @@ class TestAdaptorRaiseError: return excinfo.value - def test_without_content_set(self): err = self.run_raise_error('TEST', 404) @@ -129,7 +128,6 @@ class TestAdaptorRaiseError: assert err.msg == 'ERROR 404: TEST' assert err.status == 404 - def test_json(self): self.adaptor.content_type = 'application/json; charset=utf-8' @@ -139,7 +137,6 @@ class TestAdaptorRaiseError: assert content['code'] == 501 assert content['message'] == 'TEST' - def test_xml(self): self.adaptor.content_type = 'text/xml; charset=utf-8' @@ -235,7 +232,6 @@ class TestStatusEndpoint: monkeypatch.setattr(napi.NominatimAPIAsync, 'status', _status) - @pytest.mark.asyncio async def test_status_without_params(self): a = FakeAdaptor() @@ -247,7 +243,6 @@ class TestStatusEndpoint: assert resp.status == 200 assert resp.content_type == 'text/plain; charset=utf-8' - @pytest.mark.asyncio async def test_status_with_error(self): a = FakeAdaptor() @@ -259,7 +254,6 @@ class TestStatusEndpoint: assert resp.status == 500 assert resp.content_type == 'text/plain; charset=utf-8' - @pytest.mark.asyncio async def test_status_json_with_error(self): a = FakeAdaptor(params={'format': 'json'}) @@ -271,7 +265,6 @@ class TestStatusEndpoint: assert resp.status == 200 assert resp.content_type == 'application/json; charset=utf-8' - @pytest.mark.asyncio async def test_status_bad_format(self): a = FakeAdaptor(params={'format': 'foo'}) @@ -298,7 +291,6 @@ class TestDetailsEndpoint: monkeypatch.setattr(napi.NominatimAPIAsync, 'details', _lookup) - @pytest.mark.asyncio async def test_details_no_params(self): a = FakeAdaptor() @@ -306,7 +298,6 @@ class TestDetailsEndpoint: with pytest.raises(FakeError, match='^400 -- .*Missing'): await glue.details_endpoint(napi.NominatimAPIAsync(), a) - @pytest.mark.asyncio async def test_details_by_place_id(self): a = FakeAdaptor(params={'place_id': '4573'}) @@ -315,7 +306,6 @@ class TestDetailsEndpoint: assert self.lookup_args[0].place_id == 4573 - @pytest.mark.asyncio async def test_details_by_osm_id(self): a = FakeAdaptor(params={'osmtype': 'N', 'osmid': '45'}) @@ -326,7 +316,6 @@ class TestDetailsEndpoint: assert self.lookup_args[0].osm_id == 45 assert self.lookup_args[0].osm_class is None - @pytest.mark.asyncio async def test_details_with_debugging(self): a = FakeAdaptor(params={'osmtype': 'N', 'osmid': '45', 'debug': '1'}) @@ -337,7 +326,6 @@ class TestDetailsEndpoint: assert resp.content_type == 'text/html; charset=utf-8' assert content.tag == 'html' - @pytest.mark.asyncio async def test_details_no_result(self): a = FakeAdaptor(params={'place_id': '4573'}) @@ -353,14 +341,14 @@ class TestReverseEndPoint: @pytest.fixture(autouse=True) def patch_reverse_func(self, monkeypatch): self.result = napi.ReverseResult(napi.SourceTable.PLACEX, - ('place', 'thing'), - napi.Point(1.0, 2.0)) + ('place', 'thing'), + napi.Point(1.0, 2.0)) + async def _reverse(*args, **kwargs): return self.result monkeypatch.setattr(napi.NominatimAPIAsync, 'reverse', _reverse) - @pytest.mark.asyncio @pytest.mark.parametrize('params', [{}, {'lat': '3.4'}, {'lon': '6.7'}]) async def test_reverse_no_params(self, params): @@ -371,19 +359,6 @@ class TestReverseEndPoint: with pytest.raises(FakeError, match='^400 -- (?s:.*)missing'): await glue.reverse_endpoint(napi.NominatimAPIAsync(), a) - - @pytest.mark.asyncio - @pytest.mark.parametrize('params', [{'lat': '45.6', 'lon': '4563'}]) - async def test_reverse_success(self, params): - a = FakeAdaptor() - a.params = params - a.params['format'] = 'json' - - res = await glue.reverse_endpoint(napi.NominatimAPIAsync(), a) - - assert res == '' - - @pytest.mark.asyncio async def test_reverse_success(self): a = FakeAdaptor() @@ -392,7 +367,6 @@ class TestReverseEndPoint: assert await glue.reverse_endpoint(napi.NominatimAPIAsync(), a) - @pytest.mark.asyncio async def test_reverse_from_search(self): a = FakeAdaptor() @@ -413,12 +387,12 @@ class TestLookupEndpoint: self.results = [napi.SearchResult(napi.SourceTable.PLACEX, ('place', 'thing'), napi.Point(1.0, 2.0))] + async def _lookup(*args, **kwargs): return napi.SearchResults(self.results) monkeypatch.setattr(napi.NominatimAPIAsync, 'lookup', _lookup) - @pytest.mark.asyncio async def test_lookup_no_params(self): a = FakeAdaptor() @@ -428,7 +402,6 @@ class TestLookupEndpoint: assert res.output == '[]' - @pytest.mark.asyncio @pytest.mark.parametrize('param', ['w', 'bad', '']) async def test_lookup_bad_params(self, param): @@ -440,7 +413,6 @@ class TestLookupEndpoint: assert len(json.loads(res.output)) == 1 - @pytest.mark.asyncio @pytest.mark.parametrize('param', ['p234234', '4563']) async def test_lookup_bad_osm_type(self, param): @@ -452,7 +424,6 @@ class TestLookupEndpoint: assert len(json.loads(res.output)) == 1 - @pytest.mark.asyncio async def test_lookup_working(self): a = FakeAdaptor() @@ -473,12 +444,12 @@ class TestSearchEndPointSearch: self.results = [napi.SearchResult(napi.SourceTable.PLACEX, ('place', 'thing'), napi.Point(1.0, 2.0))] + async def _search(*args, **kwargs): return napi.SearchResults(self.results) monkeypatch.setattr(napi.NominatimAPIAsync, 'search', _search) - @pytest.mark.asyncio async def test_search_free_text(self): a = FakeAdaptor() @@ -488,7 +459,6 @@ class TestSearchEndPointSearch: assert len(json.loads(res.output)) == 1 - @pytest.mark.asyncio async def test_search_free_text_xml(self): a = FakeAdaptor() @@ -500,7 +470,6 @@ class TestSearchEndPointSearch: assert res.status == 200 assert res.output.index('something') > 0 - @pytest.mark.asyncio async def test_search_free_and_structured(self): a = FakeAdaptor() @@ -508,8 +477,7 @@ class TestSearchEndPointSearch: a.params['city'] = 'ignored' with pytest.raises(FakeError, match='^400 -- .*cannot be used together'): - res = await glue.search_endpoint(napi.NominatimAPIAsync(), a) - + await glue.search_endpoint(napi.NominatimAPIAsync(), a) @pytest.mark.asyncio @pytest.mark.parametrize('dedupe,numres', [(True, 1), (False, 2)]) @@ -532,12 +500,12 @@ class TestSearchEndPointSearchAddress: self.results = [napi.SearchResult(napi.SourceTable.PLACEX, ('place', 'thing'), napi.Point(1.0, 2.0))] + async def _search(*args, **kwargs): return napi.SearchResults(self.results) monkeypatch.setattr(napi.NominatimAPIAsync, 'search_address', _search) - @pytest.mark.asyncio async def test_search_structured(self): a = FakeAdaptor() @@ -555,12 +523,12 @@ class TestSearchEndPointSearchCategory: self.results = [napi.SearchResult(napi.SourceTable.PLACEX, ('place', 'thing'), napi.Point(1.0, 2.0))] + async def _search(*args, **kwargs): return napi.SearchResults(self.results) monkeypatch.setattr(napi.NominatimAPIAsync, 'search_category', _search) - @pytest.mark.asyncio async def test_search_category(self): a = FakeAdaptor() diff --git a/test/python/api/test_warm.py b/test/python/api/test_warm.py index f0c9986d..02ca0766 100644 --- a/test/python/api/test_warm.py +++ b/test/python/api/test_warm.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for warm-up CLI function. @@ -11,6 +11,7 @@ import pytest import nominatim_db.cli + @pytest.fixture(autouse=True) def setup_database_with_context(apiobj, table_factory): table_factory('word', @@ -21,12 +22,12 @@ def setup_database_with_context(apiobj, table_factory): apiobj.add_data('properties', [{'property': 'tokenizer', 'value': 'icu'}, {'property': 'tokenizer_import_normalisation', 'value': ':: lower();'}, - {'property': 'tokenizer_import_transliteration', 'value': "'1' > '/1/'; 'ä' > 'ä '"}, - ]) + {'property': 'tokenizer_import_transliteration', + 'value': "'1' > '/1/'; 'ä' > 'ä '"} + ]) @pytest.mark.parametrize('args', [['--search-only'], ['--reverse-only']]) def test_warm_all(tmp_path, args): - assert 0 == nominatim_db.cli.nominatim(osm2pgsql_path='OSM2PGSQL NOT AVAILABLE', - cli_args=['admin', '--project-dir', str(tmp_path), + assert 0 == nominatim_db.cli.nominatim(cli_args=['admin', '--project-dir', str(tmp_path), '--warm'] + args) diff --git a/test/python/cli/conftest.py b/test/python/cli/conftest.py index 84f2d659..0c61b29e 100644 --- a/test/python/cli/conftest.py +++ b/test/python/cli/conftest.py @@ -2,12 +2,13 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. import pytest import nominatim_db.cli + class MockParamCapture: """ Mock that records the parameters with which a function was called as well as the number of calls. @@ -68,8 +69,7 @@ def cli_call(): Returns a function that can be called with the desired CLI arguments. """ def _call_nominatim(*args): - return nominatim_db.cli.nominatim(osm2pgsql_path='OSM2PGSQL NOT AVAILABLE', - cli_args=args) + return nominatim_db.cli.nominatim(cli_args=args) return _call_nominatim diff --git a/test/python/cli/test_cli.py b/test/python/cli/test_cli.py index d42df50a..a538049e 100644 --- a/test/python/cli/test_cli.py +++ b/test/python/cli/test_cli.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for command line interface wrapper. @@ -11,7 +11,6 @@ These tests just check that the various command line parameters route to the correct functionality. They use a lot of monkeypatching to avoid executing the actual functions. """ -import importlib import pytest import nominatim_db.indexer.indexer @@ -28,6 +27,7 @@ def test_cli_help(cli_call, capsys): captured = capsys.readouterr() assert captured.out.startswith('usage:') + def test_cli_version(cli_call, capsys): """ Running nominatim tool --version prints a version string. """ @@ -46,7 +46,6 @@ class TestCliWithDb: # Make sure tools.freeze.is_frozen doesn't report database as frozen. Monkeypatching failed table_factory('place') - @pytest.mark.parametrize("name,oid", [('file', 'foo.osm'), ('diff', 'foo.osc')]) def test_cli_add_data_file_command(self, cli_call, mock_func_factory, name, oid): mock_run_legacy = mock_func_factory(nominatim_db.tools.add_osm_data, 'add_data_from_file') @@ -54,7 +53,6 @@ class TestCliWithDb: assert mock_run_legacy.called == 1 - @pytest.mark.parametrize("name,oid", [('node', 12), ('way', 8), ('relation', 32)]) def test_cli_add_data_object_command(self, cli_call, mock_func_factory, name, oid): mock_run_legacy = mock_func_factory(nominatim_db.tools.add_osm_data, 'add_osm_object') @@ -62,8 +60,6 @@ class TestCliWithDb: assert mock_run_legacy.called == 1 - - def test_cli_add_data_tiger_data(self, cli_call, cli_tokenizer_mock, async_mock_func_factory): mock = async_mock_func_factory(nominatim_db.tools.tiger_data, 'add_tiger_data') @@ -80,7 +76,6 @@ class TestCliWithDb: assert mock_drop.called == 1 assert mock_flatnode.called == 1 - @pytest.mark.parametrize("params,do_bnds,do_ranks", [ ([], 2, 2), (['--boundaries-only'], 2, 0), @@ -89,11 +84,14 @@ class TestCliWithDb: def test_index_command(self, monkeypatch, async_mock_func_factory, table_factory, params, do_bnds, do_ranks): table_factory('import_status', 'indexed bool') - bnd_mock = async_mock_func_factory(nominatim_db.indexer.indexer.Indexer, 'index_boundaries') - rank_mock = async_mock_func_factory(nominatim_db.indexer.indexer.Indexer, 'index_by_rank') - postcode_mock = async_mock_func_factory(nominatim_db.indexer.indexer.Indexer, 'index_postcodes') - - monkeypatch.setattr(nominatim_db.indexer.indexer.Indexer, 'has_pending', + bnd_mock = async_mock_func_factory(nominatim_db.indexer.indexer.Indexer, + 'index_boundaries') + rank_mock = async_mock_func_factory(nominatim_db.indexer.indexer.Indexer, + 'index_by_rank') + postcode_mock = async_mock_func_factory(nominatim_db.indexer.indexer.Indexer, + 'index_postcodes') + + monkeypatch.setattr(nominatim_db.indexer.indexer.Indexer, 'has_pending', [False, True].pop) assert self.call_nominatim('index', *params) == 0 @@ -102,7 +100,6 @@ class TestCliWithDb: assert rank_mock.called == do_ranks assert postcode_mock.called == do_ranks - def test_special_phrases_wiki_command(self, mock_func_factory): func = mock_func_factory(nominatim_db.clicmd.special_phrases.SPImporter, 'import_phrases') @@ -110,7 +107,6 @@ class TestCliWithDb: assert func.called == 1 - def test_special_phrases_csv_command(self, src_dir, mock_func_factory): func = mock_func_factory(nominatim_db.clicmd.special_phrases.SPImporter, 'import_phrases') testdata = src_dir / 'test' / 'testdb' @@ -120,7 +116,6 @@ class TestCliWithDb: assert func.called == 1 - def test_special_phrases_csv_bad_file(self, src_dir): testdata = src_dir / 'something349053905.csv' diff --git a/test/python/cli/test_cmd_admin.py b/test/python/cli/test_cmd_admin.py index 7b0b9cd4..9732d734 100644 --- a/test/python/cli/test_cmd_admin.py +++ b/test/python/cli/test_cmd_admin.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Test for the command line interface wrapper admin subcommand. @@ -39,11 +39,13 @@ def test_admin_clean_deleted_relations(cli_call, mock_func_factory): assert cli_call('admin', '--clean-deleted', '1 month') == 0 assert mock.called == 1 + def test_admin_clean_deleted_relations_no_age(cli_call, mock_func_factory): - mock = mock_func_factory(nominatim_db.tools.admin, 'clean_deleted_relations') + mock_func_factory(nominatim_db.tools.admin, 'clean_deleted_relations') assert cli_call('admin', '--clean-deleted') == 1 + class TestCliAdminWithDb: @pytest.fixture(autouse=True) @@ -51,7 +53,6 @@ class TestCliAdminWithDb: self.call_nominatim = cli_call self.tokenizer_mock = cli_tokenizer_mock - @pytest.mark.parametrize("func, params", [('analyse_indexing', ('--analyse-indexing', ))]) def test_analyse_indexing(self, mock_func_factory, func, params): mock = mock_func_factory(nominatim_db.tools.admin, func) diff --git a/test/python/cli/test_cmd_api.py b/test/python/cli/test_cmd_api.py index 1c0750d1..541b680c 100644 --- a/test/python/cli/test_cmd_api.py +++ b/test/python/cli/test_cmd_api.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for API access commands of command-line interface wrapper. @@ -10,9 +10,9 @@ Tests for API access commands of command-line interface wrapper. import json import pytest -import nominatim_db.clicmd.api import nominatim_api as napi + @pytest.mark.parametrize('call', ['search', 'reverse', 'lookup', 'details', 'status']) def test_list_format(cli_call, call): assert 0 == cli_call(call, '--list-formats') @@ -30,13 +30,11 @@ class TestCliStatusCall: monkeypatch.setattr(napi.NominatimAPI, 'status', lambda self: napi.StatusResult(200, 'OK')) - def test_status_simple(self, cli_call, tmp_path): result = cli_call('status', '--project-dir', str(tmp_path)) assert result == 0 - def test_status_json_format(self, cli_call, tmp_path, capsys): result = cli_call('status', '--project-dir', str(tmp_path), '--format', 'json') @@ -60,7 +58,6 @@ class TestCliDetailsCall: ('--way', '1'), ('--relation', '1'), ('--place_id', '10001')]) - def test_details_json_format(self, cli_call, tmp_path, capsys, params): result = cli_call('details', '--project-dir', str(tmp_path), *params) @@ -75,15 +72,14 @@ class TestCliReverseCall: def setup_reverse_mock(self, monkeypatch): result = napi.ReverseResult(napi.SourceTable.PLACEX, ('place', 'thing'), napi.Point(1.0, -3.0), - names={'name':'Name', 'name:fr': 'Nom'}, - extratags={'extra':'Extra'}, + names={'name': 'Name', 'name:fr': 'Nom'}, + extratags={'extra': 'Extra'}, locale_name='Name', display_name='Name') monkeypatch.setattr(napi.NominatimAPI, 'reverse', lambda *args, **kwargs: result) - def test_reverse_simple(self, cli_call, tmp_path, capsys): result = cli_call('reverse', '--project-dir', str(tmp_path), '--lat', '34', '--lon', '34') @@ -96,7 +92,6 @@ class TestCliReverseCall: assert 'extratags' not in out assert 'namedetails' not in out - @pytest.mark.parametrize('param,field', [('--addressdetails', 'address'), ('--extratags', 'extratags'), ('--namedetails', 'namedetails')]) @@ -109,7 +104,6 @@ class TestCliReverseCall: out = json.loads(capsys.readouterr().out) assert field in out - def test_reverse_format(self, cli_call, tmp_path, capsys): result = cli_call('reverse', '--project-dir', str(tmp_path), '--lat', '34', '--lon', '34', '--format', 'geojson') @@ -125,11 +119,11 @@ class TestCliLookupCall: @pytest.fixture(autouse=True) def setup_lookup_mock(self, monkeypatch): result = napi.SearchResult(napi.SourceTable.PLACEX, ('place', 'thing'), - napi.Point(1.0, -3.0), - names={'name':'Name', 'name:fr': 'Nom'}, - extratags={'extra':'Extra'}, - locale_name='Name', - display_name='Name') + napi.Point(1.0, -3.0), + names={'name': 'Name', 'name:fr': 'Nom'}, + extratags={'extra': 'Extra'}, + locale_name='Name', + display_name='Name') monkeypatch.setattr(napi.NominatimAPI, 'lookup', lambda *args, **kwargs: napi.SearchResults([result])) @@ -150,19 +144,18 @@ class TestCliLookupCall: @pytest.mark.parametrize('endpoint, params', [('search', ('--query', 'Berlin')), ('search_address', ('--city', 'Berlin')) - ]) + ]) def test_search(cli_call, tmp_path, capsys, monkeypatch, endpoint, params): result = napi.SearchResult(napi.SourceTable.PLACEX, ('place', 'thing'), napi.Point(1.0, -3.0), - names={'name':'Name', 'name:fr': 'Nom'}, - extratags={'extra':'Extra'}, + names={'name': 'Name', 'name:fr': 'Nom'}, + extratags={'extra': 'Extra'}, locale_name='Name', display_name='Name') monkeypatch.setattr(napi.NominatimAPI, endpoint, lambda *args, **kwargs: napi.SearchResults([result])) - result = cli_call('search', '--project-dir', str(tmp_path), *params) assert result == 0 diff --git a/test/python/cli/test_cmd_import.py b/test/python/cli/test_cmd_import.py index f833dde3..e4a86fe0 100644 --- a/test/python/cli/test_cmd_import.py +++ b/test/python/cli/test_cmd_import.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for import command of the command-line interface wrapper. @@ -24,15 +24,12 @@ class TestCliImportWithDb: self.call_nominatim = cli_call self.tokenizer_mock = cli_tokenizer_mock - def test_import_missing_file(self): assert self.call_nominatim('import', '--osm-file', 'sfsafegwedgw.reh.erh') == 1 - def test_import_bad_file(self): assert self.call_nominatim('import', '--osm-file', '.') == 1 - @pytest.mark.parametrize('with_updates', [True, False]) def test_import_full(self, mock_func_factory, async_mock_func_factory, with_updates, place_table, property_table): @@ -62,7 +59,6 @@ class TestCliImportWithDb: cf_mock = mock_func_factory(nominatim_db.tools.refresh, 'create_functions') - assert self.call_nominatim(*params) == 0 assert self.tokenizer_mock.finalize_import_called @@ -71,7 +67,6 @@ class TestCliImportWithDb: for mock in mocks: assert mock.called == 1, "Mock '{}' not called".format(mock.func_name) - def test_import_continue_load_data(self, mock_func_factory, async_mock_func_factory): mocks = [ mock_func_factory(nominatim_db.tools.database_import, 'truncate_data_tables'), @@ -89,7 +84,6 @@ class TestCliImportWithDb: for mock in mocks: assert mock.called == 1, "Mock '{}' not called".format(mock.func_name) - def test_import_continue_indexing(self, mock_func_factory, async_mock_func_factory, placex_table, temp_db_conn): mocks = [ @@ -107,7 +101,6 @@ class TestCliImportWithDb: # Calling it again still works for the index assert self.call_nominatim('import', '--continue', 'indexing') == 0 - def test_import_continue_postprocess(self, mock_func_factory, async_mock_func_factory): mocks = [ async_mock_func_factory(nominatim_db.tools.database_import, 'create_search_indices'), diff --git a/test/python/cli/test_cmd_refresh.py b/test/python/cli/test_cmd_refresh.py index 9f3d7bb2..8121946f 100644 --- a/test/python/cli/test_cmd_refresh.py +++ b/test/python/cli/test_cmd_refresh.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for command line interface wrapper for refresk command. @@ -13,6 +13,7 @@ import nominatim_db.tools.refresh import nominatim_db.tools.postcodes import nominatim_db.indexer.indexer + class TestRefresh: @pytest.fixture(autouse=True) @@ -20,7 +21,6 @@ class TestRefresh: self.call_nominatim = cli_call self.tokenizer_mock = cli_tokenizer_mock - @pytest.mark.parametrize("command,func", [ ('address-levels', 'load_address_levels_from_config'), ('wiki-data', 'import_wikipedia_articles'), @@ -33,17 +33,14 @@ class TestRefresh: assert self.call_nominatim('refresh', '--' + command) == 0 assert func_mock.called == 1 - def test_refresh_word_count(self): assert self.call_nominatim('refresh', '--word-count') == 0 assert self.tokenizer_mock.update_statistics_called - def test_refresh_word_tokens(self): assert self.call_nominatim('refresh', '--word-tokens') == 0 assert self.tokenizer_mock.update_word_tokens_called - def test_refresh_postcodes(self, async_mock_func_factory, mock_func_factory, place_table): func_mock = mock_func_factory(nominatim_db.tools.postcodes, 'update_postcodes') idx_mock = async_mock_func_factory(nominatim_db.indexer.indexer.Indexer, 'index_postcodes') @@ -52,12 +49,10 @@ class TestRefresh: assert func_mock.called == 1 assert idx_mock.called == 1 - def test_refresh_postcodes_no_place_table(self): # Do nothing without the place table assert self.call_nominatim('refresh', '--postcodes') == 0 - def test_refresh_create_functions(self, mock_func_factory): func_mock = mock_func_factory(nominatim_db.tools.refresh, 'create_functions') @@ -65,17 +60,14 @@ class TestRefresh: assert func_mock.called == 1 assert self.tokenizer_mock.update_sql_functions_called - def test_refresh_wikidata_file_not_found(self, monkeypatch): monkeypatch.setenv('NOMINATIM_WIKIPEDIA_DATA_PATH', 'gjoiergjeroi345Q') assert self.call_nominatim('refresh', '--wiki-data') == 1 - def test_refresh_secondary_importance_file_not_found(self): assert self.call_nominatim('refresh', '--secondary-importance') == 1 - def test_refresh_secondary_importance_new_table(self, mock_func_factory): mocks = [mock_func_factory(nominatim_db.tools.refresh, 'import_secondary_importance'), mock_func_factory(nominatim_db.tools.refresh, 'create_functions')] @@ -84,7 +76,6 @@ class TestRefresh: assert mocks[0].called == 1 assert mocks[1].called == 1 - def test_refresh_importance_computed_after_wiki_import(self, monkeypatch, mock_func_factory): calls = [] monkeypatch.setattr(nominatim_db.tools.refresh, 'import_wikipedia_articles', @@ -102,7 +93,8 @@ class TestRefresh: ('--data-object', 'N23', '--data-object', 'N24'), ('--data-area', 'R7723'), ('--data-area', 'r7723', '--data-area', 'r2'), - ('--data-area', 'R9284425', '--data-object', 'n1234567894567')]) + ('--data-area', 'R9284425', + '--data-object', 'n1234567894567')]) def test_refresh_objects(self, params, mock_func_factory): func_mock = mock_func_factory(nominatim_db.tools.refresh, 'invalidate_osm_object') @@ -110,7 +102,6 @@ class TestRefresh: assert func_mock.called == len(params)/2 - @pytest.mark.parametrize('func', ('--data-object', '--data-area')) @pytest.mark.parametrize('param', ('234', 'a55', 'R 453', 'Rel')) def test_refresh_objects_bad_param(self, func, param, mock_func_factory): diff --git a/test/python/cli/test_cmd_replication.py b/test/python/cli/test_cmd_replication.py index 21c6350d..8ec17eaa 100644 --- a/test/python/cli/test_cmd_replication.py +++ b/test/python/cli/test_cmd_replication.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2023 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for replication command of command-line interface wrapper. @@ -18,6 +18,7 @@ import nominatim_db.tools.replication import nominatim_db.tools.refresh from nominatim_db.db import status + @pytest.fixture def tokenizer_mock(monkeypatch): class DummyTokenizer: @@ -40,7 +41,6 @@ def tokenizer_mock(monkeypatch): return tok - @pytest.fixture def init_status(temp_db_conn, status_table): status.set_status(temp_db_conn, date=dt.datetime.now(dt.timezone.utc), seq=1) @@ -62,16 +62,14 @@ class TestCliReplication: def setup_cli_call(self, cli_call, temp_db): self.call_nominatim = lambda *args: cli_call('replication', *args) - @pytest.fixture(autouse=True) def setup_update_function(self, monkeypatch): def _mock_updates(states): monkeypatch.setattr(nominatim_db.tools.replication, 'update', - lambda *args, **kwargs: states.pop()) + lambda *args, **kwargs: states.pop()) self.update_states = _mock_updates - @pytest.mark.parametrize("params,func", [ (('--init',), 'init_replication'), (('--init', '--no-update-functions'), 'init_replication'), @@ -88,36 +86,32 @@ class TestCliReplication: if params == ('--init',): assert umock.called == 1 - def test_replication_update_bad_interval(self, monkeypatch): monkeypatch.setenv('NOMINATIM_REPLICATION_UPDATE_INTERVAL', 'xx') assert self.call_nominatim() == 1 - def test_replication_update_bad_interval_for_geofabrik(self, monkeypatch): monkeypatch.setenv('NOMINATIM_REPLICATION_URL', 'https://download.geofabrik.de/europe/italy-updates') assert self.call_nominatim() == 1 - def test_replication_update_continuous_no_index(self): assert self.call_nominatim('--no-index') == 1 - def test_replication_update_once_no_index(self, update_mock): + def test_replication_update_once_no_index(self, update_mock, monkeypatch): + monkeypatch.setenv('NOMINATIM_OSM2PGSQL_BINARY', 'OSM2PGSQL NOT AVAILABLE') assert self.call_nominatim('--once', '--no-index') == 0 assert str(update_mock.last_args[1]['osm2pgsql']).endswith('OSM2PGSQL NOT AVAILABLE') - def test_replication_update_custom_osm2pgsql(self, monkeypatch, update_mock): monkeypatch.setenv('NOMINATIM_OSM2PGSQL_BINARY', '/secret/osm2pgsql') assert self.call_nominatim('--once', '--no-index') == 0 assert str(update_mock.last_args[1]['osm2pgsql']) == '/secret/osm2pgsql' - @pytest.mark.parametrize("update_interval", [60, 3600]) def test_replication_catchup(self, placex_table, monkeypatch, index_mock, update_interval): monkeypatch.setenv('NOMINATIM_REPLICATION_UPDATE_INTERVAL', str(update_interval)) @@ -125,13 +119,11 @@ class TestCliReplication: assert self.call_nominatim('--catch-up') == 0 - def test_replication_update_custom_threads(self, update_mock): assert self.call_nominatim('--once', '--no-index', '--threads', '4') == 0 assert update_mock.last_args[1]['threads'] == 4 - def test_replication_update_continuous(self, index_mock): self.update_states([nominatim_db.tools.replication.UpdateState.UP_TO_DATE, nominatim_db.tools.replication.UpdateState.UP_TO_DATE]) @@ -141,7 +133,6 @@ class TestCliReplication: assert index_mock.called == 2 - def test_replication_update_continuous_no_change(self, mock_func_factory, index_mock): self.update_states([nominatim_db.tools.replication.UpdateState.NO_CHANGES, diff --git a/test/python/config/test_config.py b/test/python/config/test_config.py index 9f68fcb9..a0dbf476 100644 --- a/test/python/config/test_config.py +++ b/test/python/config/test_config.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Test for loading dotenv configuration. @@ -13,6 +13,7 @@ import pytest from nominatim_db.config import Configuration, flatten_config_list from nominatim_db.errors import UsageError + @pytest.fixture def make_config(): """ Create a configuration object from the given project directory. @@ -22,6 +23,7 @@ def make_config(): return _mk_config + @pytest.fixture def make_config_path(tmp_path): """ Create a configuration object with project and config directories @@ -108,7 +110,7 @@ def test_get_libpq_dsn_convert_php(make_config, monkeypatch): @pytest.mark.parametrize("val,expect", [('foo bar', "'foo bar'"), ("xy'z", "xy\\'z"), - ]) + ]) def test_get_libpq_dsn_convert_php_special_chars(make_config, monkeypatch, val, expect): config = make_config() @@ -137,6 +139,7 @@ def test_get_bool(make_config, monkeypatch, value, result): assert config.get_bool('FOOBAR') == result + def test_get_bool_empty(make_config): config = make_config() @@ -303,7 +306,7 @@ def test_load_subconf_env_absolute_not_found(make_config_path, monkeypatch, tmp_ (config.config_dir / 'test.yaml').write_text('cow: muh\ncat: miau\n') with pytest.raises(UsageError, match='Config file not found.'): - rules = config.load_sub_configuration('test.yaml', config='MY_CONFIG') + config.load_sub_configuration('test.yaml', config='MY_CONFIG') @pytest.mark.parametrize("location", ['project_dir', 'config_dir']) @@ -326,7 +329,7 @@ def test_load_subconf_env_relative_not_found(make_config_path, monkeypatch): (config.config_dir / 'test.yaml').write_text('cow: muh\ncat: miau\n') with pytest.raises(UsageError, match='Config file not found.'): - rules = config.load_sub_configuration('test.yaml', config='MY_CONFIG') + config.load_sub_configuration('test.yaml', config='MY_CONFIG') def test_load_subconf_json(make_config_path): @@ -338,6 +341,7 @@ def test_load_subconf_json(make_config_path): assert rules == dict(cow='muh', cat='miau') + def test_load_subconf_not_found(make_config_path): config = make_config_path() @@ -371,7 +375,7 @@ def test_load_subconf_include_relative(make_config_path, tmp_path, location): config = make_config_path() testfile = config.config_dir / 'test.yaml' - testfile.write_text(f'base: !include inc.yaml\n') + testfile.write_text('base: !include inc.yaml\n') (getattr(config, location) / 'inc.yaml').write_text('first: 1\nsecond: 2\n') rules = config.load_sub_configuration('test.yaml') @@ -383,28 +387,28 @@ def test_load_subconf_include_bad_format(make_config_path): config = make_config_path() testfile = config.config_dir / 'test.yaml' - testfile.write_text(f'base: !include inc.txt\n') + testfile.write_text('base: !include inc.txt\n') (config.config_dir / 'inc.txt').write_text('first: 1\nsecond: 2\n') with pytest.raises(UsageError, match='Cannot handle config file format.'): - rules = config.load_sub_configuration('test.yaml') + config.load_sub_configuration('test.yaml') def test_load_subconf_include_not_found(make_config_path): config = make_config_path() testfile = config.config_dir / 'test.yaml' - testfile.write_text(f'base: !include inc.txt\n') + testfile.write_text('base: !include inc.txt\n') with pytest.raises(UsageError, match='Config file not found.'): - rules = config.load_sub_configuration('test.yaml') + config.load_sub_configuration('test.yaml') def test_load_subconf_include_recursive(make_config_path): config = make_config_path() testfile = config.config_dir / 'test.yaml' - testfile.write_text(f'base: !include inc.yaml\n') + testfile.write_text('base: !include inc.yaml\n') (config.config_dir / 'inc.yaml').write_text('- !include more.yaml\n- upper\n') (config.config_dir / 'more.yaml').write_text('- the end\n') @@ -435,6 +439,6 @@ def test_flatten_config_list_nested(): [[2, 3], [45, [56, 78], 66]], 'end' ] + assert flatten_config_list(content) == \ - [34, {'first': '1st', 'second': '2nd'}, {}, - 2, 3, 45, 56, 78, 66, 'end'] + [34, {'first': '1st', 'second': '2nd'}, {}, 2, 3, 45, 56, 78, 66, 'end'] diff --git a/test/python/config/test_config_load_module.py b/test/python/config/test_config_load_module.py index c2912180..309bd1fc 100644 --- a/test/python/config/test_config_load_module.py +++ b/test/python/config/test_config_load_module.py @@ -2,18 +2,18 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Test for loading extra Python modules. """ -from pathlib import Path import sys import pytest from nominatim_db.config import Configuration + @pytest.fixture def test_config(src_dir, tmp_path): """ Create a configuration object with project and config directories @@ -31,6 +31,7 @@ def test_load_default_module(test_config): assert isinstance(module.NOMINATIM_VERSION, tuple) + def test_load_default_module_with_hyphen(test_config): module = test_config.load_plugin_module('place-info', 'nominatim_db.data') diff --git a/test/python/conftest.py b/test/python/conftest.py index a25ff8ec..b2ab99ed 100644 --- a/test/python/conftest.py +++ b/test/python/conftest.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. import itertools import sys @@ -69,6 +69,7 @@ def temp_db_with_extensions(temp_db): return temp_db + @pytest.fixture def temp_db_conn(temp_db): """ Connection to the test database. @@ -100,8 +101,9 @@ def table_factory(temp_db_conn): if content: sql = pysql.SQL("INSERT INTO {} VALUES ({})")\ .format(pysql.Identifier(name), - pysql.SQL(',').join([pysql.Placeholder() for _ in range(len(content[0]))])) - cur.executemany(sql , content) + pysql.SQL(',').join([pysql.Placeholder() + for _ in range(len(content[0]))])) + cur.executemany(sql, content) return mk_table @@ -109,7 +111,6 @@ def table_factory(temp_db_conn): @pytest.fixture def def_config(): cfg = Configuration(None) - cfg.set_libdirs(osm2pgsql=None) return cfg @@ -118,7 +119,6 @@ def project_env(tmp_path): projdir = tmp_path / 'project' projdir.mkdir() cfg = Configuration(projdir) - cfg.set_libdirs(osm2pgsql=None) return cfg @@ -178,6 +178,7 @@ def place_row(place_table, temp_db_cursor): return _insert + @pytest.fixture def placex_table(temp_db_with_extensions, temp_db_conn): """ Create an empty version of the place table. @@ -208,7 +209,7 @@ def osmline_table(temp_db_with_extensions, table_factory): def sql_preprocessor_cfg(tmp_path, table_factory, temp_db_with_extensions): table_factory('country_name', 'partition INT', ((0, ), (1, ), (2, ))) cfg = Configuration(None) - cfg.set_libdirs(osm2pgsql=None, sql=tmp_path) + cfg.set_libdirs(sql=tmp_path) return cfg diff --git a/test/python/cursor.py b/test/python/cursor.py index b3fc260a..5dc93cd5 100644 --- a/test/python/cursor.py +++ b/test/python/cursor.py @@ -2,13 +2,14 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Specialised psycopg cursor with shortcut functions useful for testing. """ import psycopg + class CursorForTesting(psycopg.Cursor): """ Extension to the DictCursor class that provides execution short-cuts that simplify writing assertions. @@ -22,7 +23,6 @@ class CursorForTesting(psycopg.Cursor): assert self.rowcount == 1 return self.fetchone()[0] - def row_set(self, sql, params=None): """ Execute a query and return the result as a set of tuples. Fails when the SQL command returns duplicate rows. @@ -34,7 +34,6 @@ class CursorForTesting(psycopg.Cursor): return result - def table_exists(self, table): """ Check that a table with the given name exists in the database. """ @@ -42,7 +41,6 @@ class CursorForTesting(psycopg.Cursor): WHERE tablename = %s""", (table, )) return num == 1 - def index_exists(self, table, index): """ Check that an indexwith the given name exists on the given table. """ @@ -51,7 +49,6 @@ class CursorForTesting(psycopg.Cursor): (table, index)) return num == 1 - def table_rows(self, table, where=None): """ Return the number of rows in the given table. """ diff --git a/test/python/data/test_country_info.py b/test/python/data/test_country_info.py index 14b306bb..a85b7bf9 100644 --- a/test/python/data/test_country_info.py +++ b/test/python/data/test_country_info.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for function that handle country properties. @@ -12,6 +12,7 @@ import pytest from nominatim_db.data import country_info + @pytest.fixture def loaded_country(def_config): country_info.setup_country_config(def_config) @@ -115,8 +116,8 @@ def test_setup_country_config_languages_not_loaded(env_with_country_config): info = country_info._CountryInfo() info.load(config) assert dict(info.items()) == {'de': {'partition': 3, - 'languages': [], - 'names': {'name': 'Deutschland'}}} + 'languages': [], + 'names': {'name': 'Deutschland'}}} def test_setup_country_config_name_not_loaded(env_with_country_config): @@ -132,8 +133,7 @@ def test_setup_country_config_name_not_loaded(env_with_country_config): assert dict(info.items()) == {'de': {'partition': 3, 'languages': ['de'], - 'names': {} - }} + 'names': {}}} def test_setup_country_config_names_not_loaded(env_with_country_config): @@ -148,8 +148,7 @@ def test_setup_country_config_names_not_loaded(env_with_country_config): assert dict(info.items()) == {'de': {'partition': 3, 'languages': ['de'], - 'names': {} - }} + 'names': {}}} def test_setup_country_config_special_character(env_with_country_config): @@ -157,8 +156,8 @@ def test_setup_country_config_special_character(env_with_country_config): bq: partition: 250 languages: nl - names: - name: + names: + name: default: "\\N" """) @@ -167,5 +166,4 @@ def test_setup_country_config_special_character(env_with_country_config): assert dict(info.items()) == {'bq': {'partition': 250, 'languages': ['nl'], - 'names': {'name': '\x85'} - }} + 'names': {'name': '\x85'}}} diff --git a/test/python/db/test_connection.py b/test/python/db/test_connection.py index a8b5d677..19b945fd 100644 --- a/test/python/db/test_connection.py +++ b/test/python/db/test_connection.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for specialised connection and cursor classes. @@ -12,6 +12,7 @@ import psycopg import nominatim_db.db.connection as nc + @pytest.fixture def db(dsn): with nc.connect(dsn) as conn: @@ -36,6 +37,7 @@ def test_has_column(db, table_factory, name, result): assert nc.table_has_column(db, 'stuff', name) == result + def test_connection_index_exists(db, table_factory, temp_db_cursor): assert not nc.index_exists(db, 'some_index') @@ -76,6 +78,7 @@ def test_drop_table_non_existing_force(db): with pytest.raises(psycopg.ProgrammingError, match='.*does not exist.*'): nc.drop_tables(db, 'dfkjgjriogjigjgjrdghehtre', if_exists=False) + def test_connection_server_version_tuple(db): ver = nc.server_version_tuple(db) diff --git a/test/python/db/test_properties.py b/test/python/db/test_properties.py index e55bb973..84d7dae0 100644 --- a/test/python/db/test_properties.py +++ b/test/python/db/test_properties.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for property table manpulation. @@ -11,6 +11,7 @@ import pytest from nominatim_db.db import properties + @pytest.fixture def property_factory(property_table, temp_db_cursor): """ A function fixture that adds a property into the property table. diff --git a/test/python/db/test_sql_preprocessor.py b/test/python/db/test_sql_preprocessor.py index 45109c70..f2fbbb2a 100644 --- a/test/python/db/test_sql_preprocessor.py +++ b/test/python/db/test_sql_preprocessor.py @@ -2,16 +2,17 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for SQL preprocessing. """ import pytest -import pytest_asyncio +import pytest_asyncio # noqa from nominatim_db.db.sql_preprocessor import SQLPreprocessor + @pytest.fixture def sql_factory(tmp_path): def _mk_sql(sql_body): @@ -26,6 +27,7 @@ def sql_factory(tmp_path): return _mk_sql + @pytest.mark.parametrize("expr,ret", [ ("'a'", 'a'), ("'{{db.partitions|join}}'", '012'), @@ -61,8 +63,7 @@ def test_load_file_with_params(sql_preprocessor, sql_factory, temp_db_conn, temp async def test_load_parallel_file(dsn, sql_preprocessor, tmp_path, temp_db_cursor): (tmp_path / 'test.sql').write_text(""" CREATE TABLE foo (a TEXT); - CREATE TABLE foo2(a TEXT);""" + - "\n---\nCREATE TABLE bar (b INT);") + CREATE TABLE foo2(a TEXT);""" + "\n---\nCREATE TABLE bar (b INT);") await sql_preprocessor.run_parallel_sql_file(dsn, 'test.sql', num_threads=4) diff --git a/test/python/db/test_status.py b/test/python/db/test_status.py index 77135a8c..462b8e3d 100644 --- a/test/python/db/test_status.py +++ b/test/python/db/test_status.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for status table manipulation. @@ -19,7 +19,8 @@ OSM_NODE_DATA = """\ -""" +""" # noqa + def iso_date(date): return dt.datetime.strptime(date, nominatim_db.db.status.ISODATE_FORMAT)\ @@ -43,7 +44,8 @@ def test_compute_database_date_from_osm2pgsql(table_factory, temp_db_conn, offli def test_compute_database_date_from_osm2pgsql_nodata(table_factory, temp_db_conn): table_factory('osm2pgsql_properties', 'property TEXT, value TEXT') - with pytest.raises(UsageError, match='Cannot determine database date from data in offline mode'): + with pytest.raises(UsageError, + match='Cannot determine database date from data in offline mode'): nominatim_db.db.status.compute_database_date(temp_db_conn, offline=True) @@ -56,6 +58,7 @@ def test_compute_database_date_valid(monkeypatch, place_row, temp_db_conn): place_row(osm_type='N', osm_id=45673) requested_url = [] + def mock_url(url): requested_url.append(url) return OSM_NODE_DATA @@ -72,6 +75,7 @@ def test_compute_database_broken_api(monkeypatch, place_row, temp_db_conn): place_row(osm_type='N', osm_id=45673) requested_url = [] + def mock_url(url): requested_url.append(url) return ' ' '"), sanitizers=[hnr], with_housenumber=True) as anl: + with analyzer(trans=(":: upper()", "'🜵' > ' '"), sanitizers=[hnr], + with_housenumber=True) as anl: self.analyzer = anl yield anl - @pytest.fixture def getorcreate_hnr_id(self, temp_db_cursor): - temp_db_cursor.execute("""CREATE OR REPLACE FUNCTION create_analyzed_hnr_id(norm_term TEXT, lookup_terms TEXT[]) - RETURNS INTEGER AS $$ - SELECT -nextval('seq_word')::INTEGER; $$ LANGUAGE SQL""") - + temp_db_cursor.execute(""" + CREATE OR REPLACE FUNCTION create_analyzed_hnr_id(norm_term TEXT, lookup_terms TEXT[]) + RETURNS INTEGER AS $$ + SELECT -nextval('seq_word')::INTEGER; $$ LANGUAGE SQL""") def process_address(self, **kwargs): return self.analyzer.process_place(PlaceInfo({'address': kwargs})) - def name_token_set(self, *expected_terms): tokens = self.analyzer.get_word_token_info(expected_terms) for token in tokens: @@ -598,7 +570,6 @@ class TestPlaceHousenumberWithAnalyser: return set((t[2] for t in tokens)) - @pytest.mark.parametrize('hnr', ['123 a', '1', '101']) def test_process_place_housenumbers_simple(self, hnr, getorcreate_hnr_id): info = self.process_address(housenumber=hnr) @@ -606,7 +577,6 @@ class TestPlaceHousenumberWithAnalyser: assert info['hnr'] == hnr.upper() assert info['hnr_tokens'] == "{-1}" - def test_process_place_housenumbers_duplicates(self, getorcreate_hnr_id): info = self.process_address(housenumber='134', conscriptionnumber='134', @@ -615,7 +585,6 @@ class TestPlaceHousenumberWithAnalyser: assert set(info['hnr'].split(';')) == set(('134', '99 A')) assert info['hnr_tokens'] == "{-1,-2}" - def test_process_place_housenumbers_cached(self, getorcreate_hnr_id): info = self.process_address(housenumber="45") assert info['hnr_tokens'] == "{-1}" @@ -637,7 +606,6 @@ class TestUpdateWordTokens: table_factory('search_name', 'place_id BIGINT, name_vector INT[]') self.tok = tokenizer_factory() - @pytest.fixture def search_entry(self, temp_db_cursor): place_id = itertools.count(1000) @@ -648,7 +616,6 @@ class TestUpdateWordTokens: return _insert - @pytest.fixture(params=['simple', 'analyzed']) def add_housenumber(self, request, word_table): if request.param == 'simple': @@ -660,7 +627,6 @@ class TestUpdateWordTokens: return _make - @pytest.mark.parametrize('hnr', ('1a', '1234567', '34 5')) def test_remove_unused_housenumbers(self, add_housenumber, word_table, hnr): word_table.add_housenumber(1000, hnr) @@ -669,7 +635,6 @@ class TestUpdateWordTokens: self.tok.update_word_tokens() assert word_table.count_housenumbers() == 0 - def test_keep_unused_numeral_housenumbers(self, add_housenumber, word_table): add_housenumber(1000, '5432') @@ -677,8 +642,8 @@ class TestUpdateWordTokens: self.tok.update_word_tokens() assert word_table.count_housenumbers() == 1 - - def test_keep_housenumbers_from_search_name_table(self, add_housenumber, word_table, search_entry): + def test_keep_housenumbers_from_search_name_table(self, add_housenumber, + word_table, search_entry): add_housenumber(9999, '5432a') add_housenumber(9991, '9 a') search_entry(123, 9999, 34) @@ -687,8 +652,8 @@ class TestUpdateWordTokens: self.tok.update_word_tokens() assert word_table.count_housenumbers() == 1 - - def test_keep_housenumbers_from_placex_table(self, add_housenumber, word_table, placex_table): + def test_keep_housenumbers_from_placex_table(self, add_housenumber, word_table, + placex_table): add_housenumber(9999, '5432a') add_housenumber(9990, '34z') placex_table.add(housenumber='34z') @@ -698,8 +663,8 @@ class TestUpdateWordTokens: self.tok.update_word_tokens() assert word_table.count_housenumbers() == 1 - - def test_keep_housenumbers_from_placex_table_hnr_list(self, add_housenumber, word_table, placex_table): + def test_keep_housenumbers_from_placex_table_hnr_list(self, add_housenumber, + word_table, placex_table): add_housenumber(9991, '9 b') add_housenumber(9990, '34z') placex_table.add(housenumber='9 a;9 b;9 c') diff --git a/test/python/tokenizer/test_icu_rule_loader.py b/test/python/tokenizer/test_icu_rule_loader.py index a3fae758..f26b84c2 100644 --- a/test/python/tokenizer/test_icu_rule_loader.py +++ b/test/python/tokenizer/test_icu_rule_loader.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for converting a config file to ICU rules. @@ -19,17 +19,16 @@ from icu import Transliterator CONFIG_SECTIONS = ('normalization', 'transliteration', 'token-analysis') + class TestIcuRuleLoader: @pytest.fixture(autouse=True) def init_env(self, project_env): self.project_env = project_env - def write_config(self, content): (self.project_env.project_dir / 'icu_tokenizer.yaml').write_text(dedent(content)) - def config_rules(self, *variants): content = dedent("""\ normalization: @@ -49,14 +48,12 @@ class TestIcuRuleLoader: content += '\n'.join((" - " + s for s in variants)) + '\n' self.write_config(content) - def get_replacements(self, *variants): self.config_rules(*variants) loader = ICURuleLoader(self.project_env) rules = loader.analysis[None].config['replacements'] - return sorted((k, sorted(v)) for k,v in rules) - + return sorted((k, sorted(v)) for k, v in rules) def test_empty_rule_set(self): self.write_config("""\ @@ -72,16 +69,14 @@ class TestIcuRuleLoader: assert rules.get_normalization_rules() == '' assert rules.get_transliteration_rules() == '' - @pytest.mark.parametrize("section", CONFIG_SECTIONS) def test_missing_section(self, section): - rule_cfg = { s: [] for s in CONFIG_SECTIONS if s != section} + rule_cfg = {s: [] for s in CONFIG_SECTIONS if s != section} self.write_config(yaml.dump(rule_cfg)) with pytest.raises(UsageError): ICURuleLoader(self.project_env) - def test_get_search_rules(self): self.config_rules() loader = ICURuleLoader(self.project_env) @@ -97,7 +92,6 @@ class TestIcuRuleLoader: assert trans.transliterate(" Αθήνα ") == " athēna " assert trans.transliterate(" проспект ") == " prospekt " - def test_get_normalization_rules(self): self.config_rules() loader = ICURuleLoader(self.project_env) @@ -106,7 +100,6 @@ class TestIcuRuleLoader: assert trans.transliterate(" проспект-Prospekt ") == " проспект prospekt " - def test_get_transliteration_rules(self): self.config_rules() loader = ICURuleLoader(self.project_env) @@ -115,7 +108,6 @@ class TestIcuRuleLoader: assert trans.transliterate(" проспект-Prospekt ") == " prospekt Prospekt " - def test_transliteration_rules_from_file(self): self.write_config("""\ normalization: @@ -135,7 +127,6 @@ class TestIcuRuleLoader: assert trans.transliterate(" axxt ") == " byt " - def test_search_rules(self): self.config_rules('~street => s,st', 'master => mstr') proc = ICURuleLoader(self.project_env).make_token_analysis() @@ -144,7 +135,6 @@ class TestIcuRuleLoader: assert proc.search.transliterate('Earnes St').strip() == 'earnes st' assert proc.search.transliterate('Nostreet').strip() == 'nostreet' - @pytest.mark.parametrize("variant", ['foo > bar', 'foo -> bar -> bar', '~foo~ -> bar', 'fo~ o -> bar']) def test_invalid_variant_description(self, variant): @@ -157,25 +147,21 @@ class TestIcuRuleLoader: assert repl == [(' foo ', [' bar', ' foo'])] - def test_replace_full(self): repl = self.get_replacements("foo => bar") assert repl == [(' foo ', [' bar'])] - def test_add_suffix_no_decompose(self): repl = self.get_replacements("~berg |-> bg") assert repl == [(' berg ', [' berg', ' bg']), ('berg ', ['berg', 'bg'])] - def test_replace_suffix_no_decompose(self): repl = self.get_replacements("~berg |=> bg") - assert repl == [(' berg ', [' bg']),('berg ', ['bg'])] - + assert repl == [(' berg ', [' bg']), ('berg ', ['bg'])] def test_add_suffix_decompose(self): repl = self.get_replacements("~berg -> bg") @@ -183,26 +169,22 @@ class TestIcuRuleLoader: assert repl == [(' berg ', [' berg', ' bg', 'berg', 'bg']), ('berg ', [' berg', ' bg', 'berg', 'bg'])] - def test_replace_suffix_decompose(self): repl = self.get_replacements("~berg => bg") assert repl == [(' berg ', [' bg', 'bg']), ('berg ', [' bg', 'bg'])] - def test_add_prefix_no_compose(self): repl = self.get_replacements("hinter~ |-> hnt") assert repl == [(' hinter', [' hinter', ' hnt']), (' hinter ', [' hinter', ' hnt'])] - def test_replace_prefix_no_compose(self): repl = self.get_replacements("hinter~ |=> hnt") - assert repl == [(' hinter', [' hnt']), (' hinter ', [' hnt'])] - + assert repl == [(' hinter', [' hnt']), (' hinter ', [' hnt'])] def test_add_prefix_compose(self): repl = self.get_replacements("hinter~-> h") @@ -210,45 +192,38 @@ class TestIcuRuleLoader: assert repl == [(' hinter', [' h', ' h ', ' hinter', ' hinter ']), (' hinter ', [' h', ' h', ' hinter', ' hinter'])] - def test_replace_prefix_compose(self): repl = self.get_replacements("hinter~=> h") assert repl == [(' hinter', [' h', ' h ']), (' hinter ', [' h', ' h'])] - def test_add_beginning_only(self): repl = self.get_replacements("^Premier -> Pr") assert repl == [('^ premier ', ['^ pr', '^ premier'])] - def test_replace_beginning_only(self): repl = self.get_replacements("^Premier => Pr") assert repl == [('^ premier ', ['^ pr'])] - def test_add_final_only(self): repl = self.get_replacements("road$ -> rd") assert repl == [(' road ^', [' rd ^', ' road ^'])] - def test_replace_final_only(self): repl = self.get_replacements("road$ => rd") assert repl == [(' road ^', [' rd ^'])] - def test_decompose_only(self): repl = self.get_replacements("~foo -> foo") assert repl == [(' foo ', [' foo', 'foo']), ('foo ', [' foo', 'foo'])] - def test_add_suffix_decompose_end_only(self): repl = self.get_replacements("~berg |-> bg", "~berg$ -> bg") @@ -257,7 +232,6 @@ class TestIcuRuleLoader: ('berg ', ['berg', 'bg']), ('berg ^', [' berg ^', ' bg ^', 'berg ^', 'bg ^'])] - def test_replace_suffix_decompose_end_only(self): repl = self.get_replacements("~berg |=> bg", "~berg$ => bg") @@ -266,7 +240,6 @@ class TestIcuRuleLoader: ('berg ', ['bg']), ('berg ^', [' bg ^', 'bg ^'])] - def test_add_multiple_suffix(self): repl = self.get_replacements("~berg,~burg -> bg") diff --git a/test/python/tokenizer/test_place_sanitizer.py b/test/python/tokenizer/test_place_sanitizer.py index 25844459..fcf02bd3 100644 --- a/test/python/tokenizer/test_place_sanitizer.py +++ b/test/python/tokenizer/test_place_sanitizer.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for execution of the sanitztion step. @@ -50,13 +50,13 @@ def test_placeinfo_has_attr(): def test_sanitizer_default(def_config): san = sanitizer.PlaceSanitizer([{'step': 'split-name-list'}], def_config) - name, address = san.process_names(PlaceInfo({'name': {'name:de:de': '1;2;3'}, - 'address': {'street': 'Bald'}})) + name, address = san.process_names(PlaceInfo({'name': {'name:de:de': '1;2;3'}, + 'address': {'street': 'Bald'}})) assert len(name) == 3 assert all(isinstance(n, sanitizer.PlaceName) for n in name) - assert all(n.kind == 'name' for n in name) - assert all(n.suffix == 'de:de' for n in name) + assert all(n.kind == 'name' for n in name) + assert all(n.suffix == 'de:de' for n in name) assert len(address) == 1 assert all(isinstance(n, sanitizer.PlaceName) for n in address) @@ -66,7 +66,7 @@ def test_sanitizer_default(def_config): def test_sanitizer_empty_list(def_config, rules): san = sanitizer.PlaceSanitizer(rules, def_config) - name, address = san.process_names(PlaceInfo({'name': {'name:de:de': '1;2;3'}})) + name, address = san.process_names(PlaceInfo({'name': {'name:de:de': '1;2;3'}})) assert len(name) == 1 assert all(isinstance(n, sanitizer.PlaceName) for n in name) @@ -74,4 +74,4 @@ def test_sanitizer_empty_list(def_config, rules): def test_sanitizer_missing_step_definition(def_config): with pytest.raises(UsageError): - san = sanitizer.PlaceSanitizer([{'id': 'split-name-list'}], def_config) + sanitizer.PlaceSanitizer([{'id': 'split-name-list'}], def_config) diff --git a/test/python/tokenizer/token_analysis/test_analysis_postcodes.py b/test/python/tokenizer/token_analysis/test_analysis_postcodes.py index 870c8a5d..1eb15a50 100644 --- a/test/python/tokenizer/token_analysis/test_analysis_postcodes.py +++ b/test/python/tokenizer/token_analysis/test_analysis_postcodes.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for special postcode analysis and variant generation. @@ -13,7 +13,6 @@ from icu import Transliterator import nominatim_db.tokenizer.token_analysis.postcodes as module from nominatim_db.data.place_name import PlaceName -from nominatim_db.errors import UsageError DEFAULT_NORMALIZATION = """ :: NFD (); '🜳' > ' '; @@ -27,9 +26,10 @@ DEFAULT_TRANSLITERATION = """ :: Latin (); '🜵' > ' '; """ + @pytest.fixture def analyser(): - rules = { 'analyzer': 'postcodes'} + rules = {'analyzer': 'postcodes'} config = module.configure(rules, DEFAULT_NORMALIZATION) trans = Transliterator.createFromRules("test_trans", DEFAULT_TRANSLITERATION) diff --git a/test/python/tokenizer/token_analysis/test_generic.py b/test/python/tokenizer/token_analysis/test_generic.py index 191f551f..02870f24 100644 --- a/test/python/tokenizer/token_analysis/test_generic.py +++ b/test/python/tokenizer/token_analysis/test_generic.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for import name normalisation and variant generation. @@ -26,8 +26,9 @@ DEFAULT_TRANSLITERATION = """ :: Latin (); '🜵' > ' '; """ + def make_analyser(*variants, variant_only=False): - rules = { 'analyzer': 'generic', 'variants': [{'words': variants}]} + rules = {'analyzer': 'generic', 'variants': [{'words': variants}]} if variant_only: rules['mode'] = 'variant-only' trans = Transliterator.createFromRules("test_trans", DEFAULT_TRANSLITERATION) @@ -43,7 +44,7 @@ def get_normalized_variants(proc, name): def test_no_variants(): - rules = { 'analyzer': 'generic' } + rules = {'analyzer': 'generic'} trans = Transliterator.createFromRules("test_trans", DEFAULT_TRANSLITERATION) norm = Transliterator.createFromRules("test_norm", DEFAULT_NORMALIZATION) config = module.configure(rules, norm, trans) @@ -62,35 +63,36 @@ def test_variants_empty(): VARIANT_TESTS = [ -(('~strasse,~straße -> str', '~weg => weg'), "hallo", {'hallo'}), -(('weg => wg',), "holzweg", {'holzweg'}), -(('weg -> wg',), "holzweg", {'holzweg'}), -(('~weg => weg',), "holzweg", {'holz weg', 'holzweg'}), -(('~weg -> weg',), "holzweg", {'holz weg', 'holzweg'}), -(('~weg => w',), "holzweg", {'holz w', 'holzw'}), -(('~weg -> w',), "holzweg", {'holz weg', 'holzweg', 'holz w', 'holzw'}), -(('~weg => weg',), "Meier Weg", {'meier weg', 'meierweg'}), -(('~weg -> weg',), "Meier Weg", {'meier weg', 'meierweg'}), -(('~weg => w',), "Meier Weg", {'meier w', 'meierw'}), -(('~weg -> w',), "Meier Weg", {'meier weg', 'meierweg', 'meier w', 'meierw'}), -(('weg => wg',), "Meier Weg", {'meier wg'}), -(('weg -> wg',), "Meier Weg", {'meier weg', 'meier wg'}), -(('~strasse,~straße -> str', '~weg => weg'), "Bauwegstraße", + (('~strasse,~straße -> str', '~weg => weg'), "hallo", {'hallo'}), + (('weg => wg',), "holzweg", {'holzweg'}), + (('weg -> wg',), "holzweg", {'holzweg'}), + (('~weg => weg',), "holzweg", {'holz weg', 'holzweg'}), + (('~weg -> weg',), "holzweg", {'holz weg', 'holzweg'}), + (('~weg => w',), "holzweg", {'holz w', 'holzw'}), + (('~weg -> w',), "holzweg", {'holz weg', 'holzweg', 'holz w', 'holzw'}), + (('~weg => weg',), "Meier Weg", {'meier weg', 'meierweg'}), + (('~weg -> weg',), "Meier Weg", {'meier weg', 'meierweg'}), + (('~weg => w',), "Meier Weg", {'meier w', 'meierw'}), + (('~weg -> w',), "Meier Weg", {'meier weg', 'meierweg', 'meier w', 'meierw'}), + (('weg => wg',), "Meier Weg", {'meier wg'}), + (('weg -> wg',), "Meier Weg", {'meier weg', 'meier wg'}), + (('~strasse,~straße -> str', '~weg => weg'), "Bauwegstraße", {'bauweg straße', 'bauweg str', 'bauwegstraße', 'bauwegstr'}), -(('am => a', 'bach => b'), "am bach", {'a b'}), -(('am => a', '~bach => b'), "am bach", {'a b'}), -(('am -> a', '~bach -> b'), "am bach", {'am bach', 'a bach', 'am b', 'a b'}), -(('am -> a', '~bach -> b'), "ambach", {'ambach', 'am bach', 'amb', 'am b'}), -(('saint -> s,st', 'street -> st'), "Saint Johns Street", + (('am => a', 'bach => b'), "am bach", {'a b'}), + (('am => a', '~bach => b'), "am bach", {'a b'}), + (('am -> a', '~bach -> b'), "am bach", {'am bach', 'a bach', 'am b', 'a b'}), + (('am -> a', '~bach -> b'), "ambach", {'ambach', 'am bach', 'amb', 'am b'}), + (('saint -> s,st', 'street -> st'), "Saint Johns Street", {'saint johns street', 's johns street', 'st johns street', 'saint johns st', 's johns st', 'st johns st'}), -(('river$ -> r',), "River Bend Road", {'river bend road'}), -(('river$ -> r',), "Bent River", {'bent river', 'bent r'}), -(('^north => n',), "North 2nd Street", {'n 2nd street'}), -(('^north => n',), "Airport North", {'airport north'}), -(('am -> a',), "am am am am am am am am", {'am am am am am am am am'}), -(('am => a',), "am am am am am am am am", {'a a a a a a a a'}) -] + (('river$ -> r',), "River Bend Road", {'river bend road'}), + (('river$ -> r',), "Bent River", {'bent river', 'bent r'}), + (('^north => n',), "North 2nd Street", {'n 2nd street'}), + (('^north => n',), "Airport North", {'airport north'}), + (('am -> a',), "am am am am am am am am", {'am am am am am am am am'}), + (('am => a',), "am am am am am am am am", {'a a a a a a a a'}) + ] + @pytest.mark.parametrize("rules,name,variants", VARIANT_TESTS) def test_variants(rules, name, variants): @@ -103,10 +105,11 @@ def test_variants(rules, name, variants): VARIANT_ONLY_TESTS = [ -(('weg => wg',), "hallo", set()), -(('weg => wg',), "Meier Weg", {'meier wg'}), -(('weg -> wg',), "Meier Weg", {'meier wg'}), -] + (('weg => wg',), "hallo", set()), + (('weg => wg',), "Meier Weg", {'meier wg'}), + (('weg -> wg',), "Meier Weg", {'meier wg'}), + ] + @pytest.mark.parametrize("rules,name,variants", VARIANT_ONLY_TESTS) def test_variants_only(rules, name, variants): @@ -122,17 +125,15 @@ class TestGetReplacements: @staticmethod def configure_rules(*variants): - rules = { 'analyzer': 'generic', 'variants': [{'words': variants}]} + rules = {'analyzer': 'generic', 'variants': [{'words': variants}]} trans = Transliterator.createFromRules("test_trans", DEFAULT_TRANSLITERATION) norm = Transliterator.createFromRules("test_norm", DEFAULT_NORMALIZATION) return module.configure(rules, norm, trans) - def get_replacements(self, *variants): config = self.configure_rules(*variants) - return sorted((k, sorted(v)) for k,v in config['replacements']) - + return sorted((k, sorted(v)) for k, v in config['replacements']) @pytest.mark.parametrize("variant", ['foo > bar', 'foo -> bar -> bar', '~foo~ -> bar', 'fo~ o -> bar']) @@ -140,38 +141,32 @@ class TestGetReplacements: with pytest.raises(UsageError): self.configure_rules(variant) - @pytest.mark.parametrize("rule", ["!!! -> bar", "bar => !!!"]) def test_ignore_unnormalizable_terms(self, rule): repl = self.get_replacements(rule) assert repl == [] - def test_add_full(self): repl = self.get_replacements("foo -> bar") assert repl == [(' foo ', [' bar', ' foo'])] - def test_replace_full(self): repl = self.get_replacements("foo => bar") assert repl == [(' foo ', [' bar'])] - def test_add_suffix_no_decompose(self): repl = self.get_replacements("~berg |-> bg") assert repl == [(' berg ', [' berg', ' bg']), ('berg ', ['berg', 'bg'])] - def test_replace_suffix_no_decompose(self): repl = self.get_replacements("~berg |=> bg") - assert repl == [(' berg ', [' bg']),('berg ', ['bg'])] - + assert repl == [(' berg ', [' bg']), ('berg ', ['bg'])] def test_add_suffix_decompose(self): repl = self.get_replacements("~berg -> bg") @@ -179,26 +174,22 @@ class TestGetReplacements: assert repl == [(' berg ', [' berg', ' bg', 'berg', 'bg']), ('berg ', [' berg', ' bg', 'berg', 'bg'])] - def test_replace_suffix_decompose(self): repl = self.get_replacements("~berg => bg") assert repl == [(' berg ', [' bg', 'bg']), ('berg ', [' bg', 'bg'])] - def test_add_prefix_no_compose(self): repl = self.get_replacements("hinter~ |-> hnt") assert repl == [(' hinter', [' hinter', ' hnt']), (' hinter ', [' hinter', ' hnt'])] - def test_replace_prefix_no_compose(self): repl = self.get_replacements("hinter~ |=> hnt") - assert repl == [(' hinter', [' hnt']), (' hinter ', [' hnt'])] - + assert repl == [(' hinter', [' hnt']), (' hinter ', [' hnt'])] def test_add_prefix_compose(self): repl = self.get_replacements("hinter~-> h") @@ -206,45 +197,38 @@ class TestGetReplacements: assert repl == [(' hinter', [' h', ' h ', ' hinter', ' hinter ']), (' hinter ', [' h', ' h', ' hinter', ' hinter'])] - def test_replace_prefix_compose(self): repl = self.get_replacements("hinter~=> h") assert repl == [(' hinter', [' h', ' h ']), (' hinter ', [' h', ' h'])] - def test_add_beginning_only(self): repl = self.get_replacements("^Premier -> Pr") assert repl == [('^ premier ', ['^ pr', '^ premier'])] - def test_replace_beginning_only(self): repl = self.get_replacements("^Premier => Pr") assert repl == [('^ premier ', ['^ pr'])] - def test_add_final_only(self): repl = self.get_replacements("road$ -> rd") assert repl == [(' road ^', [' rd ^', ' road ^'])] - def test_replace_final_only(self): repl = self.get_replacements("road$ => rd") assert repl == [(' road ^', [' rd ^'])] - def test_decompose_only(self): repl = self.get_replacements("~foo -> foo") assert repl == [(' foo ', [' foo', 'foo']), ('foo ', [' foo', 'foo'])] - def test_add_suffix_decompose_end_only(self): repl = self.get_replacements("~berg |-> bg", "~berg$ -> bg") @@ -253,7 +237,6 @@ class TestGetReplacements: ('berg ', ['berg', 'bg']), ('berg ^', [' berg ^', ' bg ^', 'berg ^', 'bg ^'])] - def test_replace_suffix_decompose_end_only(self): repl = self.get_replacements("~berg |=> bg", "~berg$ => bg") @@ -262,7 +245,6 @@ class TestGetReplacements: ('berg ', ['bg']), ('berg ^', [' bg ^', 'bg ^'])] - @pytest.mark.parametrize('rule', ["~berg,~burg -> bg", "~berg, ~burg -> bg", "~berg,,~burg -> bg"]) diff --git a/test/python/tokenizer/token_analysis/test_generic_mutation.py b/test/python/tokenizer/token_analysis/test_generic_mutation.py index 7d0db925..2ce2236a 100644 --- a/test/python/tokenizer/token_analysis/test_generic_mutation.py +++ b/test/python/tokenizer/token_analysis/test_generic_mutation.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for generic token analysis, mutation part. @@ -24,37 +24,34 @@ DEFAULT_TRANSLITERATION = """ :: Latin (); '🜵' > ' '; """ + class TestMutationNoVariants: def make_analyser(self, *mutations): - rules = { 'analyzer': 'generic', - 'mutations': [ {'pattern': m[0], 'replacements': m[1]} - for m in mutations] - } + rules = {'analyzer': 'generic', + 'mutations': [{'pattern': m[0], 'replacements': m[1]} + for m in mutations] + } trans = Transliterator.createFromRules("test_trans", DEFAULT_TRANSLITERATION) norm = Transliterator.createFromRules("test_norm", DEFAULT_NORMALIZATION) config = module.configure(rules, norm, trans) self.analysis = module.create(norm, trans, config) - def variants(self, name): norm = Transliterator.createFromRules("test_norm", DEFAULT_NORMALIZATION) return set(self.analysis.compute_variants(norm.transliterate(name).strip())) - @pytest.mark.parametrize('pattern', ('(capture)', ['a list'])) def test_bad_pattern(self, pattern): with pytest.raises(UsageError): self.make_analyser((pattern, ['b'])) - @pytest.mark.parametrize('replacements', (None, 'a string')) def test_bad_replacement(self, replacements): with pytest.raises(UsageError): self.make_analyser(('a', replacements)) - def test_simple_replacement(self): self.make_analyser(('a', ['b'])) @@ -62,27 +59,23 @@ class TestMutationNoVariants: assert self.variants('abba') == {'bbbb'} assert self.variants('2 aar') == {'2 bbr'} - def test_multichar_replacement(self): self.make_analyser(('1 1', ['1 1 1'])) assert self.variants('1 1456') == {'1 1 1456'} assert self.variants('1 1 1') == {'1 1 1 1'} - def test_removement_replacement(self): self.make_analyser((' ', [' ', ''])) assert self.variants('A 345') == {'a 345', 'a345'} assert self.variants('a g b') == {'a g b', 'ag b', 'a gb', 'agb'} - def test_regex_pattern(self): self.make_analyser(('[^a-z]+', ['XXX', ' '])) assert self.variants('a-34n12') == {'aXXXnXXX', 'aXXXn', 'a nXXX', 'a n'} - def test_multiple_mutations(self): self.make_analyser(('ä', ['ä', 'ae']), ('ö', ['ö', 'oe'])) diff --git a/test/python/tokenizer/token_analysis/test_simple_trie.py b/test/python/tokenizer/token_analysis/test_simple_trie.py index 0384a456..6ce66580 100644 --- a/test/python/tokenizer/token_analysis/test_simple_trie.py +++ b/test/python/tokenizer/token_analysis/test_simple_trie.py @@ -10,6 +10,7 @@ Tests for simplified trie structure. from nominatim_db.tokenizer.token_analysis.simple_trie import SimpleTrie + def test_single_item_trie(): t = SimpleTrie([('foob', 42)]) @@ -18,6 +19,7 @@ def test_single_item_trie(): assert t.longest_prefix('foob') == (42, 4) assert t.longest_prefix('123foofoo', 3) == (None, 3) + def test_complex_item_tree(): t = SimpleTrie([('a', 1), ('b', 2), diff --git a/test/python/tools/conftest.py b/test/python/tools/conftest.py index 0098747e..dc9346c8 100644 --- a/test/python/tools/conftest.py +++ b/test/python/tools/conftest.py @@ -2,10 +2,11 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. import pytest + @pytest.fixture def osm2pgsql_options(temp_db, tmp_path): """ A standard set of options for osm2pgsql diff --git a/test/python/tools/test_add_osm_data.py b/test/python/tools/test_add_osm_data.py index c5aaaaae..38cf87c4 100644 --- a/test/python/tools/test_add_osm_data.py +++ b/test/python/tools/test_add_osm_data.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for functions to add additional data to the database. @@ -13,6 +13,7 @@ import pytest from nominatim_db.tools import add_osm_data + class CaptureGetUrl: def __init__(self, monkeypatch): @@ -29,6 +30,7 @@ def setup_delete_postprocessing(temp_db_cursor): temp_db_cursor.execute("""CREATE OR REPLACE FUNCTION flush_deleted_places() RETURNS INTEGER AS $$ SELECT 1 $$ LANGUAGE SQL""") + def test_import_osm_file_simple(dsn, table_factory, osm2pgsql_options, capfd): assert add_osm_data.add_data_from_file(dsn, Path('change.osm'), osm2pgsql_options) == 0 diff --git a/test/python/tools/test_admin.py b/test/python/tools/test_admin.py index 1e1f0e29..e758bca2 100644 --- a/test/python/tools/test_admin.py +++ b/test/python/tools/test_admin.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for maintenance and analysis functions. @@ -14,6 +14,7 @@ from nominatim_db.tools import admin from nominatim_db.tokenizer import factory from nominatim_db.db.sql_preprocessor import SQLPreprocessor + @pytest.fixture(autouse=True) def create_placex_table(project_env, tokenizer_mock, temp_db_cursor, placex_table): """ All tests in this module require the placex table to be set up. @@ -76,7 +77,8 @@ def test_analyse_indexing_with_osm_id(project_env, temp_db_cursor): class TestAdminCleanDeleted: @pytest.fixture(autouse=True) - def setup_polygon_delete(self, project_env, table_factory, place_table, osmline_table, temp_db_cursor, temp_db_conn, def_config, src_dir): + def setup_polygon_delete(self, project_env, table_factory, place_table, + osmline_table, temp_db_cursor, temp_db_conn, def_config, src_dir): """ Set up place_force_delete function and related tables """ self.project_env = project_env @@ -87,12 +89,14 @@ class TestAdminCleanDeleted: class TEXT NOT NULL, type TEXT NOT NULL""", ((100, 'N', 'boundary', 'administrative'), - (145, 'N', 'boundary', 'administrative'), - (175, 'R', 'landcover', 'grass'))) - temp_db_cursor.execute("""INSERT INTO placex (place_id, osm_id, osm_type, class, type, indexed_date, indexed_status) - VALUES(1, 100, 'N', 'boundary', 'administrative', current_date - INTERVAL '1 month', 1), - (2, 145, 'N', 'boundary', 'administrative', current_date - INTERVAL '3 month', 1), - (3, 175, 'R', 'landcover', 'grass', current_date - INTERVAL '3 months', 1)""") + (145, 'N', 'boundary', 'administrative'), + (175, 'R', 'landcover', 'grass'))) + temp_db_cursor.execute(""" + INSERT INTO placex (place_id, osm_id, osm_type, class, type, + indexed_date, indexed_status) + VALUES(1, 100, 'N', 'boundary', 'administrative', current_date - INTERVAL '1 month', 1), + (2, 145, 'N', 'boundary', 'administrative', current_date - INTERVAL '3 month', 1), + (3, 175, 'R', 'landcover', 'grass', current_date - INTERVAL '3 months', 1)""") # set up tables and triggers for utils function table_factory('place_to_be_deleted', """osm_id BIGINT, @@ -116,33 +120,42 @@ class TestAdminCleanDeleted: sqlproc = SQLPreprocessor(temp_db_conn, def_config) sqlproc.run_sql_file(temp_db_conn, 'functions/utils.sql') def_config.lib_dir.sql = orig_sql - def test_admin_clean_deleted_no_records(self): admin.clean_deleted_relations(self.project_env, age='1 year') - assert self.temp_db_cursor.row_set('SELECT osm_id, osm_type, class, type, indexed_status FROM placex') == {(100, 'N', 'boundary', 'administrative', 1), - (145, 'N', 'boundary', 'administrative', 1), - (175, 'R', 'landcover', 'grass', 1)} - assert self.temp_db_cursor.table_rows('import_polygon_delete') == 3 + rowset = self.temp_db_cursor.row_set( + 'SELECT osm_id, osm_type, class, type, indexed_status FROM placex') + + assert rowset == {(100, 'N', 'boundary', 'administrative', 1), + (145, 'N', 'boundary', 'administrative', 1), + (175, 'R', 'landcover', 'grass', 1)} + assert self.temp_db_cursor.table_rows('import_polygon_delete') == 3 @pytest.mark.parametrize('test_age', ['T week', '1 welk', 'P1E']) def test_admin_clean_deleted_bad_age(self, test_age): with pytest.raises(UsageError): - admin.clean_deleted_relations(self.project_env, age = test_age) - + admin.clean_deleted_relations(self.project_env, age=test_age) def test_admin_clean_deleted_partial(self): - admin.clean_deleted_relations(self.project_env, age = '2 months') - assert self.temp_db_cursor.row_set('SELECT osm_id, osm_type, class, type, indexed_status FROM placex') == {(100, 'N', 'boundary', 'administrative', 1), - (145, 'N', 'boundary', 'administrative', 100), - (175, 'R', 'landcover', 'grass', 100)} + admin.clean_deleted_relations(self.project_env, age='2 months') + + rowset = self.temp_db_cursor.row_set( + 'SELECT osm_id, osm_type, class, type, indexed_status FROM placex') + + assert rowset == {(100, 'N', 'boundary', 'administrative', 1), + (145, 'N', 'boundary', 'administrative', 100), + (175, 'R', 'landcover', 'grass', 100)} assert self.temp_db_cursor.table_rows('import_polygon_delete') == 1 @pytest.mark.parametrize('test_age', ['1 week', 'P3D', '5 hours']) def test_admin_clean_deleted(self, test_age): - admin.clean_deleted_relations(self.project_env, age = test_age) - assert self.temp_db_cursor.row_set('SELECT osm_id, osm_type, class, type, indexed_status FROM placex') == {(100, 'N', 'boundary', 'administrative', 100), - (145, 'N', 'boundary', 'administrative', 100), - (175, 'R', 'landcover', 'grass', 100)} + admin.clean_deleted_relations(self.project_env, age=test_age) + + rowset = self.temp_db_cursor.row_set( + 'SELECT osm_id, osm_type, class, type, indexed_status FROM placex') + + assert rowset == {(100, 'N', 'boundary', 'administrative', 100), + (145, 'N', 'boundary', 'administrative', 100), + (175, 'R', 'landcover', 'grass', 100)} assert self.temp_db_cursor.table_rows('import_polygon_delete') == 0 diff --git a/test/python/tools/test_check_database.py b/test/python/tools/test_check_database.py index 886bd75b..66506f56 100644 --- a/test/python/tools/test_check_database.py +++ b/test/python/tools/test_check_database.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for database integrity checks. @@ -12,6 +12,7 @@ import pytest from nominatim_db.tools import check_database as chkdb import nominatim_db.version + def test_check_database_unknown_db(def_config, monkeypatch): monkeypatch.setenv('NOMINATIM_DATABASE_DSN', 'pgsql:dbname=fjgkhughwgh2423gsags') assert chkdb.check_database(def_config) == 1 @@ -35,6 +36,7 @@ def test_check_database_version_good(property_table, temp_db_conn, def_config): str(nominatim_db.version.NOMINATIM_VERSION)) assert chkdb.check_database_version(temp_db_conn, def_config) == chkdb.CheckState.OK + def test_check_database_version_bad(property_table, temp_db_conn, def_config): property_table.set('database_version', '3.9.9-9') assert chkdb.check_database_version(temp_db_conn, def_config) == chkdb.CheckState.FATAL diff --git a/test/python/tools/test_database_import.py b/test/python/tools/test_database_import.py index df204298..f8cea2cc 100644 --- a/test/python/tools/test_database_import.py +++ b/test/python/tools/test_database_import.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for functions to import a new database. @@ -10,13 +10,14 @@ Tests for functions to import a new database. from pathlib import Path import pytest -import pytest_asyncio +import pytest_asyncio # noqa import psycopg from psycopg import sql as pysql from nominatim_db.tools import database_import from nominatim_db.errors import UsageError + class TestDatabaseSetup: DBNAME = 'test_nominatim_python_unittest' @@ -31,18 +32,15 @@ class TestDatabaseSetup: with conn.cursor() as cur: cur.execute(f'DROP DATABASE IF EXISTS {self.DBNAME}') - @pytest.fixture def cursor(self): with psycopg.connect(dbname=self.DBNAME) as conn: with conn.cursor() as cur: yield cur - def conn(self): return psycopg.connect(dbname=self.DBNAME) - def test_setup_skeleton(self): database_import.setup_database_skeleton(f'dbname={self.DBNAME}') @@ -51,25 +49,21 @@ class TestDatabaseSetup: with conn.cursor() as cur: cur.execute('CREATE TABLE t (h HSTORE, geom GEOMETRY(Geometry, 4326))') - def test_unsupported_pg_version(self, monkeypatch): monkeypatch.setattr(database_import, 'POSTGRESQL_REQUIRED_VERSION', (100, 4)) with pytest.raises(UsageError, match='PostgreSQL server is too old.'): database_import.setup_database_skeleton(f'dbname={self.DBNAME}') - def test_create_db_explicit_ro_user(self): database_import.setup_database_skeleton(f'dbname={self.DBNAME}', rouser='postgres') - def test_create_db_missing_ro_user(self): with pytest.raises(UsageError, match='Missing read-only user.'): database_import.setup_database_skeleton(f'dbname={self.DBNAME}', rouser='sdfwkjkjgdugu2;jgsafkljas;') - def test_setup_extensions_old_postgis(self, monkeypatch): monkeypatch.setattr(database_import, 'POSTGIS_REQUIRED_VERSION', (50, 50)) @@ -173,7 +167,7 @@ def test_truncate_database_tables(temp_db_conn, temp_db_cursor, table_factory, w @pytest.mark.parametrize("threads", (1, 5)) @pytest.mark.asyncio async def test_load_data(dsn, place_row, placex_table, osmline_table, - temp_db_cursor, threads): + temp_db_cursor, threads): for func in ('precompute_words', 'getorcreate_housenumber_id', 'make_standard_name'): temp_db_cursor.execute(pysql.SQL("""CREATE FUNCTION {} (src TEXT) RETURNS TEXT AS $$ SELECT 'a'::TEXT $$ LANGUAGE SQL @@ -198,11 +192,9 @@ class TestSetupSQL: self.config = def_config - def write_sql(self, fname, content): (self.config.lib_dir.sql / fname).write_text(content) - @pytest.mark.parametrize("reverse", [True, False]) def test_create_tables(self, temp_db_conn, temp_db_cursor, reverse): self.write_sql('tables.sql', @@ -213,7 +205,6 @@ class TestSetupSQL: temp_db_cursor.scalar('SELECT test()') == reverse - def test_create_table_triggers(self, temp_db_conn, temp_db_cursor): self.write_sql('table-triggers.sql', """CREATE FUNCTION test() RETURNS TEXT @@ -223,7 +214,6 @@ class TestSetupSQL: temp_db_cursor.scalar('SELECT test()') == 'a' - def test_create_partition_tables(self, temp_db_conn, temp_db_cursor): self.write_sql('partition-tables.src.sql', """CREATE FUNCTION test() RETURNS TEXT @@ -233,7 +223,6 @@ class TestSetupSQL: temp_db_cursor.scalar('SELECT test()') == 'b' - @pytest.mark.parametrize("drop", [True, False]) @pytest.mark.asyncio async def test_create_search_indices(self, temp_db_conn, temp_db_cursor, drop): diff --git a/test/python/tools/test_exec_utils.py b/test/python/tools/test_exec_utils.py index 666ef0b8..216f1a40 100644 --- a/test/python/tools/test_exec_utils.py +++ b/test/python/tools/test_exec_utils.py @@ -2,19 +2,14 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for tools.exec_utils module. """ -from pathlib import Path -import subprocess - -import pytest - -from nominatim_db.config import Configuration import nominatim_db.tools.exec_utils as exec_utils + def test_run_osm2pgsql(osm2pgsql_options): osm2pgsql_options['append'] = False osm2pgsql_options['import_file'] = 'foo.bar' diff --git a/test/python/tools/test_freeze.py b/test/python/tools/test_freeze.py index f64850fb..21e49b8d 100644 --- a/test/python/tools/test_freeze.py +++ b/test/python/tools/test_freeze.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for freeze functions (removing unused database parts). @@ -26,6 +26,7 @@ NOMINATIM_DROP_TABLES = [ 'wikipedia_article', 'wikipedia_redirect' ] + def test_drop_tables(temp_db_conn, temp_db_cursor, table_factory): for table in NOMINATIM_RUNTIME_TABLES + NOMINATIM_DROP_TABLES: table_factory(table) @@ -42,6 +43,7 @@ def test_drop_tables(temp_db_conn, temp_db_cursor, table_factory): assert freeze.is_frozen(temp_db_conn) + def test_drop_flatnode_file_no_file(): freeze.drop_flatnode_file(None) diff --git a/test/python/tools/test_import_special_phrases.py b/test/python/tools/test_import_special_phrases.py index 0d33e6e0..d8fe8946 100644 --- a/test/python/tools/test_import_special_phrases.py +++ b/test/python/tools/test_import_special_phrases.py @@ -2,20 +2,17 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for import special phrases methods of the class SPImporter. """ -from shutil import copyfile import pytest from nominatim_db.tools.special_phrases.sp_importer import SPImporter from nominatim_db.tools.special_phrases.sp_wiki_loader import SPWikiLoader from nominatim_db.tools.special_phrases.special_phrase import SpecialPhrase -from nominatim_db.errors import UsageError -from cursor import CursorForTesting @pytest.fixture def sp_importer(temp_db_conn, def_config, monkeypatch): @@ -53,6 +50,7 @@ def test_fetch_existing_place_classtype_tables(sp_importer, table_factory): contained_table = sp_importer.table_phrases_to_delete.pop() assert contained_table == 'place_classtype_testclasstypetable' + def test_check_sanity_class(sp_importer): """ Check for _check_sanity() method. @@ -65,6 +63,7 @@ def test_check_sanity_class(sp_importer): assert sp_importer._check_sanity(SpecialPhrase('en', 'class', 'type', '')) + def test_load_white_and_black_lists(sp_importer): """ Test that _load_white_and_black_lists() well return @@ -93,6 +92,7 @@ def test_create_place_classtype_indexes(temp_db_with_extensions, assert check_placeid_and_centroid_indexes(temp_db_cursor, phrase_class, phrase_type) + def test_create_place_classtype_table(temp_db_conn, temp_db_cursor, placex_table, sp_importer): """ Test that _create_place_classtype_table() create @@ -105,6 +105,7 @@ def test_create_place_classtype_table(temp_db_conn, temp_db_cursor, placex_table assert check_table_exist(temp_db_cursor, phrase_class, phrase_type) + def test_grant_access_to_web_user(temp_db_conn, temp_db_cursor, table_factory, def_config, sp_importer): """ @@ -120,7 +121,9 @@ def test_grant_access_to_web_user(temp_db_conn, temp_db_cursor, table_factory, sp_importer._grant_access_to_webuser(phrase_class, phrase_type) temp_db_conn.commit() - assert check_grant_access(temp_db_cursor, def_config.DATABASE_WEBUSER, phrase_class, phrase_type) + assert check_grant_access(temp_db_cursor, def_config.DATABASE_WEBUSER, + phrase_class, phrase_type) + def test_create_place_classtype_table_and_indexes( temp_db_cursor, def_config, placex_table, @@ -141,6 +144,7 @@ def test_create_place_classtype_table_and_indexes( assert check_placeid_and_centroid_indexes(temp_db_cursor, pair[0], pair[1]) assert check_grant_access(temp_db_cursor, def_config.DATABASE_WEBUSER, pair[0], pair[1]) + def test_remove_non_existent_tables_from_db(sp_importer, default_phrases, temp_db_conn, temp_db_cursor): """ @@ -168,7 +172,7 @@ def test_remove_non_existent_tables_from_db(sp_importer, default_phrases, temp_db_conn.commit() assert temp_db_cursor.row_set(query_tables) \ - == {('place_classtype_testclasstypetable_to_keep', )} + == {('place_classtype_testclasstypetable_to_keep', )} @pytest.mark.parametrize("should_replace", [(True), (False)]) @@ -182,8 +186,8 @@ def test_import_phrases(monkeypatch, temp_db_cursor, def_config, sp_importer, It should also update the database well by deleting or preserving existing entries of the database. """ - #Add some data to the database before execution in order to test - #what is deleted and what is preserved. + # Add some data to the database before execution in order to test + # what is deleted and what is preserved. table_factory('place_classtype_amenity_animal_shelter') table_factory('place_classtype_wrongclass_wrongtype') @@ -209,6 +213,7 @@ def test_import_phrases(monkeypatch, temp_db_cursor, def_config, sp_importer, if should_replace: assert not temp_db_cursor.table_exists('place_classtype_wrongclass_wrongtype') + def check_table_exist(temp_db_cursor, phrase_class, phrase_type): """ Verify that the place_classtype table exists for the given @@ -231,6 +236,7 @@ def check_grant_access(temp_db_cursor, user, phrase_class, phrase_type): AND privilege_type='SELECT'""".format(table_name, user)) return temp_db_cursor.fetchone() + def check_placeid_and_centroid_indexes(temp_db_cursor, phrase_class, phrase_type): """ Check that the place_id index and centroid index exist for the diff --git a/test/python/tools/test_migration.py b/test/python/tools/test_migration.py index 0b4d2ec6..00f6a7d7 100644 --- a/test/python/tools/test_migration.py +++ b/test/python/tools/test_migration.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for migration functions @@ -11,9 +11,9 @@ import pytest from nominatim_db.tools import migration from nominatim_db.errors import UsageError -from nominatim_db.db.connection import server_version_tuple import nominatim_db.version + class DummyTokenizer: def update_sql_functions(self, config): @@ -49,6 +49,7 @@ def test_run_single_migration(temp_db_with_extensions, def_config, temp_db_curso str(nominatim_db.version.NominatimVersion(*oldversion))) done = {'old': False, 'new': False} + def _migration(**_): """ Dummy migration""" done['new'] = True @@ -69,7 +70,7 @@ def test_run_single_migration(temp_db_with_extensions, def_config, temp_db_curso assert property_table.get('database_version') == str(nominatim_db.version.NOMINATIM_VERSION) -###### Tests for specific migrations +# Tests for specific migrations # # Each migration should come with two tests: # 1. Test that migration from old to new state works as expected. diff --git a/test/python/tools/test_postcodes.py b/test/python/tools/test_postcodes.py index f035bb19..b03c9748 100644 --- a/test/python/tools/test_postcodes.py +++ b/test/python/tools/test_postcodes.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for functions to maintain the artificial postcode table. @@ -15,6 +15,7 @@ from nominatim_db.tools import postcodes from nominatim_db.data import country_info import dummy_tokenizer + class MockPostcodeTable: """ A location_postcode table for testing. """ @@ -35,7 +36,7 @@ class MockPostcodeTable: RETURNS TEXT AS $$ BEGIN RETURN postcode; END; $$ LANGUAGE plpgsql; CREATE OR REPLACE FUNCTION get_country_code(place geometry) - RETURNS TEXT AS $$ BEGIN + RETURNS TEXT AS $$ BEGIN RETURN null; END; $$ LANGUAGE plpgsql; """) @@ -51,7 +52,6 @@ class MockPostcodeTable: (country, postcode, x, y)) self.conn.commit() - @property def row_set(self): with self.conn.cursor() as cur: @@ -180,7 +180,7 @@ def test_postcodes_extern(dsn, postcode_table, tmp_path, ('xx', 'CD 4511', -10, -5)} -def test_postcodes_extern_bad_column(dsn, postcode_table, tmp_path, +def test_postcodes_extern_bad_column(dsn, postcode_table, tmp_path, insert_implicit_postcode, tokenizer): insert_implicit_postcode(1, 'xx', 'POINT(10 12)', dict(postcode='AB 4511')) @@ -204,6 +204,7 @@ def test_postcodes_extern_bad_number(dsn, insert_implicit_postcode, assert postcode_table.row_set == {('xx', 'AB 4511', 10, 12), ('xx', 'CD 4511', -10, -5)} + def test_can_compute(dsn, table_factory): assert not postcodes.can_compute(dsn) table_factory('place') @@ -211,10 +212,10 @@ def test_can_compute(dsn, table_factory): def test_no_placex_entry(dsn, tmp_path, temp_db_cursor, place_row, postcode_table, tokenizer): - #Rewrite the get_country_code function to verify its execution. + # Rewrite the get_country_code function to verify its execution. temp_db_cursor.execute(""" CREATE OR REPLACE FUNCTION get_country_code(place geometry) - RETURNS TEXT AS $$ BEGIN + RETURNS TEXT AS $$ BEGIN RETURN 'yy'; END; $$ LANGUAGE plpgsql; """) @@ -224,11 +225,12 @@ def test_no_placex_entry(dsn, tmp_path, temp_db_cursor, place_row, postcode_tabl assert postcode_table.row_set == {('yy', 'AB 4511', 10, 12)} -def test_discard_badly_formatted_postcodes(dsn, tmp_path, temp_db_cursor, place_row, postcode_table, tokenizer): - #Rewrite the get_country_code function to verify its execution. +def test_discard_badly_formatted_postcodes(dsn, tmp_path, temp_db_cursor, place_row, + postcode_table, tokenizer): + # Rewrite the get_country_code function to verify its execution. temp_db_cursor.execute(""" CREATE OR REPLACE FUNCTION get_country_code(place geometry) - RETURNS TEXT AS $$ BEGIN + RETURNS TEXT AS $$ BEGIN RETURN 'fr'; END; $$ LANGUAGE plpgsql; """) diff --git a/test/python/tools/test_refresh.py b/test/python/tools/test_refresh.py index 8f735180..95feef0d 100644 --- a/test/python/tools/test_refresh.py +++ b/test/python/tools/test_refresh.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Test for various refresh functions. @@ -12,7 +12,7 @@ from pathlib import Path import pytest from nominatim_db.tools import refresh -from nominatim_db.db.connection import postgis_version_tuple + def test_refresh_import_wikipedia_not_existing(dsn): assert refresh.import_wikipedia_articles(dsn, Path('.')) == 1 @@ -21,6 +21,7 @@ def test_refresh_import_wikipedia_not_existing(dsn): def test_refresh_import_secondary_importance_non_existing(dsn): assert refresh.import_secondary_importance(dsn, Path('.')) == 1 + def test_refresh_import_secondary_importance_testdb(dsn, src_dir, temp_db_conn, temp_db_cursor): temp_db_cursor.execute('CREATE EXTENSION postgis') temp_db_cursor.execute('CREATE EXTENSION postgis_raster') diff --git a/test/python/tools/test_refresh_address_levels.py b/test/python/tools/test_refresh_address_levels.py index 6e094cdc..f2bfdea6 100644 --- a/test/python/tools/test_refresh_address_levels.py +++ b/test/python/tools/test_refresh_address_levels.py @@ -2,23 +2,24 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for function for importing address ranks. """ import json -from pathlib import Path import pytest from nominatim_db.tools.refresh import load_address_levels, load_address_levels_from_config + def test_load_ranks_def_config(temp_db_conn, temp_db_cursor, def_config): load_address_levels_from_config(temp_db_conn, def_config) assert temp_db_cursor.table_rows('address_levels') > 0 + def test_load_ranks_from_project_dir(project_env, temp_db_conn, temp_db_cursor): test_file = project_env.project_dir / 'address-levels.json' test_file.write_text('[{"tags":{"place":{"sea":2}}}]') @@ -43,14 +44,14 @@ def test_load_ranks_country(temp_db_conn, temp_db_cursor): "tags": {"place": {"village": 15}}}, {"countries": ['uk', 'us'], "tags": {"place": {"village": 16}}} - ]) + ]) assert temp_db_cursor.row_set('SELECT * FROM levels') == \ set([(None, 'place', 'village', 14, 14), ('de', 'place', 'village', 15, 15), ('uk', 'place', 'village', 16, 16), ('us', 'place', 'village', 16, 16), - ]) + ]) def test_load_ranks_default_value(temp_db_conn, temp_db_cursor): @@ -58,33 +59,33 @@ def test_load_ranks_default_value(temp_db_conn, temp_db_cursor): [{"tags": {"boundary": {"": 28}}}, {"countries": ['hu'], "tags": {"boundary": {"": 29}}} - ]) + ]) assert temp_db_cursor.row_set('SELECT * FROM levels') == \ set([(None, 'boundary', None, 28, 28), ('hu', 'boundary', None, 29, 29), - ]) + ]) def test_load_ranks_multiple_keys(temp_db_conn, temp_db_cursor): load_address_levels(temp_db_conn, 'levels', [{"tags": {"place": {"city": 14}, - "boundary": {"administrative2" : 4}} - }]) + "boundary": {"administrative2": 4}} + }]) assert temp_db_cursor.row_set('SELECT * FROM levels') == \ set([(None, 'place', 'city', 14, 14), (None, 'boundary', 'administrative2', 4, 4), - ]) + ]) def test_load_ranks_address(temp_db_conn, temp_db_cursor): load_address_levels(temp_db_conn, 'levels', [{"tags": {"place": {"city": 14, - "town" : [14, 13]}} - }]) + "town": [14, 13]}} + }]) assert temp_db_cursor.row_set('SELECT * FROM levels') == \ set([(None, 'place', 'city', 14, 14), (None, 'place', 'town', 14, 13), - ]) + ]) diff --git a/test/python/tools/test_refresh_create_functions.py b/test/python/tools/test_refresh_create_functions.py index 984a1610..bd8724d6 100644 --- a/test/python/tools/test_refresh_create_functions.py +++ b/test/python/tools/test_refresh_create_functions.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for creating PL/pgSQL functions for Nominatim. @@ -11,6 +11,7 @@ import pytest from nominatim_db.tools.refresh import create_functions + class TestCreateFunctions: @pytest.fixture(autouse=True) def init_env(self, sql_preprocessor, temp_db_conn, def_config, tmp_path): @@ -18,12 +19,10 @@ class TestCreateFunctions: self.config = def_config def_config.lib_dir.sql = tmp_path - def write_functions(self, content): sqlfile = self.config.lib_dir.sql / 'functions.sql' sqlfile.write_text(content) - def test_create_functions(self, temp_db_cursor): self.write_functions("""CREATE OR REPLACE FUNCTION test() RETURNS INTEGER AS $$ @@ -37,7 +36,6 @@ class TestCreateFunctions: assert temp_db_cursor.scalar('SELECT test()') == 43 - @pytest.mark.parametrize("dbg,ret", ((True, 43), (False, 22))) def test_create_functions_with_template(self, temp_db_cursor, dbg, ret): self.write_functions("""CREATE OR REPLACE FUNCTION test() RETURNS INTEGER diff --git a/test/python/tools/test_refresh_wiki_data.py b/test/python/tools/test_refresh_wiki_data.py index 997ba04d..046e9191 100644 --- a/test/python/tools/test_refresh_wiki_data.py +++ b/test/python/tools/test_refresh_wiki_data.py @@ -12,7 +12,10 @@ import csv import pytest -from nominatim_db.tools.refresh import import_wikipedia_articles, recompute_importance, create_functions +from nominatim_db.tools.refresh import (import_wikipedia_articles, + recompute_importance, + create_functions) + @pytest.fixture def wiki_csv(tmp_path, sql_preprocessor): @@ -25,7 +28,7 @@ def wiki_csv(tmp_path, sql_preprocessor): for lang, title, importance, wd in data: writer.writerow({'language': lang, 'type': 'a', 'title': title, 'importance': str(importance), - 'wikidata_id' : wd}) + 'wikidata_id': wd}) return tmp_path return _import diff --git a/test/python/tools/test_replication.py b/test/python/tools/test_replication.py index 392ea075..347899bd 100644 --- a/test/python/tools/test_replication.py +++ b/test/python/tools/test_replication.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for replication functionality. @@ -22,13 +22,15 @@ OSM_NODE_DATA = """\ -""" +""" # noqa + @pytest.fixture(autouse=True) def setup_status_table(status_table): pass -### init replication + +# init replication def test_init_replication_bad_base_url(monkeypatch, place_row, temp_db_conn): place_row(osm_type='N', osm_id=100) @@ -50,13 +52,13 @@ def test_init_replication_success(monkeypatch, place_row, temp_db_conn, temp_db_ nominatim_db.tools.replication.init_replication(temp_db_conn, 'https://test.io') expected_date = dt.datetime.strptime('2006-01-27T19:09:10', status.ISODATE_FORMAT)\ - .replace(tzinfo=dt.timezone.utc) + .replace(tzinfo=dt.timezone.utc) assert temp_db_cursor.row_set("SELECT * FROM import_status") \ - == {(expected_date, 234, True)} + == {(expected_date, 234, True)} -### checking for updates +# checking for updates def test_check_for_updates_empty_status_table(temp_db_conn): assert nominatim_db.tools.replication.check_for_updates(temp_db_conn, 'https://test.io') == 254 @@ -87,10 +89,11 @@ def test_check_for_updates_no_new_data(monkeypatch, temp_db_conn, "get_state_info", lambda self: OsmosisState(server_sequence, date)) - assert nominatim_db.tools.replication.check_for_updates(temp_db_conn, 'https://test.io') == result + assert result == \ + nominatim_db.tools.replication.check_for_updates(temp_db_conn, 'https://test.io') -### updating +# updating @pytest.fixture def update_options(tmpdir): @@ -100,6 +103,7 @@ def update_options(tmpdir): import_file=tmpdir / 'foo.osm', max_diff_size=1) + def test_update_empty_status_table(dsn): with pytest.raises(UsageError): nominatim_db.tools.replication.update(dsn, {}) @@ -109,7 +113,7 @@ def test_update_already_indexed(temp_db_conn, dsn): status.set_status(temp_db_conn, dt.datetime.now(dt.timezone.utc), seq=34, indexed=False) assert nominatim_db.tools.replication.update(dsn, dict(indexed_only=True)) \ - == nominatim_db.tools.replication.UpdateState.MORE_PENDING + == nominatim_db.tools.replication.UpdateState.MORE_PENDING def test_update_no_data_no_sleep(monkeypatch, temp_db_conn, dsn, update_options): @@ -124,7 +128,7 @@ def test_update_no_data_no_sleep(monkeypatch, temp_db_conn, dsn, update_options) monkeypatch.setattr(time, 'sleep', sleeptime.append) assert nominatim_db.tools.replication.update(dsn, update_options) \ - == nominatim_db.tools.replication.UpdateState.NO_CHANGES + == nominatim_db.tools.replication.UpdateState.NO_CHANGES assert not sleeptime @@ -141,7 +145,7 @@ def test_update_no_data_sleep(monkeypatch, temp_db_conn, dsn, update_options): monkeypatch.setattr(time, 'sleep', sleeptime.append) assert nominatim_db.tools.replication.update(dsn, update_options) \ - == nominatim_db.tools.replication.UpdateState.NO_CHANGES + == nominatim_db.tools.replication.UpdateState.NO_CHANGES assert len(sleeptime) == 1 assert sleeptime[0] < 3600 diff --git a/test/python/tools/test_sp_csv_loader.py b/test/python/tools/test_sp_csv_loader.py index 9d0ad9cc..67d6eed5 100644 --- a/test/python/tools/test_sp_csv_loader.py +++ b/test/python/tools/test_sp_csv_loader.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for methods of the SPCsvLoader class. @@ -13,6 +13,7 @@ from nominatim_db.errors import UsageError from nominatim_db.tools.special_phrases.sp_csv_loader import SPCsvLoader from nominatim_db.tools.special_phrases.special_phrase import SpecialPhrase + @pytest.fixture def sp_csv_loader(src_dir): """ diff --git a/test/python/tools/test_sp_wiki_loader.py b/test/python/tools/test_sp_wiki_loader.py index 5c37c32f..b8e41cbe 100644 --- a/test/python/tools/test_sp_wiki_loader.py +++ b/test/python/tools/test_sp_wiki_loader.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for methods of the SPWikiLoader class. @@ -36,22 +36,22 @@ def test_generate_phrases(sp_wiki_loader): """ phrases = list(sp_wiki_loader.generate_phrases()) - assert set((p.p_label, p.p_class, p.p_type, p.p_operator) for p in phrases) ==\ - {('Zip Line', 'aerialway', 'zip_line', '-'), - ('Zip Lines', 'aerialway', 'zip_line', '-'), - ('Zip Line in', 'aerialway', 'zip_line', 'in'), - ('Zip Lines in', 'aerialway', 'zip_line', 'in'), - ('Zip Line near', 'aerialway', 'zip_line', 'near'), - ('Animal shelter', 'amenity', 'animal_shelter', '-'), - ('Animal shelters', 'amenity', 'animal_shelter', '-'), - ('Animal shelter in', 'amenity', 'animal_shelter', 'in'), - ('Animal shelters in', 'amenity', 'animal_shelter', 'in'), - ('Animal shelter near', 'amenity', 'animal_shelter', 'near'), - ('Animal shelters near', 'amenity', 'animal_shelter', 'near'), - ('Drinking Water near', 'amenity', 'drinking_water', 'near'), - ('Water', 'amenity', 'drinking_water', '-'), - ('Water in', 'amenity', 'drinking_water', 'in'), - ('Water near', 'amenity', 'drinking_water', 'near'), - ('Embassy', 'amenity', 'embassy', '-'), - ('Embassys', 'amenity', 'embassy', '-'), - ('Embassies', 'amenity', 'embassy', '-')} + assert set((p.p_label, p.p_class, p.p_type, p.p_operator) for p in phrases) == \ + {('Zip Line', 'aerialway', 'zip_line', '-'), + ('Zip Lines', 'aerialway', 'zip_line', '-'), + ('Zip Line in', 'aerialway', 'zip_line', 'in'), + ('Zip Lines in', 'aerialway', 'zip_line', 'in'), + ('Zip Line near', 'aerialway', 'zip_line', 'near'), + ('Animal shelter', 'amenity', 'animal_shelter', '-'), + ('Animal shelters', 'amenity', 'animal_shelter', '-'), + ('Animal shelter in', 'amenity', 'animal_shelter', 'in'), + ('Animal shelters in', 'amenity', 'animal_shelter', 'in'), + ('Animal shelter near', 'amenity', 'animal_shelter', 'near'), + ('Animal shelters near', 'amenity', 'animal_shelter', 'near'), + ('Drinking Water near', 'amenity', 'drinking_water', 'near'), + ('Water', 'amenity', 'drinking_water', '-'), + ('Water in', 'amenity', 'drinking_water', 'in'), + ('Water near', 'amenity', 'drinking_water', 'near'), + ('Embassy', 'amenity', 'embassy', '-'), + ('Embassys', 'amenity', 'embassy', '-'), + ('Embassies', 'amenity', 'embassy', '-')} diff --git a/test/python/tools/test_tiger_data.py b/test/python/tools/test_tiger_data.py index 5d65fafb..f7dfe32e 100644 --- a/test/python/tools/test_tiger_data.py +++ b/test/python/tools/test_tiger_data.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Test for tiger data function @@ -11,12 +11,13 @@ import tarfile from textwrap import dedent import pytest -import pytest_asyncio +import pytest_asyncio # noqa: F401 from nominatim_db.db.connection import execute_scalar from nominatim_db.tools import tiger_data, freeze from nominatim_db.errors import UsageError + class MockTigerTable: def __init__(self, conn): @@ -40,6 +41,7 @@ class MockTigerTable: cur.execute("SELECT * FROM tiger LIMIT 1") return cur.fetchone() + @pytest.fixture def tiger_table(def_config, temp_db_conn, sql_preprocessor, temp_db_with_extensions, tmp_path): @@ -87,7 +89,7 @@ async def test_add_tiger_data(def_config, src_dir, tiger_table, tokenizer_mock, @pytest.mark.asyncio async def test_add_tiger_data_database_frozen(def_config, temp_db_conn, tiger_table, tokenizer_mock, - tmp_path): + tmp_path): freeze.drop_update_tables(temp_db_conn) with pytest.raises(UsageError) as excinfo: @@ -100,7 +102,7 @@ async def test_add_tiger_data_database_frozen(def_config, temp_db_conn, tiger_ta @pytest.mark.asyncio async def test_add_tiger_data_no_files(def_config, tiger_table, tokenizer_mock, - tmp_path): + tmp_path): await tiger_data.add_tiger_data(str(tmp_path), def_config, 1, tokenizer_mock()) assert tiger_table.count() == 0 @@ -108,7 +110,7 @@ async def test_add_tiger_data_no_files(def_config, tiger_table, tokenizer_mock, @pytest.mark.asyncio async def test_add_tiger_data_bad_file(def_config, tiger_table, tokenizer_mock, - tmp_path): + tmp_path): sqlfile = tmp_path / '1010.csv' sqlfile.write_text("""Random text""") @@ -119,7 +121,7 @@ async def test_add_tiger_data_bad_file(def_config, tiger_table, tokenizer_mock, @pytest.mark.asyncio async def test_add_tiger_data_hnr_nan(def_config, tiger_table, tokenizer_mock, - csv_factory, tmp_path): + csv_factory, tmp_path): csv_factory('file1', hnr_from=99) csv_factory('file2', hnr_from='L12') csv_factory('file3', hnr_to='12.4') @@ -133,7 +135,7 @@ async def test_add_tiger_data_hnr_nan(def_config, tiger_table, tokenizer_mock, @pytest.mark.parametrize("threads", (1, 5)) @pytest.mark.asyncio async def test_add_tiger_data_tarfile(def_config, tiger_table, tokenizer_mock, - tmp_path, src_dir, threads): + tmp_path, src_dir, threads): tar = tarfile.open(str(tmp_path / 'sample.tar.gz'), "w:gz") tar.add(str(src_dir / 'test' / 'testdb' / 'tiger' / '01001.csv')) tar.close() @@ -146,7 +148,7 @@ async def test_add_tiger_data_tarfile(def_config, tiger_table, tokenizer_mock, @pytest.mark.asyncio async def test_add_tiger_data_bad_tarfile(def_config, tiger_table, tokenizer_mock, - tmp_path): + tmp_path): tarfile = tmp_path / 'sample.tar.gz' tarfile.write_text("""Random text""") @@ -156,7 +158,7 @@ async def test_add_tiger_data_bad_tarfile(def_config, tiger_table, tokenizer_moc @pytest.mark.asyncio async def test_add_tiger_data_empty_tarfile(def_config, tiger_table, tokenizer_mock, - tmp_path): + tmp_path): tar = tarfile.open(str(tmp_path / 'sample.tar.gz'), "w:gz") tar.add(__file__) tar.close() diff --git a/test/python/utils/test_centroid.py b/test/python/utils/test_centroid.py index bac0edb3..664d5cd7 100644 --- a/test/python/utils/test_centroid.py +++ b/test/python/utils/test_centroid.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for centroid computation. @@ -11,6 +11,7 @@ import pytest from nominatim_db.utils.centroid import PointsCentroid + def test_empty_set(): c = PointsCentroid() @@ -18,7 +19,7 @@ def test_empty_set(): c.centroid() -@pytest.mark.parametrize("centroid", [(0,0), (-1, 3), [0.0000032, 88.4938]]) +@pytest.mark.parametrize("centroid", [(0, 0), (-1, 3), [0.0000032, 88.4938]]) def test_one_point_centroid(centroid): c = PointsCentroid() diff --git a/test/python/utils/test_json_writer.py b/test/python/utils/test_json_writer.py index 53e3f4d3..c0946f01 100644 --- a/test/python/utils/test_json_writer.py +++ b/test/python/utils/test_json_writer.py @@ -2,7 +2,7 @@ # # This file is part of Nominatim. (https://nominatim.org) # -# Copyright (C) 2024 by the Nominatim developer community. +# Copyright (C) 2025 by the Nominatim developer community. # For a full list of authors see the git log. """ Tests for the streaming JSON writer. @@ -13,6 +13,7 @@ import pytest from nominatim_api.utils.json_writer import JsonWriter + @pytest.mark.parametrize("inval,outstr", [(None, 'null'), (True, 'true'), (False, 'false'), (23, '23'), (0, '0'), (-1.3, '-1.3'), @@ -71,6 +72,7 @@ def test_object_single_entry(): assert writer() == '{"something":5}' json.loads(writer()) + def test_object_many_values(): writer = JsonWriter()\ .start_object()\ @@ -82,6 +84,7 @@ def test_object_many_values(): assert writer() == '{"foo":null,"bar":{},"baz":"b\\taz"}' json.loads(writer()) + def test_object_many_values_without_none(): writer = JsonWriter()\ .start_object()\ @@ -89,7 +92,7 @@ def test_object_many_values_without_none(): .keyval_not_none('bar', None)\ .keyval_not_none('baz', '')\ .keyval_not_none('eve', False, - transform = lambda v: 'yes' if v else 'no')\ + transform=lambda v: 'yes' if v else 'no')\ .end_object() assert writer() == '{"foo":0,"baz":"","eve":"no"}'