diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index e562055d..00000000 --- a/.pylintrc +++ /dev/null @@ -1,22 +0,0 @@ -[MASTER] - -extension-pkg-whitelist=osmium,falcon -ignored-modules=icu,datrie - -[MESSAGES CONTROL] - -[TYPECHECK] - -# closing added here because it sometimes triggers a false positive with -# 'with' statements. -ignored-classes=NominatimArgs,closing -# 'too-many-ancestors' is triggered already by deriving from UserDict -# 'not-context-manager' disabled because it causes false positives once -# typed Python is enabled. See also https://github.com/PyCQA/pylint/issues/5273 -disable=too-few-public-methods,duplicate-code,too-many-ancestors,bad-option-value,no-self-use,not-context-manager,use-dict-literal,chained-comparison,attribute-defined-outside-init,too-many-boolean-expressions,contextmanager-generator-missing-cleanup,too-many-positional-arguments - -good-names=i,j,x,y,m,t,fd,db,cc,x1,x2,y1,y2,pt,k,v,nr - -[DESIGN] - -max-returns=7 diff --git a/src/nominatim_api/config.py b/src/nominatim_api/config.py index 18afda66..94f3bb5d 100644 --- a/src/nominatim_api/config.py +++ b/src/nominatim_api/config.py @@ -8,5 +8,5 @@ # This file is just a placeholder to make the config module available # during development. It will be replaced by nominatim_db/config.py on # installation. -# pylint: skip-file +# flake8: noqa from nominatim_db.config import * diff --git a/src/nominatim_api/connection.py b/src/nominatim_api/connection.py index 167ffaa4..e104745e 100644 --- a/src/nominatim_api/connection.py +++ b/src/nominatim_api/connection.py @@ -21,6 +21,7 @@ from .logging import log T = TypeVar('T') + class SearchConnection: """ An extended SQLAlchemy connection class, that also contains the table definitions. The underlying asynchronous SQLAlchemy @@ -32,37 +33,32 @@ class SearchConnection: tables: SearchTables, properties: Dict[str, Any]) -> None: self.connection = conn - self.t = tables # pylint: disable=invalid-name + self.t = tables self._property_cache = properties self._classtables: Optional[Set[str]] = None self.query_timeout: Optional[int] = None - def set_query_timeout(self, timeout: Optional[int]) -> None: """ Set the timeout after which a query over this connection is cancelled. """ self.query_timeout = timeout - async def scalar(self, sql: sa.sql.base.Executable, - params: Union[Mapping[str, Any], None] = None - ) -> Any: + params: Union[Mapping[str, Any], None] = None) -> Any: """ Execute a 'scalar()' query on the connection. """ log().sql(self.connection, sql, params) return await asyncio.wait_for(self.connection.scalar(sql, params), self.query_timeout) - async def execute(self, sql: 'sa.Executable', params: Union[Mapping[str, Any], Sequence[Mapping[str, Any]], None] = None - ) -> 'sa.Result[Any]': + ) -> 'sa.Result[Any]': """ Execute a 'execute()' query on the connection. """ log().sql(self.connection, sql, params) return await asyncio.wait_for(self.connection.execute(sql, params), self.query_timeout) - async def get_property(self, name: str, cached: bool = True) -> str: """ Get a property from Nominatim's property table. @@ -89,7 +85,6 @@ class SearchConnection: return cast(str, value) - async def get_db_property(self, name: str) -> Any: """ Get a setting from the database. At the moment, only 'server_version', the version of the database software, can @@ -102,7 +97,6 @@ class SearchConnection: return self._property_cache['DB:server_version'] - async def get_cached_value(self, group: str, name: str, factory: Callable[[], Awaitable[T]]) -> T: """ Access the cache for this Nominatim instance. @@ -125,7 +119,6 @@ class SearchConnection: return value - async def get_class_table(self, cls: str, typ: str) -> Optional[SaFromClause]: """ Lookup up if there is a classtype table for the given category and return a SQLAlchemy table for it, if it exists. diff --git a/src/nominatim_api/core.py b/src/nominatim_api/core.py index ff0db39f..3f4652bf 100644 --- a/src/nominatim_api/core.py +++ b/src/nominatim_api/core.py @@ -7,7 +7,7 @@ """ Implementation of classes for API access via libraries. """ -from typing import Mapping, Optional, Any, AsyncIterator, Dict, Sequence, List,\ +from typing import Mapping, Optional, Any, AsyncIterator, Dict, Sequence, List, \ Union, Tuple, cast import asyncio import sys @@ -21,7 +21,7 @@ from .errors import UsageError from .sql.sqlalchemy_schema import SearchTables from .sql.async_core_library import PGCORE_LIB, PGCORE_ERROR from .config import Configuration -from .sql import sqlite_functions, sqlalchemy_functions #pylint: disable=unused-import +from .sql import sqlite_functions, sqlalchemy_functions # noqa from .connection import SearchConnection from .status import get_status, StatusResult from .lookup import get_detailed_place, get_simple_place @@ -31,7 +31,7 @@ from . import types as ntyp from .results import DetailedResult, ReverseResult, SearchResults -class NominatimAPIAsync: #pylint: disable=too-many-instance-attributes +class NominatimAPIAsync: """ The main frontend to the Nominatim database implements the functions for lookup, forward and reverse geocoding using asynchronous functions. @@ -61,19 +61,18 @@ class NominatimAPIAsync: #pylint: disable=too-many-instance-attributes """ self.config = Configuration(project_dir, environ) self.query_timeout = self.config.get_int('QUERY_TIMEOUT') \ - if self.config.QUERY_TIMEOUT else None + if self.config.QUERY_TIMEOUT else None self.reverse_restrict_to_country_area = self.config.get_bool('SEARCH_WITHIN_COUNTRIES') self.server_version = 0 if sys.version_info >= (3, 10): self._engine_lock = asyncio.Lock() else: - self._engine_lock = asyncio.Lock(loop=loop) # pylint: disable=unexpected-keyword-arg + self._engine_lock = asyncio.Lock(loop=loop) self._engine: Optional[sa_asyncio.AsyncEngine] = None self._tables: Optional[SearchTables] = None self._property_cache: Dict[str, Any] = {'DB:server_version': 0} - async def setup_database(self) -> None: """ Set up the SQL engine and connections. @@ -95,7 +94,6 @@ class NominatimAPIAsync: #pylint: disable=too-many-instance-attributes extra_args['max_overflow'] = 0 extra_args['pool_size'] = self.config.get_int('API_POOL_SIZE') - is_sqlite = self.config.DATABASE_DSN.startswith('sqlite:') if is_sqlite: @@ -156,10 +154,9 @@ class NominatimAPIAsync: #pylint: disable=too-many-instance-attributes self._property_cache['DB:server_version'] = server_version - self._tables = SearchTables(sa.MetaData()) # pylint: disable=no-member + self._tables = SearchTables(sa.MetaData()) self._engine = engine - async def close(self) -> None: """ Close all active connections to the database. The NominatimAPIAsync object remains usable after closing. If a new API functions is @@ -168,15 +165,12 @@ class NominatimAPIAsync: #pylint: disable=too-many-instance-attributes if self._engine is not None: await self._engine.dispose() - async def __aenter__(self) -> 'NominatimAPIAsync': return self - async def __aexit__(self, *_: Any) -> None: await self.close() - @contextlib.asynccontextmanager async def begin(self) -> AsyncIterator[SearchConnection]: """ Create a new connection with automatic transaction handling. @@ -194,7 +188,6 @@ class NominatimAPIAsync: #pylint: disable=too-many-instance-attributes async with self._engine.begin() as conn: yield SearchConnection(conn, self._tables, self._property_cache) - async def status(self) -> StatusResult: """ Return the status of the database. """ @@ -207,7 +200,6 @@ class NominatimAPIAsync: #pylint: disable=too-many-instance-attributes return status - async def details(self, place: ntyp.PlaceRef, **params: Any) -> Optional[DetailedResult]: """ Get detailed information about a place in the database. @@ -220,7 +212,6 @@ class NominatimAPIAsync: #pylint: disable=too-many-instance-attributes await make_query_analyzer(conn) return await get_detailed_place(conn, place, details) - async def lookup(self, places: Sequence[ntyp.PlaceRef], **params: Any) -> SearchResults: """ Get simple information about a list of places. @@ -234,7 +225,6 @@ class NominatimAPIAsync: #pylint: disable=too-many-instance-attributes return SearchResults(filter(None, [await get_simple_place(conn, p, details) for p in places])) - async def reverse(self, coord: ntyp.AnyPoint, **params: Any) -> Optional[ReverseResult]: """ Find a place by its coordinates. Also known as reverse geocoding. @@ -255,7 +245,6 @@ class NominatimAPIAsync: #pylint: disable=too-many-instance-attributes self.reverse_restrict_to_country_area) return await geocoder.lookup(coord) - async def search(self, query: str, **params: Any) -> SearchResults: """ Find a place by free-text search. Also known as forward geocoding. """ @@ -266,13 +255,11 @@ class NominatimAPIAsync: #pylint: disable=too-many-instance-attributes async with self.begin() as conn: conn.set_query_timeout(self.query_timeout) geocoder = ForwardGeocoder(conn, ntyp.SearchDetails.from_kwargs(params), - self.config.get_int('REQUEST_TIMEOUT') \ - if self.config.REQUEST_TIMEOUT else None) + self.config.get_int('REQUEST_TIMEOUT') + if self.config.REQUEST_TIMEOUT else None) phrases = [Phrase(PhraseType.NONE, p.strip()) for p in query.split(',')] return await geocoder.lookup(phrases) - - # pylint: disable=too-many-arguments,too-many-branches async def search_address(self, amenity: Optional[str] = None, street: Optional[str] = None, city: Optional[str] = None, @@ -326,11 +313,10 @@ class NominatimAPIAsync: #pylint: disable=too-many-instance-attributes details.layers |= ntyp.DataLayer.POI geocoder = ForwardGeocoder(conn, details, - self.config.get_int('REQUEST_TIMEOUT') \ - if self.config.REQUEST_TIMEOUT else None) + self.config.get_int('REQUEST_TIMEOUT') + if self.config.REQUEST_TIMEOUT else None) return await geocoder.lookup(phrases) - async def search_category(self, categories: List[Tuple[str, str]], near_query: Optional[str] = None, **params: Any) -> SearchResults: @@ -352,12 +338,11 @@ class NominatimAPIAsync: #pylint: disable=too-many-instance-attributes await make_query_analyzer(conn) geocoder = ForwardGeocoder(conn, details, - self.config.get_int('REQUEST_TIMEOUT') \ - if self.config.REQUEST_TIMEOUT else None) + self.config.get_int('REQUEST_TIMEOUT') + if self.config.REQUEST_TIMEOUT else None) return await geocoder.lookup_pois(categories, phrases) - class NominatimAPI: """ This class provides a thin synchronous wrapper around the asynchronous Nominatim functions. It creates its own event loop and runs each @@ -382,7 +367,6 @@ class NominatimAPI: self._loop = asyncio.new_event_loop() self._async_api = NominatimAPIAsync(project_dir, environ, loop=self._loop) - def close(self) -> None: """ Close all active connections to the database. @@ -393,15 +377,12 @@ class NominatimAPI: self._loop.run_until_complete(self._async_api.close()) self._loop.close() - def __enter__(self) -> 'NominatimAPI': return self - def __exit__(self, *_: Any) -> None: self.close() - @property def config(self) -> Configuration: """ Provide read-only access to the [configuration](Configuration.md) @@ -427,7 +408,6 @@ class NominatimAPI: """ return self._loop.run_until_complete(self._async_api.status()) - def details(self, place: ntyp.PlaceRef, **params: Any) -> Optional[DetailedResult]: """ Get detailed information about a place in the database. @@ -510,7 +490,6 @@ class NominatimAPI: """ return self._loop.run_until_complete(self._async_api.details(place, **params)) - def lookup(self, places: Sequence[ntyp.PlaceRef], **params: Any) -> SearchResults: """ Get simple information about a list of places. @@ -587,7 +566,6 @@ class NominatimAPI: """ return self._loop.run_until_complete(self._async_api.lookup(places, **params)) - def reverse(self, coord: ntyp.AnyPoint, **params: Any) -> Optional[ReverseResult]: """ Find a place by its coordinates. Also known as reverse geocoding. @@ -669,7 +647,6 @@ class NominatimAPI: """ return self._loop.run_until_complete(self._async_api.reverse(coord, **params)) - def search(self, query: str, **params: Any) -> SearchResults: """ Find a place by free-text search. Also known as forward geocoding. @@ -769,8 +746,6 @@ class NominatimAPI: return self._loop.run_until_complete( self._async_api.search(query, **params)) - - # pylint: disable=too-many-arguments def search_address(self, amenity: Optional[str] = None, street: Optional[str] = None, city: Optional[str] = None, @@ -888,7 +863,6 @@ class NominatimAPI: self._async_api.search_address(amenity, street, city, county, state, country, postalcode, **params)) - def search_category(self, categories: List[Tuple[str, str]], near_query: Optional[str] = None, **params: Any) -> SearchResults: diff --git a/src/nominatim_api/errors.py b/src/nominatim_api/errors.py index c7331a89..98fe693d 100644 --- a/src/nominatim_api/errors.py +++ b/src/nominatim_api/errors.py @@ -8,6 +8,7 @@ Custom exception and error classes for Nominatim. """ + class UsageError(Exception): """ An error raised because of bad user input. This error will usually not cause a stack trace to be printed unless debugging is enabled. diff --git a/src/nominatim_api/localization.py b/src/nominatim_api/localization.py index 5964bbee..bbf9225b 100644 --- a/src/nominatim_api/localization.py +++ b/src/nominatim_api/localization.py @@ -11,6 +11,7 @@ from typing import Mapping, List, Optional import re + class Locales: """ Helper class for localization of names. @@ -28,24 +29,20 @@ class Locales: self._add_lang_tags('official_name', 'short_name') self._add_tags('official_name', 'short_name', 'ref') - def __bool__(self) -> bool: return len(self.languages) > 0 - def _add_tags(self, *tags: str) -> None: for tag in tags: self.name_tags.append(tag) self.name_tags.append(f"_place_{tag}") - def _add_lang_tags(self, *tags: str) -> None: for tag in tags: for lang in self.languages: self.name_tags.append(f"{tag}:{lang}") self.name_tags.append(f"_place_{tag}:{lang}") - def display_name(self, names: Optional[Mapping[str, str]]) -> str: """ Return the best matching name from a dictionary of names containing different name variants. @@ -64,7 +61,6 @@ class Locales: # Nothing? Return any of the other names as a default. return next(iter(names.values())) - @staticmethod def from_accept_languages(langstr: str) -> 'Locales': """ Create a localization object from a language list in the diff --git a/src/nominatim_api/logging.py b/src/nominatim_api/logging.py index 7df36ec1..1a6aef9b 100644 --- a/src/nominatim_api/logging.py +++ b/src/nominatim_api/logging.py @@ -49,41 +49,35 @@ class BaseLogger: """ Start a new debug chapter for the given function and its parameters. """ - def section(self, heading: str) -> None: """ Start a new section with the given title. """ - def comment(self, text: str) -> None: """ Add a simple comment to the debug output. """ - def var_dump(self, heading: str, var: Any) -> None: """ Print the content of the variable to the debug output prefixed by the given heading. """ - def table_dump(self, heading: str, rows: Iterator[Optional[List[Any]]]) -> None: """ Print the table generated by the generator function. """ - def result_dump(self, heading: str, results: Iterator[Tuple[Any, Any]]) -> None: """ Print a list of search results generated by the generator function. """ - def sql(self, conn: AsyncConnection, statement: 'sa.Executable', params: Union[Mapping[str, Any], Sequence[Mapping[str, Any]], None]) -> None: """ Print the SQL for the given statement. """ def format_sql(self, conn: AsyncConnection, statement: 'sa.Executable', - extra_params: Union[Mapping[str, Any], - Sequence[Mapping[str, Any]], None]) -> str: + extra_params: Union[Mapping[str, Any], Sequence[Mapping[str, Any]], None] + ) -> str: """ Return the compiled version of the statement. """ compiled = cast('sa.ClauseElement', statement).compile(conn.sync_engine) @@ -108,7 +102,7 @@ class BaseLogger: try: sqlstr = re.sub(r'__\[POSTCOMPILE_[^]]*\]', '%s', sqlstr) return sqlstr % tuple((repr(params.get(name, None)) - for name in compiled.positiontup)) # type: ignore + for name in compiled.positiontup)) # type: ignore except TypeError: return sqlstr @@ -121,28 +115,26 @@ class BaseLogger: assert conn.dialect.name == 'sqlite' # params in positional order - pparams = (repr(params.get(name, None)) for name in compiled.positiontup) # type: ignore + pparams = (repr(params.get(name, None)) for name in compiled.positiontup) # type: ignore sqlstr = re.sub(r'__\[POSTCOMPILE_([^]]*)\]', '?', sqlstr) sqlstr = re.sub(r"\?", lambda m: next(pparams), sqlstr) return sqlstr + class HTMLLogger(BaseLogger): """ Logger that formats messages in HTML. """ def __init__(self) -> None: self.buffer = io.StringIO() - def _timestamp(self) -> None: self._write(f'

