]> git.openstreetmap.org Git - nominatim.git/commitdiff
use custom result formatters in CLI commands
authorSarah Hoffmann <lonvia@denofr.de>
Fri, 16 Aug 2024 17:30:57 +0000 (19:30 +0200)
committerSarah Hoffmann <lonvia@denofr.de>
Fri, 16 Aug 2024 17:30:57 +0000 (19:30 +0200)
src/nominatim_api/__init__.py
src/nominatim_api/v1/__init__.py
src/nominatim_db/clicmd/api.py
src/nominatim_db/clicmd/args.py
test/python/api/test_result_formatting_v1.py
test/python/api/test_result_formatting_v1_reverse.py
test/python/cli/test_cmd_api.py

index 50f99701a00fd9a462ab0531ae1cff342d649946..242ff892ffab4a96e231ae060f83621e6918e974 100644 (file)
@@ -39,6 +39,7 @@ from .results import (SourceTable as SourceTable,
                       SearchResult as SearchResult,
                       SearchResults as SearchResults)
 from .localization import (Locales as Locales)
-from .result_formatting import (FormatDispatcher as FormatDispatcher)
+from .result_formatting import (FormatDispatcher as FormatDispatcher,
+                                load_format_dispatcher as load_format_dispatcher)
 
 from .version import NOMINATIM_API_VERSION as __version__
index c7f150f0da28f0bddd071b7eabd9d312475904e2..4f684a91782ae1cdc55e128c712dc8a2e7e6cae4 100644 (file)
@@ -11,9 +11,3 @@ Implementation of API version v1 (aka the legacy version).
 #pylint: disable=useless-import-alias
 
 from .server_glue import ROUTES as ROUTES
-
-from . import format as _format
-
-list_formats = _format.dispatch.list_formats
-supports_format = _format.dispatch.supports_format
-format_result = _format.dispatch.format_result
index fac88bdd34a2684683e0313465699cab5af9bd7e..3deb7e45e04f181f5d1babf0b023064be00856c4 100644 (file)
@@ -7,7 +7,7 @@
 """
 Subcommand definitions for API calls from the command line.
 """
-from typing import Dict, Any, Optional
+from typing import Dict, Any, Optional, Type, Mapping
 import argparse
 import logging
 import json
@@ -15,9 +15,8 @@ import sys
 from functools import reduce
 
 import nominatim_api as napi
-import nominatim_api.v1 as api_output
 from nominatim_api.v1.helpers import zoom_to_rank, deduplicate_results
-from nominatim_api.v1.format import dispatch as formatting
+from nominatim_api.server.content_types import CONTENT_JSON
 import nominatim_api.logging as loglib
 from ..errors import UsageError
 from .args import NominatimArgs
@@ -44,11 +43,16 @@ EXTRADATA_PARAMS = (
     ('namedetails', 'Include a list of alternative names')
 )
 
+def _add_list_format(parser: argparse.ArgumentParser) -> None:
+    group = parser.add_argument_group('Other options')
+    group.add_argument('--list-formats', action='store_true',
+                       help='List supported output formats and exit.')
+
+
 def _add_api_output_arguments(parser: argparse.ArgumentParser) -> None:
-    group = parser.add_argument_group('Output arguments')
-    group.add_argument('--format', default='jsonv2',
-                       choices=formatting.list_formats(napi.SearchResults) + ['debug'],
-                       help='Format of result')
+    group = parser.add_argument_group('Output formatting')
+    group.add_argument('--format', type=str, default='jsonv2',
+                       help='Format of result (use --list-format to see supported formats)')
     for name, desc in EXTRADATA_PARAMS:
         group.add_argument('--' + name, action='store_true', help=desc)
 
@@ -105,6 +109,24 @@ def _get_layers(args: NominatimArgs, default: napi.DataLayer) -> Optional[napi.D
                   (napi.DataLayer[s.upper()] for s in args.layers))
 
 
