From: Sarah Hoffmann Date: Wed, 13 Nov 2024 18:35:54 +0000 (+0100) Subject: Merge remote-tracking branch 'upstream/master' X-Git-Url: https://git.openstreetmap.org/nominatim.git/commitdiff_plain/e1dc4379e0cd100200ac53752442143ca4846fc5?hp=aee051740e2159ec2eea1af4f5844748c38a1905 Merge remote-tracking branch 'upstream/master' --- diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..82d77ed3 --- /dev/null +++ b/.flake8 @@ -0,0 +1,8 @@ +[flake8] +max-line-length = 100 +max-doc-length = 100 +extend-ignore = + # something == None constructs are needed for SQLAlchemy + E711 +per-file-ignores = + __init__.py: F401 diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 1b81f4f2..fb664d99 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -103,12 +103,12 @@ jobs: - name: Install Python webservers run: pip3 install falcon starlette asgi_lifespan - - name: Install latest pylint - run: pip3 install -U pylint + - name: Install latest flake8 + run: pip3 install -U flake8 if: matrix.flavour == 'ubuntu-22' - name: Python linting - run: python3 -m pylint src + run: python3 -m flake8 src working-directory: Nominatim if: matrix.flavour == 'ubuntu-22' 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/CONTRIBUTING.md b/CONTRIBUTING.md index a78bbfb3..78c4dd67 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,6 +30,19 @@ feature pull requests. If you plan to make larger changes, please open an issue first or comment on the appropriate issue already existing so that duplicate work can be avoided. +### Using AI-assisted code generators + +PRs that include AI-generated content, may that be in code, in the PR +description or in documentation need to + +1. clearly mark the AI-generated sections as such, for example, by + mentioning all use of AI in the PR description, and +2. include proof that you have run the generated code on an actual + installation of Nominatim. Adding and excuting tests will not be + sufficient. You need to show that the code actually solves the problem + the PR claims to solve. + + ## Coding style Nominatim historically hasn't followed a particular coding style but we @@ -46,14 +59,11 @@ are in process of consolidating the style. The following rules apply: * no spaces after opening and before closing bracket * leave out space between a function name and bracket but add one between control statement(if, while, etc.) and bracket - * for PHP variables use CamelCase with a prefixing letter indicating the type - (i - integer, f - float, a - array, s - string, o - object) -The coding style is enforced with PHPCS and pylint. It can be tested with: +The coding style is enforced with flake8. It can be tested with: ``` -phpcs --report-width=120 --colors . -pylint3 --extension-pkg-whitelist=osmium nominatim +make lint ``` ## Testing diff --git a/Makefile b/Makefile index 421b671a..9e914850 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ pytest: pytest test/python lint: - pylint src + flake8 src bdd: cd test/bdd; behave -DREMOVE_TEMPLATE=1 diff --git a/docs/develop/Development-Environment.md b/docs/develop/Development-Environment.md index 441556ff..a6558c7d 100644 --- a/docs/develop/Development-Environment.md +++ b/docs/develop/Development-Environment.md @@ -29,7 +29,7 @@ The Nominatim test suite consists of behavioural tests (using behave) and unit tests (using pytest). It has the following additional requirements: * [behave test framework](https://behave.readthedocs.io) >= 1.2.6 -* [Pylint](https://pylint.org/) (CI always runs the latest version from pip) +* [flake8](https://flake8.pycqa.org/en/stable/) (CI always runs the latest version from pip) * [mypy](http://mypy-lang.org/) (plus typing information for external libs) * [Python Typing Extensions](https://github.com/python/typing_extensions) (for Python < 3.9) * [pytest](https://pytest.org) @@ -55,7 +55,6 @@ the vendored version of osm2pgsql, you need to set the PATH accordingly. ### Installing prerequisites on Ubuntu/Debian The Python tools should always be run with the most recent version. -In particular, pylint tends to have a lot of breaking changes between versions. The easiest way, to handle these Python dependencies is to run your development from within a virtual environment. @@ -70,7 +69,7 @@ virtualenv ~/nominatim-dev-venv ~/nominatim-dev-venv/bin/pip install\ psutil psycopg[binary] PyICU SQLAlchemy \ python-dotenv jinja2 pyYAML datrie behave \ - mkdocs mkdocstrings mkdocs-gen-files pytest pytest-asyncio pylint \ + mkdocs mkdocstrings mkdocs-gen-files pytest pytest-asyncio flake8 \ types-jinja2 types-markupsafe types-psutil types-psycopg2 \ types-pygments types-pyyaml types-requests types-ujson \ types-urllib3 typing-extensions unicorn falcon starlette \ diff --git a/src/nominatim_api/__init__.py b/src/nominatim_api/__init__.py index 242ff892..89c57b65 100644 --- a/src/nominatim_api/__init__.py +++ b/src/nominatim_api/__init__.py @@ -11,9 +11,6 @@ Classes and functions defined in this file are considered stable. Always import from this file, not from the source files directly. """ -# See also https://github.com/PyCQA/pylint/issues/6006 -# pylint: disable=useless-import-alias - from .errors import (UsageError as UsageError) from .config import (Configuration as Configuration) 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..c45b24de 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,17 +21,17 @@ 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 +from .lookup import get_places, get_detailed_place from .reverse import ReverseGeocoder from .search import ForwardGeocoder, Phrase, PhraseType, make_query_analyzer 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. @@ -231,9 +222,7 @@ class NominatimAPIAsync: #pylint: disable=too-many-instance-attributes conn.set_query_timeout(self.query_timeout) if details.keywords: await make_query_analyzer(conn) - return SearchResults(filter(None, - [await get_simple_place(conn, p, details) for p in places])) - + return await get_places(conn, places, details) async def reverse(self, coord: ntyp.AnyPoint, **params: Any) -> Optional[ReverseResult]: """ Find a place by its coordinates. Also known as reverse geocoding. @@ -255,7 +244,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 +254,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 +312,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 +337,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 +366,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 +376,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 +407,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 +489,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 +565,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 +646,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 +745,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 +862,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