From 18476614721db0769412b78716d49cd7cb276cb0 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 7 Feb 2023 15:58:00 +0200 Subject: [PATCH 1/2] Replace babel.dates._pattern_cache with an LRU cache Fixes #962 --- babel/dates.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/babel/dates.py b/babel/dates.py index ce439eecd..78c7facaf 100644 --- a/babel/dates.py +++ b/babel/dates.py @@ -19,6 +19,7 @@ import re import warnings +from functools import lru_cache from typing import TYPE_CHECKING, SupportsInt try: @@ -1667,10 +1668,8 @@ def get_week_number(self, day_of_period: int, day_of_week: int | None = None) -> #: in order of decreasing magnitude. PATTERN_CHAR_ORDER = "GyYuUQqMLlwWdDFgEecabBChHKkjJmsSAzZOvVXx" -_pattern_cache = {} - -def parse_pattern(pattern: str) -> DateTimePattern: +def parse_pattern(pattern: str | DateTimePattern) -> DateTimePattern: """Parse date, time, and datetime format patterns. >>> parse_pattern("MMMMd").format @@ -1693,10 +1692,11 @@ def parse_pattern(pattern: str) -> DateTimePattern: """ if isinstance(pattern, DateTimePattern): return pattern + return _cached_parse_pattern(pattern) - if pattern in _pattern_cache: - return _pattern_cache[pattern] +@lru_cache(maxsize=1024) +def _cached_parse_pattern(pattern: str) -> DateTimePattern: result = [] for tok_type, tok_value in tokenize_pattern(pattern): @@ -1710,9 +1710,7 @@ def parse_pattern(pattern: str) -> DateTimePattern: result.append('%%(%s)s' % (fieldchar * fieldnum)) else: raise NotImplementedError(f"Unknown token type: {tok_type}") - - _pattern_cache[pattern] = pat = DateTimePattern(pattern, ''.join(result)) - return pat + return DateTimePattern(pattern, ''.join(result)) def tokenize_pattern(pattern: str) -> list[tuple[str, str | tuple[str, int]]]: From 152a9e38339254afe12160eac16e945e05131a16 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 7 Feb 2023 16:00:41 +0200 Subject: [PATCH 2/2] Replace babel.localedata.locale_identifiers cache with LRU cache --- babel/localedata.py | 23 ++++++++++------------- tests/test_localedata.py | 13 +++++-------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/babel/localedata.py b/babel/localedata.py index f765a1ea3..a9c7c75ec 100644 --- a/babel/localedata.py +++ b/babel/localedata.py @@ -20,6 +20,7 @@ import threading from collections import abc from collections.abc import Iterator, Mapping, MutableMapping +from functools import lru_cache from itertools import chain from typing import Any @@ -74,28 +75,24 @@ def exists(name: str) -> bool: return True if file_found else bool(normalize_locale(name)) +@lru_cache(maxsize=None) def locale_identifiers() -> list[str]: """Return a list of all locale identifiers for which locale data is available. - This data is cached after the first invocation in `locale_identifiers.cache`. - - Removing the `locale_identifiers.cache` attribute or setting it to `None` - will cause this function to re-read the list from disk. + This data is cached after the first invocation. + You can clear the cache by calling `locale_identifiers.cache_clear()`. .. versionadded:: 0.8.1 :return: a list of locale identifiers (strings) """ - data = getattr(locale_identifiers, 'cache', None) - if data is None: - locale_identifiers.cache = data = [ - stem - for stem, extension in - (os.path.splitext(filename) for filename in os.listdir(_dirname)) - if extension == '.dat' and stem != 'root' - ] - return data + return [ + stem + for stem, extension in + (os.path.splitext(filename) for filename in os.listdir(_dirname)) + if extension == '.dat' and stem != 'root' + ] def load(name: os.PathLike[str] | str, merge_inherited: bool = True) -> dict[str, Any]: diff --git a/tests/test_localedata.py b/tests/test_localedata.py index 913922ee4..75c34b19b 100644 --- a/tests/test_localedata.py +++ b/tests/test_localedata.py @@ -110,18 +110,15 @@ def listdir_spy(*args): rv = original_listdir(*args) listdir_calls.append((args, rv)) return rv - monkeypatch.setattr(localedata.os, 'listdir', listdir_spy) - - # In case we've already run some tests... - if hasattr(localedata.locale_identifiers, 'cache'): - del localedata.locale_identifiers.cache + monkeypatch.setattr(localedata.os, 'listdir', listdir_spy) + localedata.locale_identifiers.cache_clear() assert not listdir_calls - assert localedata.locale_identifiers() + l = localedata.locale_identifiers() assert len(listdir_calls) == 1 - assert localedata.locale_identifiers() is localedata.locale_identifiers.cache + assert localedata.locale_identifiers() is l assert len(listdir_calls) == 1 - localedata.locale_identifiers.cache = None + localedata.locale_identifiers.cache_clear() assert localedata.locale_identifiers() assert len(listdir_calls) == 2