+def _list_formats(formatter: napi.FormatDispatcher, rtype: Type[Any]) -> int:
+    for fmt in formatter.list_formats(rtype):
+        print(fmt)
+    print('debug')
+
+    return 0
+
+
+def _print_output(formatter: napi.FormatDispatcher, result: Any,
+                  fmt: str, options: Mapping[str, Any]) -> None:
+    output = formatter.format_result(result, fmt, options)
+    if formatter.get_content_type(fmt) == CONTENT_JSON:
+        # reformat the result, so it is pretty-printed
+        json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
+    else:
+        sys.stdout.write(output)
+    sys.stdout.write('\n')
+
 class APISearch:
     """\
     Execute a search query.
@@ -135,18 +157,24 @@ class APISearch:
                            help='Preferred area to find search results')
         group.add_argument('--bounded', action='store_true',
                            help='Strictly restrict results to viewbox area')
-
-        group = parser.add_argument_group('Other arguments')
         group.add_argument('--no-dedupe', action='store_false', dest='dedupe',
                            help='Do not remove duplicates from the result list')
+        _add_list_format(parser)
 
 
     def run(self, args: NominatimArgs) -> int:
+        formatter = napi.load_format_dispatcher('v1', args.project_dir)
+
+        if args.list_formats:
+            return _list_formats(formatter, napi.SearchResults)
+
         if args.format == 'debug':
             loglib.set_log_output('text')
+        elif not formatter.supports_format(napi.SearchResults, args.format):
+            raise UsageError(f"Unsupported format '{args.format}'. "
+                             'Use --list-formats to see supported formats.')
 
         api = napi.NominatimAPI(args.project_dir)
-
         params: Dict[str, Any] = {'max_results': args.limit + min(args.limit, 10),
                                   'address_details': True, # needed for display name
                                   'geometry_output': _get_geometry_output(args),
@@ -177,19 +205,10 @@ class APISearch:
             print(loglib.get_and_disable())
             return 0
 
-        output = api_output.format_result(
-                    results,
-                    args.format,
-                    {'extratags': args.extratags,
-                     'namedetails': args.namedetails,
-                     'addressdetails': args.addressdetails})
-        if args.format != 'xml':
-            # reformat the result, so it is pretty-printed
-            json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
-        else:
-            sys.stdout.write(output)
-        sys.stdout.write('\n')
-
+        _print_output(formatter, results, args.format,
+                      {'extratags': args.extratags,
+                       'namedetails': args.namedetails,
+                       'addressdetails': args.addressdetails})
         return 0
 
 
@@ -205,9 +224,9 @@ class APIReverse:
 
     def add_args(self, parser: argparse.ArgumentParser) -> None:
         group = parser.add_argument_group('Query arguments')
-        group.add_argument('--lat', type=float, required=True,
+        group.add_argument('--lat', type=float,
                            help='Latitude of coordinate to look up (in WGS84)')
-        group.add_argument('--lon', type=float, required=True,
+        group.add_argument('--lon', type=float,
                            help='Longitude of coordinate to look up (in WGS84)')
         group.add_argument('--zoom', type=int,
                            help='Level of detail required for the address')
@@ -217,14 +236,25 @@ class APIReverse:
                            help='OSM id to lookup in format <NRW><id> (may be repeated)')
 
         _add_api_output_arguments(parser)
+        _add_list_format(parser)
 
 
     def run(self, args: NominatimArgs) -> int:
+        formatter = napi.load_format_dispatcher('v1', args.project_dir)
+
+        if args.list_formats:
+            return _list_formats(formatter, napi.ReverseResults)
+
         if args.format == 'debug':
             loglib.set_log_output('text')
+        elif not formatter.supports_format(napi.ReverseResults, args.format):
+            raise UsageError(f"Unsupported format '{args.format}'. "
+                             'Use --list-formats to see supported formats.')
 
-        api = napi.NominatimAPI(args.project_dir)
+        if args.lat is None or args.lon is None:
+            raise UsageError("lat' and 'lon' parameters are required.")
 
+        api = napi.NominatimAPI(args.project_dir)
         result = api.reverse(napi.Point(args.lon, args.lat),
                              max_rank=zoom_to_rank(args.zoom or 18),
                              layers=_get_layers(args, napi.DataLayer.ADDRESS | napi.DataLayer.POI),
@@ -238,18 +268,10 @@ class APIReverse:
             return 0
 
         if result:
-            output = api_output.format_result(
-                        napi.ReverseResults([result]),
-                        args.format,
-                        {'extratags': args.extratags,
-                         'namedetails': args.namedetails,
-                         'addressdetails': args.addressdetails})
-            if args.format != 'xml':
-                # reformat the result, so it is pretty-printed
-                json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
-            else:
-                sys.stdout.write(output)
-            sys.stdout.write('\n')
+            _print_output(formatter, napi.ReverseResults([result]), args.format,
+                          {'extratags': args.extratags,
+                           'namedetails': args.namedetails,
+                           'addressdetails': args.addressdetails})
 
             return 0
 
@@ -271,43 +293,45 @@ class APILookup:
     def add_args(self, parser: argparse.ArgumentParser) -> None:
         group = parser.add_argument_group('Query arguments')
         group.add_argument('--id', metavar='OSMID',
-                           action='append', required=True, dest='ids',
+                           action='append', dest='ids',
                            help='OSM id to lookup in format <NRW><id> (may be repeated)')
 
         _add_api_output_arguments(parser)
+        _add_list_format(parser)
 
 
     def run(self, args: NominatimArgs) -> int:
-        if args.format == 'debug':
-            loglib.set_log_output('text')
+        formatter = napi.load_format_dispatcher('v1', args.project_dir)
 
-        api = napi.NominatimAPI(args.project_dir)
+        if args.list_formats:
+            return _list_formats(formatter, napi.ReverseResults)
 
         if args.format == 'debug':
-            print(loglib.get_and_disable())
-            return 0
+            loglib.set_log_output('text')
+        elif not formatter.supports_format(napi.ReverseResults, args.format):
+            raise UsageError(f"Unsupported format '{args.format}'. "
+                             'Use --list-formats to see supported formats.')
+
+        if args.ids is None:
+            raise UsageError("'id' parameter required.")
 
         places = [napi.OsmID(o[0], int(o[1:])) for o in args.ids]
 
+        api = napi.NominatimAPI(args.project_dir)
         results = api.lookup(places,
                              address_details=True, # needed for display name
                              geometry_output=_get_geometry_output(args),
                              geometry_simplification=args.polygon_threshold or 0.0,
                              locales=_get_locales(args, api.config.DEFAULT_LANGUAGE))
 
-        output = api_output.format_result(
-                    results,
-                    args.format,
-                    {'extratags': args.extratags,
-                     'namedetails': args.namedetails,
-                     'addressdetails': args.addressdetails})
-        if args.format != 'xml':
-            # reformat the result, so it is pretty-printed
-            json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
-        else:
-            sys.stdout.write(output)
-        sys.stdout.write('\n')
+        if args.format == 'debug':
+            print(loglib.get_and_disable())
+            return 0
 
+        _print_output(formatter, results, args.format,
+                      {'extratags': args.extratags,
+                       'namedetails': args.namedetails,
+                       'addressdetails': args.addressdetails})
         return 0
 
 
@@ -323,20 +347,21 @@ class APIDetails:
 
     def add_args(self, parser: argparse.ArgumentParser) -> None:
         group = parser.add_argument_group('Query arguments')
-        objs = group.add_mutually_exclusive_group(required=True)
-        objs.add_argument('--node', '-n', type=int,
-                          help="Look up the OSM node with the given ID.")
-        objs.add_argument('--way', '-w', type=int,
-                          help="Look up the OSM way with the given ID.")
-        objs.add_argument('--relation', '-r', type=int,
-                          help="Look up the OSM relation with the given ID.")
-        objs.add_argument('--place_id', '-p', type=int,
-                          help='Database internal identifier of the OSM object to look up')
+        group.add_argument('--node', '-n', type=int,
+                           help="Look up the OSM node with the given ID.")
+        group.add_argument('--way', '-w', type=int,
+                           help="Look up the OSM way with the given ID.")
+        group.add_argument('--relation', '-r', type=int,
+                           help="Look up the OSM relation with the given ID.")
+        group.add_argument('--place_id', '-p', type=int,
+                           help='Database internal identifier of the OSM object to look up')
         group.add_argument('--class', dest='object_class',
                            help=("Class type to disambiguated multiple entries "
                                  "of the same object."))
 
         group = parser.add_argument_group('Output arguments')
+        group.add_argument('--format', type=str, default='json',
+                           help='Format of result (use --list-formats to see supported formats)')
         group.add_argument('--addressdetails', action='store_true',
                            help='Include a breakdown of the address into elements')
         group.add_argument('--keywords', action='store_true',
@@ -351,9 +376,21 @@ class APIDetails:
                            help='Include geometry of result')
         group.add_argument('--lang', '--accept-language', metavar='LANGS',
                            help='Preferred language order for presenting search results')
+        _add_list_format(parser)
 
 
     def run(self, args: NominatimArgs) -> int:
+        formatter = napi.load_format_dispatcher('v1', args.project_dir)
+
+        if args.list_formats:
+            return _list_formats(formatter, napi.DetailedResult)
+
+        if args.format == 'debug':
+            loglib.set_log_output('text')
+        elif not formatter.supports_format(napi.DetailedResult, args.format):
+            raise UsageError(f"Unsupported format '{args.format}'. "
+                             'Use --list-formats to see supported formats.')
+
         place: napi.PlaceRef
         if args.node:
             place = napi.OsmID('N', args.node, args.object_class)
@@ -361,12 +398,13 @@ class APIDetails:
             place = napi.OsmID('W', args.way, args.object_class)
         elif args.relation:
             place = napi.OsmID('R', args.relation, args.object_class)
-        else:
-            assert args.place_id is not None
+        elif  args.place_id is not None:
             place = napi.PlaceID(args.place_id)
+        else:
+            raise UsageError('One of the arguments --node/-n --way/-w '
+                             '--relation/-r --place_id/-p is required/')
 
         api = napi.NominatimAPI(args.project_dir)
-
         locales = _get_locales(args, api.config.DEFAULT_LANGUAGE)
         result = api.details(place,
                              address_details=args.addressdetails,
@@ -378,17 +416,14 @@ class APIDetails:
                                              else napi.GeometryFormat.NONE,
                             locales=locales)
 
+        if args.format == 'debug':
+            print(loglib.get_and_disable())
+            return 0
 
         if result:
-            output = api_output.format_result(
-                        result,
-                        'json',
-                        {'locales': locales,
-                         'group_hierarchy': args.group_hierarchy})
-            # reformat the result, so it is pretty-printed
-            json.dump(json.loads(output), sys.stdout, indent=4, ensure_ascii=False)
-            sys.stdout.write('\n')
-
+            _print_output(formatter, result, args.format or 'json',
+                          {'locales': locales,
+                           'group_hierarchy': args.group_hierarchy})
             return 0
 
         LOG.error("Object not found in database.")
@@ -406,13 +441,30 @@ class APIStatus:
     """
 
     def add_args(self, parser: argparse.ArgumentParser) -> None:
-        formats = api_output.list_formats(napi.StatusResult)
         group = parser.add_argument_group('API parameters')
-        group.add_argument('--format', default=formats[0], choices=formats,
-                           help='Format of result')
+        group.add_argument('--format', type=str, default='text',
+                           help='Format of result (use --list-formats to see supported formats)')
+        _add_list_format(parser)
 
 
     def run(self, args: NominatimArgs) -> int:
+        formatter = napi.load_format_dispatcher('v1', args.project_dir)
+
+        if args.list_formats:
+            return _list_formats(formatter, napi.StatusResult)
+
+        if args.format == 'debug':
+            loglib.set_log_output('text')
+        elif not formatter.supports_format(napi.StatusResult, args.format):
+            raise UsageError(f"Unsupported format '{args.format}'. "
+                             'Use --list-formats to see supported formats.')
+
         status = napi.NominatimAPI(args.project_dir).status()
-        print(api_output.format_result(status, args.format, {}))
+
+        if args.format == 'debug':
+            print(loglib.get_and_disable())
+            return 0
+
+        _print_output(formatter, status, args.format, {})
+
         return 0
index 6a11b089eb40e5650cdb2409a012d0a049ed8221..c74bca6245a40baf3b920a7311c2228d31fa043d 100644 (file)
@@ -137,6 +137,7 @@ class NominatimArgs:
 
     # Arguments to all query functions
     format: str
