Add typing
Change-Id: I2a31ff8a9c034779b813f531ccf7b2cb166d5e07 Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
"""Translation function factory"""
|
"""Translation function factory"""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
import gettext
|
import gettext
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@@ -34,7 +35,10 @@ CONTEXT_SEPARATOR = _message.CONTEXT_SEPARATOR
|
|||||||
class TranslatorFactory:
|
class TranslatorFactory:
|
||||||
"Create translator functions"
|
"Create translator functions"
|
||||||
|
|
||||||
def __init__(self, domain, localedir=None):
|
domain: str
|
||||||
|
localedir: str | None
|
||||||
|
|
||||||
|
def __init__(self, domain: str, localedir: str | None = None) -> None:
|
||||||
"""Establish a set of translation functions for the domain.
|
"""Establish a set of translation functions for the domain.
|
||||||
|
|
||||||
:param domain: Name of translation domain,
|
:param domain: Name of translation domain,
|
||||||
@@ -49,7 +53,9 @@ class TranslatorFactory:
|
|||||||
localedir = os.environ.get(variable_name)
|
localedir = os.environ.get(variable_name)
|
||||||
self.localedir = localedir
|
self.localedir = localedir
|
||||||
|
|
||||||
def _make_translation_func(self, domain=None):
|
def _make_translation_func(
|
||||||
|
self, domain: str | None = None
|
||||||
|
) -> Callable[[str], str | _message.Message]:
|
||||||
"""Return a translation function ready for use with messages.
|
"""Return a translation function ready for use with messages.
|
||||||
|
|
||||||
The returned function takes a single value, the unicode string
|
The returned function takes a single value, the unicode string
|
||||||
@@ -74,7 +80,7 @@ class TranslatorFactory:
|
|||||||
# on the python version.
|
# on the python version.
|
||||||
m = t.gettext
|
m = t.gettext
|
||||||
|
|
||||||
def f(msg):
|
def f(msg: str) -> str | _message.Message:
|
||||||
"""oslo_i18n.gettextutils translation function."""
|
"""oslo_i18n.gettextutils translation function."""
|
||||||
if _lazy.USE_LAZY:
|
if _lazy.USE_LAZY:
|
||||||
return _message.Message(msg, domain=domain)
|
return _message.Message(msg, domain=domain)
|
||||||
@@ -82,7 +88,9 @@ class TranslatorFactory:
|
|||||||
|
|
||||||
return f
|
return f
|
||||||
|
|
||||||
def _make_contextual_translation_func(self, domain=None):
|
def _make_contextual_translation_func(
|
||||||
|
self, domain: str | None = None
|
||||||
|
) -> Callable[[str, str], str | _message.Message]:
|
||||||
"""Return a translation function ready for use with context messages.
|
"""Return a translation function ready for use with context messages.
|
||||||
|
|
||||||
The returned function takes two values, the context of
|
The returned function takes two values, the context of
|
||||||
@@ -103,7 +111,7 @@ class TranslatorFactory:
|
|||||||
# on the python version.
|
# on the python version.
|
||||||
m = t.gettext
|
m = t.gettext
|
||||||
|
|
||||||
def f(ctx, msg):
|
def f(ctx: str, msg: str) -> str | _message.Message:
|
||||||
"""oslo.i18n.gettextutils translation with context function."""
|
"""oslo.i18n.gettextutils translation with context function."""
|
||||||
if _lazy.USE_LAZY:
|
if _lazy.USE_LAZY:
|
||||||
msgid = (ctx, msg)
|
msgid = (ctx, msg)
|
||||||
@@ -120,7 +128,9 @@ class TranslatorFactory:
|
|||||||
|
|
||||||
return f
|
return f
|
||||||
|
|
||||||
def _make_plural_translation_func(self, domain=None):
|
def _make_plural_translation_func(
|
||||||
|
self, domain: str | None = None
|
||||||
|
) -> Callable[[str, str, int], str | _message.Message]:
|
||||||
"""Return a plural translation function ready for use with messages.
|
"""Return a plural translation function ready for use with messages.
|
||||||
|
|
||||||
The returned function takes three values, the single form of
|
The returned function takes three values, the single form of
|
||||||
@@ -142,7 +152,9 @@ class TranslatorFactory:
|
|||||||
# on the python version.
|
# on the python version.
|
||||||
m = t.ngettext
|
m = t.ngettext
|
||||||
|
|
||||||
def f(msgsingle, msgplural, msgcount):
|
def f(
|
||||||
|
msgsingle: str, msgplural: str, msgcount: int
|
||||||
|
) -> str | _message.Message:
|
||||||
"""oslo.i18n.gettextutils plural translation function."""
|
"""oslo.i18n.gettextutils plural translation function."""
|
||||||
if _lazy.USE_LAZY:
|
if _lazy.USE_LAZY:
|
||||||
msgid = (msgsingle, msgplural, msgcount)
|
msgid = (msgsingle, msgplural, msgcount)
|
||||||
@@ -154,12 +166,12 @@ class TranslatorFactory:
|
|||||||
return f
|
return f
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def primary(self):
|
def primary(self) -> Callable[[str], str | _message.Message]:
|
||||||
"The default translation function."
|
"The default translation function."
|
||||||
return self._make_translation_func()
|
return self._make_translation_func()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def contextual_form(self):
|
def contextual_form(self) -> Callable[[str, str], str | _message.Message]:
|
||||||
"""The contextual translation function.
|
"""The contextual translation function.
|
||||||
|
|
||||||
The returned function takes two values, the context of
|
The returned function takes two values, the context of
|
||||||
@@ -171,7 +183,7 @@ class TranslatorFactory:
|
|||||||
return self._make_contextual_translation_func()
|
return self._make_contextual_translation_func()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def plural_form(self):
|
def plural_form(self) -> Callable[[str, str, int], str | _message.Message]:
|
||||||
"""The plural translation function.
|
"""The plural translation function.
|
||||||
|
|
||||||
The returned function takes three values, the single form of
|
The returned function takes three values, the single form of
|
||||||
@@ -183,25 +195,27 @@ class TranslatorFactory:
|
|||||||
"""
|
"""
|
||||||
return self._make_plural_translation_func()
|
return self._make_plural_translation_func()
|
||||||
|
|
||||||
def _make_log_translation_func(self, level):
|
def _make_log_translation_func(
|
||||||
|
self, level: str
|
||||||
|
) -> Callable[[str], str | _message.Message]:
|
||||||
return self._make_translation_func(self.domain + '-log-' + level)
|
return self._make_translation_func(self.domain + '-log-' + level)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def log_info(self):
|
def log_info(self) -> Callable[[str], str | _message.Message]:
|
||||||
"Translate info-level log messages."
|
"Translate info-level log messages."
|
||||||
return self._make_log_translation_func('info')
|
return self._make_log_translation_func('info')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def log_warning(self):
|
def log_warning(self) -> Callable[[str], str | _message.Message]:
|
||||||
"Translate warning-level log messages."
|
"Translate warning-level log messages."
|
||||||
return self._make_log_translation_func('warning')
|
return self._make_log_translation_func('warning')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def log_error(self):
|
def log_error(self) -> Callable[[str], str | _message.Message]:
|
||||||
"Translate error-level log messages."
|
"Translate error-level log messages."
|
||||||
return self._make_log_translation_func('error')
|
return self._make_log_translation_func('error')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def log_critical(self):
|
def log_critical(self) -> Callable[[str], str | _message.Message]:
|
||||||
"Translate critical-level log messages."
|
"Translate critical-level log messages."
|
||||||
return self._make_log_translation_func('critical')
|
return self._make_log_translation_func('critical')
|
||||||
|
@@ -20,6 +20,8 @@ import copy
|
|||||||
import gettext
|
import gettext
|
||||||
import locale
|
import locale
|
||||||
import os
|
import os
|
||||||
|
from typing import Any, Literal, overload
|
||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
from oslo_i18n import _factory
|
from oslo_i18n import _factory
|
||||||
from oslo_i18n import _locale
|
from oslo_i18n import _locale
|
||||||
@@ -30,7 +32,7 @@ __all__ = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def install(domain):
|
def install(domain: str) -> None:
|
||||||
"""Install a _() function using the given translation domain.
|
"""Install a _() function using the given translation domain.
|
||||||
|
|
||||||
Given a translation domain, install a _() function using gettext's
|
Given a translation domain, install a _() function using gettext's
|
||||||
@@ -49,7 +51,7 @@ def install(domain):
|
|||||||
builtins.__dict__['_'] = tf.primary
|
builtins.__dict__['_'] = tf.primary
|
||||||
|
|
||||||
|
|
||||||
_AVAILABLE_LANGUAGES = {}
|
_AVAILABLE_LANGUAGES: dict[str, list[str]] = {}
|
||||||
# Copied from Babel so anyone using aliases that were previously provided by
|
# Copied from Babel so anyone using aliases that were previously provided by
|
||||||
# the Babel implementation of get_available_languages continues to work. These
|
# the Babel implementation of get_available_languages continues to work. These
|
||||||
# are not recommended for use in new code.
|
# are not recommended for use in new code.
|
||||||
@@ -96,7 +98,7 @@ _BABEL_ALIASES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_available_languages(domain):
|
def get_available_languages(domain: str) -> list[str]:
|
||||||
"""Lists the available languages for the given translation domain.
|
"""Lists the available languages for the given translation domain.
|
||||||
|
|
||||||
:param domain: the domain to get languages for
|
:param domain: the domain to get languages for
|
||||||
@@ -106,7 +108,7 @@ def get_available_languages(domain):
|
|||||||
|
|
||||||
localedir = os.environ.get(_locale.get_locale_dir_variable_name(domain))
|
localedir = os.environ.get(_locale.get_locale_dir_variable_name(domain))
|
||||||
|
|
||||||
def find(x):
|
def find(x: str) -> str | None:
|
||||||
return gettext.find(domain, localedir=localedir, languages=[x])
|
return gettext.find(domain, localedir=localedir, languages=[x])
|
||||||
|
|
||||||
# NOTE(mrodden): en_US should always be available (and first in case
|
# NOTE(mrodden): en_US should always be available (and first in case
|
||||||
@@ -125,10 +127,54 @@ def get_available_languages(domain):
|
|||||||
|
|
||||||
|
|
||||||
_original_find = gettext.find
|
_original_find = gettext.find
|
||||||
_FIND_CACHE = {}
|
_FIND_CACHE: dict[
|
||||||
|
tuple[str, str | None, tuple[str, ...] | None, bool | int], Any
|
||||||
|
] = {}
|
||||||
|
|
||||||
|
|
||||||
def cached_find(domain, localedir=None, languages=None, all=0):
|
@overload
|
||||||
|
def cached_find(
|
||||||
|
domain: str,
|
||||||
|
localedir: str | None = None,
|
||||||
|
languages: Iterable[str] | None = None,
|
||||||
|
all: Literal[False] = False,
|
||||||
|
) -> str | None: ...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def cached_find(
|
||||||
|
domain: str,
|
||||||
|
localedir: str | None = None,
|
||||||
|
languages: Iterable[str] | None = None,
|
||||||
|
*,
|
||||||
|
all: Literal[True],
|
||||||
|
) -> list[str]: ...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def cached_find(
|
||||||
|
domain: str,
|
||||||
|
localedir: str | None,
|
||||||
|
languages: Iterable[str] | None,
|
||||||
|
all: Literal[True],
|
||||||
|
) -> list[str]: ...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def cached_find(
|
||||||
|
domain: str,
|
||||||
|
localedir: str | None = None,
|
||||||
|
languages: Iterable[str] | None = None,
|
||||||
|
all: bool = False,
|
||||||
|
) -> Any: ...
|
||||||
|
|
||||||
|
|
||||||
|
def cached_find(
|
||||||
|
domain: str,
|
||||||
|
localedir: str | None = None,
|
||||||
|
languages: Iterable[str] | None = None,
|
||||||
|
all: bool = False,
|
||||||
|
) -> Any:
|
||||||
"""A version of gettext.find using a cache.
|
"""A version of gettext.find using a cache.
|
||||||
|
|
||||||
gettext.find looks for mo files on the disk using os.path.exists. Those
|
gettext.find looks for mo files on the disk using os.path.exists. Those
|
||||||
@@ -149,4 +195,4 @@ def cached_find(domain, localedir=None, languages=None, all=0):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
gettext.find = cached_find
|
gettext.find = cached_find # type: ignore[assignment]
|
||||||
|
@@ -21,7 +21,7 @@ __all__ = [
|
|||||||
USE_LAZY = False
|
USE_LAZY = False
|
||||||
|
|
||||||
|
|
||||||
def enable_lazy(enable=True):
|
def enable_lazy(enable: bool = True) -> None:
|
||||||
"""Convenience function for configuring _() to use lazy gettext
|
"""Convenience function for configuring _() to use lazy gettext
|
||||||
|
|
||||||
Call this at the start of execution to enable the gettextutils._
|
Call this at the start of execution to enable the gettextutils._
|
||||||
|
@@ -15,7 +15,7 @@
|
|||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
|
||||||
def get_locale_dir_variable_name(domain):
|
def get_locale_dir_variable_name(domain: str) -> str:
|
||||||
"""Build environment variable name for local dir.
|
"""Build environment variable name for local dir.
|
||||||
|
|
||||||
Convert a translation domain name to a variable for specifying
|
Convert a translation domain name to a variable for specifying
|
||||||
|
@@ -20,6 +20,7 @@ import gettext
|
|||||||
import locale
|
import locale
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from typing import Any
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from oslo_i18n import _locale
|
from oslo_i18n import _locale
|
||||||
@@ -40,16 +41,23 @@ class Message(str):
|
|||||||
and can be treated as such.
|
and can be treated as such.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Declare attributes that are set in __new__
|
||||||
|
msgid: str | tuple[str, str] | tuple[str, str, int]
|
||||||
|
domain: str
|
||||||
|
params: Any
|
||||||
|
has_contextual_form: bool
|
||||||
|
has_plural_form: bool
|
||||||
|
|
||||||
def __new__(
|
def __new__(
|
||||||
cls,
|
cls,
|
||||||
msgid,
|
msgid: str | tuple[str, str] | tuple[str, str, int],
|
||||||
msgtext=None,
|
msgtext: str | None = None,
|
||||||
params=None,
|
params: Any = None,
|
||||||
domain='oslo',
|
domain: str = 'oslo',
|
||||||
has_contextual_form=False,
|
has_contextual_form: bool = False,
|
||||||
has_plural_form=False,
|
has_plural_form: bool = False,
|
||||||
*args,
|
*args: Any,
|
||||||
):
|
) -> 'Message':
|
||||||
"""Create a new Message object.
|
"""Create a new Message object.
|
||||||
|
|
||||||
In order for translation to work gettext requires a message ID, this
|
In order for translation to work gettext requires a message ID, this
|
||||||
@@ -72,7 +80,7 @@ class Message(str):
|
|||||||
msg.has_plural_form = has_plural_form
|
msg.has_plural_form = has_plural_form
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
def translation(self, desired_locale=None):
|
def translation(self, desired_locale: str | None = None) -> str:
|
||||||
"""Translate this message to the desired locale.
|
"""Translate this message to the desired locale.
|
||||||
|
|
||||||
:param desired_locale: The desired locale to translate the message to,
|
:param desired_locale: The desired locale to translate the message to,
|
||||||
@@ -105,12 +113,12 @@ class Message(str):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _translate_msgid(
|
def _translate_msgid(
|
||||||
msgid,
|
msgid: str | tuple[str, str] | tuple[str, str, int],
|
||||||
domain,
|
domain: str,
|
||||||
desired_locale=None,
|
desired_locale: str | None = None,
|
||||||
has_contextual_form=False,
|
has_contextual_form: bool = False,
|
||||||
has_plural_form=False,
|
has_plural_form: bool = False,
|
||||||
):
|
) -> str:
|
||||||
if not desired_locale:
|
if not desired_locale:
|
||||||
system_locale = locale.getlocale(locale.LC_CTYPE)
|
system_locale = locale.getlocale(locale.LC_CTYPE)
|
||||||
# If the system locale is not available to the runtime use English
|
# If the system locale is not available to the runtime use English
|
||||||
@@ -132,6 +140,9 @@ class Message(str):
|
|||||||
if not has_contextual_form and not has_plural_form:
|
if not has_contextual_form and not has_plural_form:
|
||||||
# This is the most common case, so check it first.
|
# This is the most common case, so check it first.
|
||||||
translator = lang.gettext
|
translator = lang.gettext
|
||||||
|
# narrow type
|
||||||
|
if not isinstance(msgid, str):
|
||||||
|
raise ValueError("Simple msgid must be a string")
|
||||||
translated_message = translator(msgid)
|
translated_message = translator(msgid)
|
||||||
|
|
||||||
elif has_contextual_form and has_plural_form:
|
elif has_contextual_form and has_plural_form:
|
||||||
@@ -140,7 +151,13 @@ class Message(str):
|
|||||||
raise ValueError("Unimplemented.")
|
raise ValueError("Unimplemented.")
|
||||||
|
|
||||||
elif has_contextual_form:
|
elif has_contextual_form:
|
||||||
(msgctx, msgtxt) = msgid
|
# narrow type
|
||||||
|
if not isinstance(msgid, tuple) or len(msgid) != 2:
|
||||||
|
raise ValueError(
|
||||||
|
"contextual msgid must be a tuple of (context, text)"
|
||||||
|
)
|
||||||
|
|
||||||
|
msgctx, msgtxt = msgid
|
||||||
translator = lang.gettext
|
translator = lang.gettext
|
||||||
|
|
||||||
msg_with_ctx = f"{msgctx}{CONTEXT_SEPARATOR}{msgtxt}"
|
msg_with_ctx = f"{msgctx}{CONTEXT_SEPARATOR}{msgtxt}"
|
||||||
@@ -151,19 +168,24 @@ class Message(str):
|
|||||||
translated_message = msgtxt
|
translated_message = msgtxt
|
||||||
|
|
||||||
elif has_plural_form:
|
elif has_plural_form:
|
||||||
(msgsingle, msgplural, msgcount) = msgid
|
# narrow type
|
||||||
translator = lang.ngettext
|
if not isinstance(msgid, tuple) or len(msgid) != 3:
|
||||||
translated_message = translator(msgsingle, msgplural, msgcount)
|
raise ValueError(
|
||||||
|
"plural msgid must be a tuple of (singular, plural, count)"
|
||||||
|
)
|
||||||
|
|
||||||
|
msgsingle, msgplural, msgcount = msgid
|
||||||
|
translated_message = lang.ngettext(msgsingle, msgplural, msgcount)
|
||||||
|
|
||||||
return translated_message
|
return translated_message
|
||||||
|
|
||||||
def _safe_translate(self, translated_message, translated_params):
|
def _safe_translate(
|
||||||
|
self, translated_message: str, translated_params: Any
|
||||||
|
) -> str:
|
||||||
"""Trap translation errors and fall back to default translation.
|
"""Trap translation errors and fall back to default translation.
|
||||||
|
|
||||||
:param translated_message: the requested translation
|
:param translated_message: the requested translation
|
||||||
|
|
||||||
:param translated_params: the params to be inserted
|
:param translated_params: the params to be inserted
|
||||||
|
|
||||||
:return: if parameter insertion is successful then it is the
|
:return: if parameter insertion is successful then it is the
|
||||||
translated_message with the translated_params inserted, if the
|
translated_message with the translated_params inserted, if the
|
||||||
requested translation fails then it is the default translation
|
requested translation fails then it is the default translation
|
||||||
@@ -195,7 +217,7 @@ class Message(str):
|
|||||||
|
|
||||||
return translated_message
|
return translated_message
|
||||||
|
|
||||||
def __mod__(self, other):
|
def __mod__(self, other: Any) -> 'Message':
|
||||||
# When we mod a Message we want the actual operation to be performed
|
# When we mod a Message we want the actual operation to be performed
|
||||||
# by the base class (i.e. unicode()), the only thing we do here is
|
# by the base class (i.e. unicode()), the only thing we do here is
|
||||||
# save the original msgid and the parameters in case of a translation
|
# save the original msgid and the parameters in case of a translation
|
||||||
@@ -206,7 +228,7 @@ class Message(str):
|
|||||||
)
|
)
|
||||||
return modded
|
return modded
|
||||||
|
|
||||||
def _sanitize_mod_params(self, other):
|
def _sanitize_mod_params(self, other: Any) -> Any:
|
||||||
"""Sanitize the object being modded with this Message.
|
"""Sanitize the object being modded with this Message.
|
||||||
|
|
||||||
- Add support for modding 'None' so translation supports it
|
- Add support for modding 'None' so translation supports it
|
||||||
@@ -216,7 +238,7 @@ class Message(str):
|
|||||||
translated, it will be used as it was when the Message was created
|
translated, it will be used as it was when the Message was created
|
||||||
"""
|
"""
|
||||||
if other is None:
|
if other is None:
|
||||||
params = (other,)
|
params: Any = (other,)
|
||||||
elif isinstance(other, dict):
|
elif isinstance(other, dict):
|
||||||
# Merge the dictionaries
|
# Merge the dictionaries
|
||||||
# Copy each item in case one does not support deep copy.
|
# Copy each item in case one does not support deep copy.
|
||||||
@@ -233,7 +255,7 @@ class Message(str):
|
|||||||
params = self._copy_param(other)
|
params = self._copy_param(other)
|
||||||
return params
|
return params
|
||||||
|
|
||||||
def _copy_param(self, param):
|
def _copy_param(self, param: Any) -> Any:
|
||||||
try:
|
try:
|
||||||
return copy.deepcopy(param)
|
return copy.deepcopy(param)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -241,11 +263,11 @@ class Message(str):
|
|||||||
# python code-like objects that can't be deep-copied
|
# python code-like objects that can't be deep-copied
|
||||||
return str(param)
|
return str(param)
|
||||||
|
|
||||||
def __add__(self, other):
|
def __add__(self, other: Any) -> str:
|
||||||
from oslo_i18n._i18n import _
|
from oslo_i18n._i18n import _
|
||||||
|
|
||||||
msg = _('Message objects do not support addition.')
|
msg = _('Message objects do not support addition.')
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
|
|
||||||
def __radd__(self, other):
|
def __radd__(self, other: Any) -> str:
|
||||||
return self.__add__(other)
|
return self.__add__(other)
|
||||||
|
@@ -19,7 +19,7 @@ __all__ = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def translate(obj, desired_locale=None):
|
def translate(obj: object, desired_locale: str | None = None) -> object:
|
||||||
"""Gets the translated unicode representation of the given object.
|
"""Gets the translated unicode representation of the given object.
|
||||||
|
|
||||||
If the object is not translatable it is returned as-is.
|
If the object is not translatable it is returned as-is.
|
||||||
@@ -48,7 +48,7 @@ def translate(obj, desired_locale=None):
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
def translate_args(args, desired_locale=None):
|
def translate_args(args: object, desired_locale: str | None = None) -> object:
|
||||||
"""Translates all the translatable elements of the given arguments object.
|
"""Translates all the translatable elements of the given arguments object.
|
||||||
|
|
||||||
This method is used for translating the translatable values in method
|
This method is used for translating the translatable values in method
|
||||||
|
@@ -12,6 +12,7 @@
|
|||||||
"""Test fixtures for working with oslo_i18n."""
|
"""Test fixtures for working with oslo_i18n."""
|
||||||
|
|
||||||
import gettext
|
import gettext
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import fixtures
|
import fixtures
|
||||||
|
|
||||||
@@ -19,7 +20,7 @@ from oslo_i18n import _lazy
|
|||||||
from oslo_i18n import _message
|
from oslo_i18n import _message
|
||||||
|
|
||||||
|
|
||||||
class Translation(fixtures.Fixture):
|
class Translation(fixtures.Fixture): # type: ignore[misc]
|
||||||
"""Fixture for managing translatable strings.
|
"""Fixture for managing translatable strings.
|
||||||
|
|
||||||
This class provides methods for creating translatable strings
|
This class provides methods for creating translatable strings
|
||||||
@@ -34,7 +35,7 @@ class Translation(fixtures.Fixture):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, domain='test-domain'):
|
def __init__(self, domain: str = 'test-domain') -> None:
|
||||||
"""Initialize the fixture.
|
"""Initialize the fixture.
|
||||||
|
|
||||||
:param domain: The translation domain. This is not expected to
|
:param domain: The translation domain. This is not expected to
|
||||||
@@ -44,7 +45,7 @@ class Translation(fixtures.Fixture):
|
|||||||
"""
|
"""
|
||||||
self.domain = domain
|
self.domain = domain
|
||||||
|
|
||||||
def lazy(self, msg):
|
def lazy(self, msg: str) -> _message.Message:
|
||||||
"""Return a lazily translated message.
|
"""Return a lazily translated message.
|
||||||
|
|
||||||
:param msg: Input message string. May optionally include
|
:param msg: Input message string. May optionally include
|
||||||
@@ -54,7 +55,7 @@ class Translation(fixtures.Fixture):
|
|||||||
"""
|
"""
|
||||||
return _message.Message(msg, domain=self.domain)
|
return _message.Message(msg, domain=self.domain)
|
||||||
|
|
||||||
def immediate(self, msg):
|
def immediate(self, msg: str) -> str:
|
||||||
"""Return a string as though it had been translated immediately.
|
"""Return a string as though it had been translated immediately.
|
||||||
|
|
||||||
:param msg: Input message string. May optionally include
|
:param msg: Input message string. May optionally include
|
||||||
@@ -65,10 +66,10 @@ class Translation(fixtures.Fixture):
|
|||||||
return str(msg)
|
return str(msg)
|
||||||
|
|
||||||
|
|
||||||
class ToggleLazy(fixtures.Fixture):
|
class ToggleLazy(fixtures.Fixture): # type: ignore[misc]
|
||||||
"""Fixture to toggle lazy translation on or off for a test."""
|
"""Fixture to toggle lazy translation on or off for a test."""
|
||||||
|
|
||||||
def __init__(self, enabled):
|
def __init__(self, enabled: bool) -> None:
|
||||||
"""Force lazy translation on or off.
|
"""Force lazy translation on or off.
|
||||||
|
|
||||||
:param enabled: Flag controlling whether to enable or disable
|
:param enabled: Flag controlling whether to enable or disable
|
||||||
@@ -79,12 +80,12 @@ class ToggleLazy(fixtures.Fixture):
|
|||||||
self._enabled = enabled
|
self._enabled = enabled
|
||||||
self._original_value = _lazy.USE_LAZY
|
self._original_value = _lazy.USE_LAZY
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.addCleanup(self._restore_original)
|
self.addCleanup(self._restore_original)
|
||||||
_lazy.enable_lazy(self._enabled)
|
_lazy.enable_lazy(self._enabled)
|
||||||
|
|
||||||
def _restore_original(self):
|
def _restore_original(self) -> None:
|
||||||
_lazy.enable_lazy(self._original_value)
|
_lazy.enable_lazy(self._original_value)
|
||||||
|
|
||||||
|
|
||||||
@@ -99,25 +100,26 @@ class _PrefixTranslator(gettext.NullTranslations):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, fp=None, prefix='noprefix'):
|
def __init__(self, fp: Any = None, prefix: str = 'noprefix') -> None:
|
||||||
gettext.NullTranslations.__init__(self, fp)
|
gettext.NullTranslations.__init__(self, fp)
|
||||||
self.prefix = prefix
|
self.prefix = prefix
|
||||||
|
|
||||||
def gettext(self, message):
|
def gettext(self, message: str) -> str:
|
||||||
msg = gettext.NullTranslations.gettext(self, message)
|
msg = gettext.NullTranslations.gettext(self, message)
|
||||||
return self.prefix + msg
|
return self.prefix + msg
|
||||||
|
|
||||||
def ugettext(self, message):
|
def ugettext(self, message: str) -> str:
|
||||||
msg = gettext.NullTranslations.ugettext(self, message)
|
# Note: ugettext is deprecated, fallback to gettext
|
||||||
|
msg = gettext.NullTranslations.gettext(self, message)
|
||||||
return self.prefix + msg
|
return self.prefix + msg
|
||||||
|
|
||||||
|
|
||||||
def _prefix_translations(*x, **y):
|
def _prefix_translations(*x: Any, **y: Any) -> _PrefixTranslator:
|
||||||
"""Use message id prefixed with domain and language as translation"""
|
"""Use message id prefixed with domain and language as translation"""
|
||||||
return _PrefixTranslator(prefix=x[0] + '/' + y['languages'][0] + ': ')
|
return _PrefixTranslator(prefix=x[0] + '/' + y['languages'][0] + ': ')
|
||||||
|
|
||||||
|
|
||||||
class PrefixLazyTranslation(fixtures.Fixture):
|
class PrefixLazyTranslation(fixtures.Fixture): # type: ignore[misc]
|
||||||
"""Fixture to prefix lazy translation enabled messages
|
"""Fixture to prefix lazy translation enabled messages
|
||||||
|
|
||||||
Use of this fixture will cause messages supporting lazy translation to
|
Use of this fixture will cause messages supporting lazy translation to
|
||||||
@@ -140,12 +142,14 @@ class PrefixLazyTranslation(fixtures.Fixture):
|
|||||||
|
|
||||||
_DEFAULT_LANG = 'en_US'
|
_DEFAULT_LANG = 'en_US'
|
||||||
|
|
||||||
def __init__(self, languages=None, locale=None):
|
def __init__(
|
||||||
|
self, languages: list[str] | None = None, locale: Any = None
|
||||||
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.languages = languages or [PrefixLazyTranslation._DEFAULT_LANG]
|
self.languages = languages or [PrefixLazyTranslation._DEFAULT_LANG]
|
||||||
self.locale = locale
|
self.locale = locale
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.useFixture(ToggleLazy(True))
|
self.useFixture(ToggleLazy(True))
|
||||||
self.useFixture(
|
self.useFixture(
|
||||||
|
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
"""logging utilities for translation"""
|
"""logging utilities for translation"""
|
||||||
|
|
||||||
|
import logging
|
||||||
from logging import handlers
|
from logging import handlers
|
||||||
|
|
||||||
from oslo_i18n import _translate
|
from oslo_i18n import _translate
|
||||||
@@ -55,7 +56,9 @@ class TranslationHandler(handlers.MemoryHandler):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, locale=None, target=None):
|
def __init__(
|
||||||
|
self, locale: str | None = None, target: logging.Handler | None = None
|
||||||
|
) -> None:
|
||||||
"""Initialize a TranslationHandler
|
"""Initialize a TranslationHandler
|
||||||
|
|
||||||
:param locale: locale to use for translating messages
|
:param locale: locale to use for translating messages
|
||||||
@@ -70,10 +73,11 @@ class TranslationHandler(handlers.MemoryHandler):
|
|||||||
handlers.MemoryHandler.__init__(self, capacity=0, target=target)
|
handlers.MemoryHandler.__init__(self, capacity=0, target=target)
|
||||||
self.locale = locale
|
self.locale = locale
|
||||||
|
|
||||||
def setFormatter(self, fmt):
|
def setFormatter(self, fmt: logging.Formatter | None) -> None:
|
||||||
|
if self.target is not None:
|
||||||
self.target.setFormatter(fmt)
|
self.target.setFormatter(fmt)
|
||||||
|
|
||||||
def emit(self, record):
|
def emit(self, record: logging.LogRecord) -> None:
|
||||||
# We save the message from the original record to restore it
|
# We save the message from the original record to restore it
|
||||||
# after translation, so other handlers are not affected by this
|
# after translation, so other handlers are not affected by this
|
||||||
original_msg = record.msg
|
original_msg = record.msg
|
||||||
@@ -85,12 +89,13 @@ class TranslationHandler(handlers.MemoryHandler):
|
|||||||
record.msg = original_msg
|
record.msg = original_msg
|
||||||
record.args = original_args
|
record.args = original_args
|
||||||
|
|
||||||
def _translate_and_log_record(self, record):
|
def _translate_and_log_record(self, record: logging.LogRecord) -> None:
|
||||||
record.msg = _translate.translate(record.msg, self.locale)
|
record.msg = _translate.translate(record.msg, self.locale)
|
||||||
|
|
||||||
# In addition to translating the message, we also need to translate
|
# In addition to translating the message, we also need to translate
|
||||||
# arguments that were passed to the log method that were not part
|
# arguments that were passed to the log method that were not part
|
||||||
# of the main message e.g., log.info(_('Some message %s'), this_one))
|
# of the main message e.g., log.info(_('Some message %s'), this_one))
|
||||||
record.args = _translate.translate_args(record.args, self.locale)
|
record.args = _translate.translate_args(record.args, self.locale) # type: ignore[assignment]
|
||||||
|
|
||||||
|
if self.target is not None:
|
||||||
self.target.emit(record)
|
self.target.emit(record)
|
||||||
|
@@ -41,8 +41,10 @@ class TranslationHandlerTestCase(test_base.BaseTestCase):
|
|||||||
self.logger.addHandler(self.translation_handler)
|
self.logger.addHandler(self.translation_handler)
|
||||||
|
|
||||||
def test_set_formatter(self):
|
def test_set_formatter(self):
|
||||||
formatter = 'some formatter'
|
formatter = logging.Formatter()
|
||||||
self.translation_handler.setFormatter(formatter)
|
self.translation_handler.setFormatter(formatter)
|
||||||
|
# narrow types https://github.com/python/mypy/issues/5088
|
||||||
|
assert self.translation_handler.target is not None
|
||||||
self.assertEqual(formatter, self.translation_handler.target.formatter)
|
self.assertEqual(formatter, self.translation_handler.target.formatter)
|
||||||
|
|
||||||
@mock.patch('gettext.translation')
|
@mock.patch('gettext.translation')
|
||||||
|
@@ -346,7 +346,7 @@ class MessageTestCase(test_base.BaseTestCase):
|
|||||||
obj = utils.SomeObject(message)
|
obj = utils.SomeObject(message)
|
||||||
unicoded_obj = str(obj)
|
unicoded_obj = str(obj)
|
||||||
|
|
||||||
self.assertEqual(es_translation, unicoded_obj.translation('es'))
|
self.assertEqual(es_translation, unicoded_obj.translation('es')) # type: ignore[attr-defined]
|
||||||
|
|
||||||
@mock.patch('gettext.translation')
|
@mock.patch('gettext.translation')
|
||||||
def test_translate_multiple_languages(self, mock_translation):
|
def test_translate_multiple_languages(self, mock_translation):
|
||||||
@@ -520,8 +520,8 @@ class MessageTestCase(test_base.BaseTestCase):
|
|||||||
obj = utils.SomeObject(msg)
|
obj = utils.SomeObject(msg)
|
||||||
unicoded_obj = str(obj)
|
unicoded_obj = str(obj)
|
||||||
|
|
||||||
self.assertEqual(expected_translation, unicoded_obj.translation('es'))
|
self.assertEqual(expected_translation, unicoded_obj.translation('es')) # type: ignore[attr-defined]
|
||||||
self.assertEqual(default_translation, unicoded_obj.translation('XX'))
|
self.assertEqual(default_translation, unicoded_obj.translation('XX')) # type: ignore[attr-defined]
|
||||||
|
|
||||||
@mock.patch('gettext.translation')
|
@mock.patch('gettext.translation')
|
||||||
def test_translate_message_with_message_parameter(self, mock_translation):
|
def test_translate_message_with_message_parameter(self, mock_translation):
|
||||||
|
@@ -46,3 +46,6 @@ docstring-code-format = true
|
|||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
select = ["E4", "E7", "E9", "F", "S", "UP"]
|
select = ["E4", "E7", "E9", "F", "S", "UP"]
|
||||||
ignore = ["F403"]
|
ignore = ["F403"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
"oslo_i18n/tests/*" = ["S"]
|
||||||
|
3
tox.ini
3
tox.ini
@@ -54,6 +54,9 @@ commands =
|
|||||||
# We only enable the hacking (H) checks
|
# We only enable the hacking (H) checks
|
||||||
select = H
|
select = H
|
||||||
show-source = true
|
show-source = true
|
||||||
|
# Ignore warnings that conflict with ruff-format
|
||||||
|
# H301: one import per line
|
||||||
|
ignore = H301
|
||||||
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,__init__.py
|
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,__init__.py
|
||||||
|
|
||||||
[hacking]
|
[hacking]
|
||||||
|
Reference in New Issue
Block a user