[{dt.datetime.now()}]

') - def get_buffer(self) -> str: return HTML_HEADER + self.buffer.getvalue() + HTML_FOOTER - def function(self, func: str, **kwargs: Any) -> None: self._timestamp() self._write(f"

Debug output for {func}()

\n

Parameters:

") @@ -150,17 +142,14 @@ class HTMLLogger(BaseLogger): self._write(f'
{name}
{self._python_var(value)}
') self._write('

') - def section(self, heading: str) -> None: self._timestamp() self._write(f"

{heading}

") - def comment(self, text: str) -> None: self._timestamp() self._write(f"

{text}

") - def var_dump(self, heading: str, var: Any) -> None: self._timestamp() if callable(var): @@ -168,7 +157,6 @@ class HTMLLogger(BaseLogger): self._write(f'
{heading}
{self._python_var(var)}') - def table_dump(self, heading: str, rows: Iterator[Optional[List[Any]]]) -> None: self._timestamp() head = next(rows) @@ -185,11 +173,11 @@ class HTMLLogger(BaseLogger): self._write('') self._write('') - def result_dump(self, heading: str, results: Iterator[Tuple[Any, Any]]) -> None: """ Print a list of search results generated by the generator function. """ self._timestamp() + def format_osm(osm_object: Optional[Tuple[str, int]]) -> str: if not osm_object: return '-' @@ -218,7 +206,6 @@ class HTMLLogger(BaseLogger): total += 1 self._write(f'TOTAL: {total}