+    list_formats: bool
     addressdetails: bool
     extratags: bool
     namedetails: bool
index 6ac1bb6d037766808c337ff761eb738b5d21f62b..aaecab4539995121c0b5204b8eec1b6acb22dfec 100644 (file)
@@ -15,7 +15,7 @@ import json
 
 import pytest
 
-import nominatim_api.v1 as api_impl
+from nominatim_api.v1.format import dispatch as v1_format
 import nominatim_api as napi
 
 STATUS_FORMATS = {'text', 'json'}
@@ -23,30 +23,30 @@ STATUS_FORMATS = {'text', 'json'}
 # StatusResult
 
 def test_status_format_list():
-    assert set(api_impl.list_formats(napi.StatusResult)) == STATUS_FORMATS
+    assert set(v1_format.list_formats(napi.StatusResult)) == STATUS_FORMATS
 
 
 @pytest.mark.parametrize('fmt', list(STATUS_FORMATS))
 def test_status_supported(fmt):
-    assert api_impl.supports_format(napi.StatusResult, fmt)
+    assert v1_format.supports_format(napi.StatusResult, fmt)
 
 
 def test_status_unsupported():
-    assert not api_impl.supports_format(napi.StatusResult, 'gagaga')
+    assert not v1_format.supports_format(napi.StatusResult, 'gagaga')
 
 
 def test_status_format_text():
-    assert api_impl.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 api_impl.format_result(napi.StatusResult(500, 'message here'), 'text', {}) == 'ERROR: message here'
+    assert v1_format.format_result(napi.StatusResult(500, 'message here'), 'text', {}) == 'ERROR: message here'
 
 
 def test_status_format_json_minimal():
     status = napi.StatusResult(700, 'Bad format.')
 
-    result = api_impl.format_result(status, 'json', {})
+    result = v1_format.format_result(status, 'json', {})
 
     assert result == \
            f'{{"status":700,"message":"Bad format.","software_version":"{napi.__version__}"}}'
@@ -57,7 +57,7 @@ def test_status_format_json_full():
     status.data_updated = dt.datetime(2010, 2, 7, 20, 20, 3, 0, tzinfo=dt.timezone.utc)
     status.database_version = '5.6'
 
-    result = api_impl.format_result(status, 'json', {})
+    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"}}'
@@ -70,7 +70,7 @@ def test_search_details_minimal():
                                  ('place', 'thing'),
                                  napi.Point(1.0, 2.0))
 
-    result = api_impl.format_result(search, 'json', {})
+    result = v1_format.format_result(search, 'json', {})
 
     assert json.loads(result) == \
            {'category': 'place',
@@ -114,7 +114,7 @@ def test_search_details_full():
                   )
     search.localize(napi.Locales())
 
-    result = api_impl.format_result(search, 'json', {})
+    result = v1_format.format_result(search, 'json', {})
 
     assert json.loads(result) == \
            {'place_id': 37563,
@@ -153,7 +153,7 @@ def test_search_details_no_geometry(gtype, isarea):
                                napi.Point(1.0, 2.0),
                                geometry={'type': gtype})
 
-    result = api_impl.format_result(search, 'json', {})
+    result = v1_format.format_result(search, 'json', {})
     js = json.loads(result)
 
     assert js['geometry'] == {'type': 'Point', 'coordinates': [1.0, 2.0]}
@@ -166,7 +166,7 @@ def test_search_details_with_geometry():
                                  napi.Point(1.0, 2.0),
                                  geometry={'geojson': '{"type":"Point","coordinates":[56.947,-87.44]}'})
 