') - def sql(self, conn: AsyncConnection, statement: 'sa.Executable', params: Union[Mapping[str, Any], Sequence[Mapping[str, Any]], None]) -> None: self._timestamp() @@ -230,7 +217,6 @@ class HTMLLogger(BaseLogger): else: self._write(f'{html.escape(sqlstr)}') - def _python_var(self, var: Any) -> str: if CODE_HIGHLIGHT: fmt = highlight(str(var), PythonLexer(), HtmlFormatter(nowrap=True)) @@ -238,7 +224,6 @@ class HTMLLogger(BaseLogger): return f'{html.escape(str(var))}' - def _write(self, text: str) -> None: """ Add the raw text to the debug output. """ @@ -251,38 +236,31 @@ class TextLogger(BaseLogger): def __init__(self) -> None: self.buffer = io.StringIO() - def _timestamp(self) -> None: self._write(f'[{dt.datetime.now()}]\n') - def get_buffer(self) -> str: return self.buffer.getvalue() - def function(self, func: str, **kwargs: Any) -> None: self._write(f"#### Debug output for {func}()\n\nParameters:\n") for name, value in kwargs.items(): self._write(f' {name}: {self._python_var(value)}\n') self._write('\n') - def section(self, heading: str) -> None: self._timestamp() self._write(f"\n# {heading}\n\n") - def comment(self, text: str) -> None: self._write(f"{text}\n") - def var_dump(self, heading: str, var: Any) -> None: if callable(var): var = var() self._write(f'{heading}:\n {self._python_var(var)}\n\n') - def table_dump(self, heading: str, rows: Iterator[Optional[List[Any]]]) -> None: self._write(f'{heading}:\n') data = [list(map(self._python_var, row)) if row else None for row in rows] @@ -291,7 +269,7 @@ class TextLogger(BaseLogger): maxlens = [max(len(d[i]) for d in data if d) for i in range(num_cols)] tablewidth = sum(maxlens) + 3 * num_cols + 1 - row_format = '| ' +' | '.join(f'{{:<{l}}}' for l in maxlens) + ' |\n' + row_format = '| ' + ' | '.join(f'{{:<{ln}}}' for ln in maxlens) + ' |\n' self._write('-'*tablewidth + '\n') self._write(row_format.format(*data[0])) self._write('-'*tablewidth + '\n') @@ -303,7 +281,6 @@ class TextLogger(BaseLogger): if data[-1]: self._write('-'*tablewidth + '\n') - def result_dump(self, heading: str, results: Iterator[Tuple[Any, Any]]) -> None: self._timestamp() self._write(f'{heading}:\n') @@ -318,18 +295,15 @@ class TextLogger(BaseLogger): total += 1 self._write(f'TOTAL: {total}\n\n') - def sql(self, conn: AsyncConnection, statement: 'sa.Executable', params: Union[Mapping[str, Any], Sequence[Mapping[str, Any]], None]) -> None: self._timestamp() sqlstr = '\n| '.join(textwrap.wrap(self.format_sql(conn, statement, params), width=78)) self._write(f"| {sqlstr}\n\n") - def _python_var(self, var: Any) -> str: return str(var) - def _write(self, text: str) -> None: self.buffer.write(text) @@ -368,8 +342,8 @@ HTML_HEADER: str = """ Nominatim - Debug