-    result = api_impl.format_result(search, 'json', {})
+    result = v1_format.format_result(search, 'json', {})
     js = json.loads(result)
 
     assert js['geometry'] == {'type': 'Point', 'coordinates': [56.947, -87.44]}
@@ -178,7 +178,7 @@ def test_search_details_with_icon_available():
                                  ('amenity', 'restaurant'),
                                  napi.Point(1.0, 2.0))
 
-    result = api_impl.format_result(search, 'json', {'icon_base_url': 'foo'})
+    result = v1_format.format_result(search, 'json', {'icon_base_url': 'foo'})
     js = json.loads(result)
 
     assert js['icon'] == 'foo/food_restaurant.p.20.png'
@@ -189,7 +189,7 @@ def test_search_details_with_icon_not_available():
                                  ('amenity', 'tree'),
                                  napi.Point(1.0, 2.0))
 
-    result = api_impl.format_result(search, 'json', {'icon_base_url': 'foo'})
+    result = v1_format.format_result(search, 'json', {'icon_base_url': 'foo'})
     js = json.loads(result)
 
     assert 'icon' not in js
@@ -212,7 +212,7 @@ def test_search_details_with_address_minimal():
                                                     distance=0.0)
                                  ])
 
-    result = api_impl.format_result(search, 'json', {})
+    result = v1_format.format_result(search, 'json', {})
     js = json.loads(result)
 
     assert js['address'] == [{'localname': '',
@@ -245,7 +245,7 @@ def test_search_details_with_further_infos(field, outfield):
                                              distance=0.034)
                             ])
 
-    result = api_impl.format_result(search, 'json', {})
+    result = v1_format.format_result(search, 'json', {})
     js = json.loads(result)
 
     assert js[outfield] == [{'localname': 'Trespass',
@@ -279,7 +279,7 @@ def test_search_details_grouped_hierarchy():
                                              distance=0.034)
                                      ])
 
-    result = api_impl.format_result(search, 'json', {'group_hierarchy': True})
+    result = v1_format.format_result(search, 'json', {'group_hierarchy': True})
     js = json.loads(result)
 
     assert js['hierarchy'] == {'note': [{'localname': 'Trespass',
@@ -303,7 +303,7 @@ def test_search_details_keywords_name():
                                      napi.WordInfo(23, 'foo', 'mefoo'),
                                      napi.WordInfo(24, 'foo', 'bafoo')])
 
-    result = api_impl.format_result(search, 'json', {'keywords': True})
+    result = v1_format.format_result(search, 'json', {'keywords': True})
     js = json.loads(result)
 
     assert js['keywords'] == {'name': [{'id': 23, 'token': 'foo'},
@@ -319,7 +319,7 @@ def test_search_details_keywords_address():
                                      napi.WordInfo(23, 'foo', 'mefoo'),
                                      napi.WordInfo(24, 'foo', 'bafoo')])
 
-    result = api_impl.format_result(search, 'json', {'keywords': True})
+    result = v1_format.format_result(search, 'json', {'keywords': True})
     js = json.loads(result)
 
     assert js['keywords'] == {'address': [{'id': 23, 'token': 'foo'},
index 1248fa9e2d77e48598fbab96c17745db19ac2906..2c036a65ba254d9058961a78eb0cfb7f827a45f7 100644 (file)
@@ -15,7 +15,7 @@ import xml.etree.ElementTree as ET
 
 import pytest
 
-import nominatim_api.v1 as api_impl
+from nominatim_api.v1.format import dispatch as v1_format
 import nominatim_api as napi
 
 FORMATS = ['json', 'jsonv2', 'geojson', 'geocodejson', 'xml']
@@ -26,7 +26,7 @@ def test_format_reverse_minimal(fmt):
                                  ('amenity', 'post_box'),
                                  napi.Point(0.3, -8.9))
 
-    raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt, {})
+    raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt, {})
 
     if fmt == 'xml':
         root = ET.fromstring(raw)
@@ -38,7 +38,7 @@ def test_format_reverse_minimal(fmt):
 
 @pytest.mark.parametrize('fmt', FORMATS)
 def test_format_reverse_no_result(fmt):
-    raw = api_impl.format_result(napi.ReverseResults(), fmt, {})
+    raw = v1_format.format_result(napi.ReverseResults(), fmt, {})
 
     if fmt == 'xml':
         root = ET.fromstring(raw)
@@ -55,7 +55,7 @@ def test_format_reverse_with_osm_id(fmt):
                                  place_id=5564,
                                  osm_object=('N', 23))
 
-    raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt, {})
+    raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt, {})
 
     if fmt == 'xml':
         root = ET.fromstring(raw).find('result')
@@ -103,7 +103,7 @@ def test_format_reverse_with_address(fmt):
                                  ]))
     reverse.localize(napi.Locales())
 
-    raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
+    raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
                                  {'addressdetails': True})
 
 
@@ -167,7 +167,7 @@ def test_format_reverse_geocodejson_special_parts():
 
     reverse.localize(napi.Locales())
 
-    raw = api_impl.format_result(napi.ReverseResults([reverse]), 'geocodejson',
+    raw = v1_format.format_result(napi.ReverseResults([reverse]), 'geocodejson',
                                  {'addressdetails': True})
 
     props = json.loads(raw)['features'][0]['properties']['geocoding']
@@ -183,7 +183,7 @@ def test_format_reverse_with_address_none(fmt):
                                  napi.Point(1.0, 2.0),
                                  address_rows=napi.AddressLines())
 
-    raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
+    raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
                                  {'addressdetails': True})
 
 
@@ -213,7 +213,7 @@ def test_format_reverse_with_extratags(fmt):
                                  napi.Point(1.0, 2.0),
                                  extratags={'one': 'A', 'two':'B'})
 
-    raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
+    raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
                                  {'extratags': True})
 
     if fmt == 'xml':
@@ -235,7 +235,7 @@ def test_format_reverse_with_extratags_none(fmt):
                                  ('place', 'thing'),
                                  napi.Point(1.0, 2.0))
 
-    raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
+    raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
                                  {'extratags': True})
 
     if fmt == 'xml':
@@ -258,7 +258,7 @@ def test_format_reverse_with_namedetails_with_name(fmt):
                                  napi.Point(1.0, 2.0),
                                  names={'name': 'A', 'ref':'1'})
 
-    raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
+    raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
                                  {'namedetails': True})
 
     if fmt == 'xml':
@@ -280,7 +280,7 @@ def test_format_reverse_with_namedetails_without_name(fmt):
                                  ('place', 'thing'),
                                  napi.Point(1.0, 2.0))
 
-    raw = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
+    raw = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
                                  {'namedetails': True})
 
     if fmt == 'xml':
@@ -302,7 +302,7 @@ def test_search_details_with_icon_available(fmt):
                                  ('amenity', 'restaurant'),
                                  napi.Point(1.0, 2.0))
 
-    result = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
+    result = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
                                     {'icon_base_url': 'foo'})
 
     js = json.loads(result)
@@ -316,7 +316,7 @@ def test_search_details_with_icon_not_available(fmt):
                                  ('amenity', 'tree'),
                                  napi.Point(1.0, 2.0))
 
-    result = api_impl.format_result(napi.ReverseResults([reverse]), fmt,
+    result = v1_format.format_result(napi.ReverseResults([reverse]), fmt,
                                     {'icon_base_url': 'foo'})
 
     assert 'icon' not in json.loads(result)
index 811eadfe4112adf513ff594f43b93d57694cc92a..1c0750d1dc1cf03630ccf51c9024bbec508562dc 100644 (file)
@@ -13,6 +13,15 @@ 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')
+
+
+@pytest.mark.parametrize('call', ['search', 'reverse', 'lookup', 'details', 'status'])
+def test_bad_format(cli_call, call):
+    assert 1 == cli_call(call, '--format', 'rsdfsdfsdfsaefsdfsd')
+
 
 class TestCliStatusCall: