Prevent gettext() from installing to global _ namespace (#821)

This commit is contained in:
Chris Caron 2023-08-21 20:11:26 -04:00 committed by GitHub
parent 31caff1ac9
commit f82934a815
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 708 additions and 493 deletions

1
.gitignore vendored
View File

@ -26,6 +26,7 @@ sdist/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
*.egg *.egg
.local
# Generated from Docker Instance # Generated from Docker Instance
.bash_history .bash_history

View File

@ -40,9 +40,6 @@ from os.path import dirname
from os.path import abspath from os.path import abspath
from .logger import logger from .logger import logger
# Define our translation domain
DOMAIN = 'apprise'
LOCALE_DIR = abspath(join(dirname(__file__), 'i18n'))
# This gets toggled to True if we succeed # This gets toggled to True if we succeed
GETTEXT_LOADED = False GETTEXT_LOADED = False
@ -51,43 +48,13 @@ try:
# Initialize gettext # Initialize gettext
import gettext import gettext
# install() creates a _() in our builtins
gettext.install(DOMAIN, localedir=LOCALE_DIR)
# Toggle our flag # Toggle our flag
GETTEXT_LOADED = True GETTEXT_LOADED = True
except ImportError: except ImportError:
# gettext isn't available; no problem, just fall back to using # gettext isn't available; no problem; Use the library features without
# the library features without multi-language support. # multi-language support.
import builtins pass
builtins.__dict__['_'] = lambda x: x # pragma: no branch
class LazyTranslation:
"""
Doesn't translate anything until str() or unicode() references
are made.
"""
def __init__(self, text, *args, **kwargs):
"""
Store our text
"""
self.text = text
super().__init__(*args, **kwargs)
def __str__(self):
return gettext.gettext(self.text)
# Lazy translation handling
def gettext_lazy(text):
"""
A dummy function that can be referenced
"""
return LazyTranslation(text=text)
class AppriseLocale: class AppriseLocale:
@ -97,15 +64,24 @@ class AppriseLocale:
""" """
# Define our translation domain
_domain = 'apprise'
# The path to our translations
_locale_dir = abspath(join(dirname(__file__), 'i18n'))
# Locale regular expression # Locale regular expression
_local_re = re.compile( _local_re = re.compile(
r'^\s*(?P<lang>[a-z]{2})([_:]((?P<country>[a-z]{2}))?' r'^((?P<ansii>C)|(?P<lang>([a-z]{2}))([_:](?P<country>[a-z]{2}))?)'
r'(\.(?P<enc>[a-z0-9]+))?|.+)?', re.IGNORECASE) r'(\.(?P<enc>[a-z0-9-]+))?$', re.IGNORECASE)
# Define our default encoding # Define our default encoding
_default_encoding = 'utf-8' _default_encoding = 'utf-8'
# Define our default language # The function to assign `_` by default
_fn = 'gettext'
# The language we should fall back to if all else fails
_default_language = 'en' _default_language = 'en'
def __init__(self, language=None): def __init__(self, language=None):
@ -123,25 +99,55 @@ class AppriseLocale:
# Get our language # Get our language
self.lang = AppriseLocale.detect_language(language) self.lang = AppriseLocale.detect_language(language)
# Our mapping to our _fn
self.__fn_map = None
if GETTEXT_LOADED is False: if GETTEXT_LOADED is False:
# We're done # We're done
return return
if self.lang: # Add language
self.add(self.lang)
def add(self, lang=None, set_default=True):
"""
Add a language to our list
"""
lang = lang if lang else self._default_language
if lang not in self._gtobjs:
# Load our gettext object and install our language # Load our gettext object and install our language
try: try:
self._gtobjs[self.lang] = gettext.translation( self._gtobjs[lang] = gettext.translation(
DOMAIN, localedir=LOCALE_DIR, languages=[self.lang]) self._domain, localedir=self._locale_dir, languages=[lang],
fallback=False)
# Install our language # The non-intrusive method of applying the gettext change to
self._gtobjs[self.lang].install() # the global namespace only
self.__fn_map = getattr(self._gtobjs[lang], self._fn)
except IOError: except FileNotFoundError:
# This occurs if we can't access/load our translations # The translation directory does not exist
pass logger.debug(
'Could not load translation path: %s',
join(self._locale_dir, lang))
# Fallback (handle case where self.lang does not exist)
if self.lang not in self._gtobjs:
self._gtobjs[self.lang] = gettext
self.__fn_map = getattr(self._gtobjs[self.lang], self._fn)
return False
logger.trace('Loaded language %s', lang)
if set_default:
logger.debug('Language set to %s', lang)
self.lang = lang
return True
@contextlib.contextmanager @contextlib.contextmanager
def lang_at(self, lang): def lang_at(self, lang, mapto=_fn):
""" """
The syntax works as: The syntax works as:
with at.lang_at('fr'): with at.lang_at('fr'):
@ -151,46 +157,32 @@ class AppriseLocale:
""" """
if GETTEXT_LOADED is False: if GETTEXT_LOADED is False:
# yield # Do nothing
yield yield None
# we're done # we're done
return return
# Tidy the language # Tidy the language
lang = AppriseLocale.detect_language(lang, detect_fallback=False) lang = AppriseLocale.detect_language(lang, detect_fallback=False)
if lang not in self._gtobjs and not self.add(lang, set_default=False):
# Now attempt to load it # Do Nothing
try: yield getattr(self._gtobjs[self.lang], mapto)
if lang in self._gtobjs: else:
if lang != self.lang:
# Install our language only if we aren't using it
# already
self._gtobjs[lang].install()
else:
self._gtobjs[lang] = gettext.translation(
DOMAIN, localedir=LOCALE_DIR, languages=[self.lang])
# Install our language
self._gtobjs[lang].install()
# Yield # Yield
yield yield getattr(self._gtobjs[lang], mapto)
except (IOError, KeyError):
# This occurs if we can't access/load our translations
# Yield reguardless
yield
finally:
# Fall back to our previous language
if lang != self.lang and lang in self._gtobjs:
# Install our language
self._gtobjs[self.lang].install()
return return
@property
def gettext(self):
"""
Return the current language gettext() function
Useful for assigning to `_`
"""
return self._gtobjs[self.lang].gettext
@staticmethod @staticmethod
def detect_language(lang=None, detect_fallback=True): def detect_language(lang=None, detect_fallback=True):
""" """
@ -227,12 +219,12 @@ class AppriseLocale:
# Fallback to posix detection # Fallback to posix detection
pass pass
# Linux Handling # Built in locale library check
try: try:
# Acquire our locale # Acquire our locale
lang = locale.getlocale()[0] lang = locale.getlocale()[0]
except TypeError as e: except (ValueError, TypeError) as e:
# This occurs when an invalid locale was parsed from the # This occurs when an invalid locale was parsed from the
# environment variable. While we still return None in this # environment variable. While we still return None in this
# case, we want to better notify the end user of this. Users # case, we want to better notify the end user of this. Users
@ -249,8 +241,10 @@ class AppriseLocale:
Pickle Support dumps() Pickle Support dumps()
""" """
state = self.__dict__.copy() state = self.__dict__.copy()
# Remove the unpicklable entries. # Remove the unpicklable entries.
del state['_gtobjs'] del state['_gtobjs']
del state['_AppriseLocale__fn_map']
return state return state
def __setstate__(self, state): def __setstate__(self, state):
@ -258,4 +252,39 @@ class AppriseLocale:
Pickle Support loads() Pickle Support loads()
""" """
self.__dict__.update(state) self.__dict__.update(state)
# Our mapping to our _fn
self.__fn_map = None
self._gtobjs = {} self._gtobjs = {}
self.add(state['lang'], set_default=True)
#
# Prepare our default LOCALE Singleton
#
LOCALE = AppriseLocale()
class LazyTranslation:
"""
Doesn't translate anything until str() or unicode() references
are made.
"""
def __init__(self, text, *args, **kwargs):
"""
Store our text
"""
self.text = text
super().__init__(*args, **kwargs)
def __str__(self):
return LOCALE.gettext(self.text) if GETTEXT_LOADED else self.text
# Lazy translation handling
def gettext_lazy(text):
"""
A dummy function that can be referenced
"""
return LazyTranslation(text=text)

View File

@ -3,9 +3,10 @@
# This file is distributed under the same license as the apprise project. # This file is distributed under the same license as the apprise project.
# Chris Caron <lead2gold@gmail.com>, 2019. # Chris Caron <lead2gold@gmail.com>, 2019.
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: apprise 0.7.6\n"
"Project-Id-Version: apprise 1.4.5\n"
"Report-Msgid-Bugs-To: lead2gold@gmail.com\n" "Report-Msgid-Bugs-To: lead2gold@gmail.com\n"
"POT-Creation-Date: 2019-05-28 16:56-0400\n" "POT-Creation-Date: 2019-05-28 16:56-0400\n"
"PO-Revision-Date: 2019-05-24 20:00-0400\n" "PO-Revision-Date: 2019-05-24 20:00-0400\n"
@ -18,276 +19,272 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.6.0\n" "Generated-By: Babel 2.6.0\n"
msgid "API Key" msgid "API Key"
msgstr "" msgstr "API Key"
msgid "Access Key" msgid "Access Key"
msgstr "" msgstr "Access Key"
msgid "Access Key ID" msgid "Access Key ID"
msgstr "" msgstr "Access Key ID"
msgid "Access Secret" msgid "Access Secret"
msgstr "" msgstr "Access Secret"
msgid "Access Token" msgid "Access Token"
msgstr "" msgstr "Access Token"
msgid "Account SID" msgid "Account SID"
msgstr "" msgstr "Account SID"
msgid "Add Tokens" msgid "Add Tokens"
msgstr "" msgstr "Add Tokens"
msgid "Application Key" msgid "Application Key"
msgstr "" msgstr "Application Key"
msgid "Application Secret" msgid "Application Secret"
msgstr "" msgstr "Application Secret"
msgid "Auth Token" msgid "Auth Token"
msgstr "" msgstr "Auth Token"
msgid "Authorization Token" msgid "Authorization Token"
msgstr "" msgstr "Authorization Token"
msgid "Avatar Image" msgid "Avatar Image"
msgstr "" msgstr "Avatar Image"
msgid "Bot Name" msgid "Bot Name"
msgstr "" msgstr "Bot Name"
msgid "Bot Token" msgid "Bot Token"
msgstr "" msgstr "Bot Token"
msgid "Channels" msgid "Channels"
msgstr "" msgstr "Channels"
msgid "Consumer Key" msgid "Consumer Key"
msgstr "" msgstr "Consumer Key"
msgid "Consumer Secret" msgid "Consumer Secret"
msgstr "" msgstr "Consumer Secret"
msgid "Detect Bot Owner" msgid "Detect Bot Owner"
msgstr "" msgstr "Detect Bot Owner"
msgid "Device ID" msgid "Device ID"
msgstr "" msgstr "Device ID"
msgid "Display Footer" msgid "Display Footer"
msgstr "" msgstr "Display Footer"
msgid "Domain" msgid "Domain"
msgstr "" msgstr "Domain"
msgid "Duration" msgid "Duration"
msgstr "" msgstr "Duration"
msgid "Events" msgid "Events"
msgstr "" msgstr "Events"
msgid "Footer Logo" msgid "Footer Logo"
msgstr "" msgstr "Footer Logo"
msgid "From Email" msgid "From Email"
msgstr "" msgstr "From Email"
msgid "From Name" msgid "From Name"
msgstr "" msgstr "From Name"
msgid "From Phone No" msgid "From Phone No"
msgstr "" msgstr "From Phone No"
msgid "Group" msgid "Group"
msgstr "" msgstr "Group"
msgid "HTTP Header" msgid "HTTP Header"
msgstr "" msgstr "HTTP Header"
msgid "Hostname" msgid "Hostname"
msgstr "" msgstr "Hostname"
msgid "Include Image" msgid "Include Image"
msgstr "" msgstr "Include Image"
msgid "Modal" msgid "Modal"
msgstr "" msgstr "Modal"
msgid "Notify Format" msgid "Notify Format"
msgstr "" msgstr "Notify Format"
msgid "Organization" msgid "Organization"
msgstr "" msgstr "Organization"
msgid "Overflow Mode" msgid "Overflow Mode"
msgstr "" msgstr "Overflow Mode"
msgid "Password" msgid "Password"
msgstr "" msgstr "Password"
msgid "Port" msgid "Port"
msgstr "" msgstr "Port"
msgid "Priority" msgid "Priority"
msgstr "" msgstr "Priority"
msgid "Provider Key" msgid "Provider Key"
msgstr "" msgstr "Provider Key"
msgid "Region" msgid "Region"
msgstr "" msgstr "Region"
msgid "Region Name" msgid "Region Name"
msgstr "" msgstr "Region Name"
msgid "Remove Tokens" msgid "Remove Tokens"
msgstr "" msgstr "Remove Tokens"
msgid "Rooms" msgid "Rooms"
msgstr "" msgstr "Rooms"
msgid "SMTP Server" msgid "SMTP Server"
msgstr "" msgstr "SMTP Server"
msgid "Schema" msgid "Schema"
msgstr "" msgstr "Schema"
msgid "Secret Access Key" msgid "Secret Access Key"
msgstr "" msgstr "Secret Access Key"
msgid "Secret Key" msgid "Secret Key"
msgstr "" msgstr "Secret Key"
msgid "Secure Mode" msgid "Secure Mode"
msgstr "" msgstr "Secure Mode"
msgid "Server Timeout" msgid "Server Timeout"
msgstr "" msgstr "Server Timeout"
msgid "Sound" msgid "Sound"
msgstr "" msgstr "Sound"
msgid "Source JID" msgid "Source JID"
msgstr "" msgstr "Source JID"
msgid "Target Channel" msgid "Target Channel"
msgstr "" msgstr "Target Channel"
msgid "Target Chat ID" msgid "Target Chat ID"
msgstr "" msgstr "Target Chat ID"
msgid "Target Device" msgid "Target Device"
msgstr "" msgstr "Target Device"
msgid "Target Device ID" msgid "Target Device ID"
msgstr "" msgstr "Target Device ID"
msgid "Target Email" msgid "Target Email"
msgstr "" msgstr "Target Email"
msgid "Target Emails" msgid "Target Emails"
msgstr "" msgstr "Target Emails"
msgid "Target Encoded ID" msgid "Target Encoded ID"
msgstr "" msgstr "Target Encoded ID"
msgid "Target JID" msgid "Target JID"
msgstr "" msgstr "Target JID"
msgid "Target Phone No" msgid "Target Phone No"
msgstr "" msgstr "Target Phone No"
msgid "Target Room Alias" msgid "Target Room Alias"
msgstr "" msgstr "Target Room Alias"
msgid "Target Room ID" msgid "Target Room ID"
msgstr "" msgstr "Target Room ID"
msgid "Target Short Code" msgid "Target Short Code"
msgstr "" msgstr "Target Short Code"
msgid "Target Tag ID" msgid "Target Tag ID"
msgstr "" msgstr "Target Tag ID"
msgid "Target Topic" msgid "Target Topic"
msgstr "" msgstr "Target Topic"
msgid "Target User" msgid "Target User"
msgstr "" msgstr "Target User"
msgid "Targets" msgid "Targets"
msgstr "" msgstr "Targets"
msgid "Text To Speech" msgid "Text To Speech"
msgstr "" msgstr "Text To Speech"
msgid "To Channel ID" msgid "To Channel ID"
msgstr "" msgstr "To Channel ID"
msgid "To Email" msgid "To Email"
msgstr "" msgstr "To Email"
msgid "To User ID" msgid "To User ID"
msgstr "" msgstr "To User ID"
msgid "Token" msgid "Token"
msgstr "" msgstr "Token"
msgid "Token A" msgid "Token A"
msgstr "" msgstr "Token A"
msgid "Token B" msgid "Token B"
msgstr "" msgstr "Token B"
msgid "Token C" msgid "Token C"
msgstr "" msgstr "Token C"
msgid "Urgency" msgid "Urgency"
msgstr "" msgstr "Urgency"
msgid "Use Avatar" msgid "Use Avatar"
msgstr "" msgstr "Use Avatar"
msgid "User" msgid "User"
msgstr "" msgstr "User"
msgid "User Key" msgid "User Key"
msgstr "" msgstr "User Key"
msgid "User Name" msgid "User Name"
msgstr "" msgstr "User Name"
msgid "Username" msgid "Username"
msgstr "" msgstr "Username"
msgid "Verify SSL" msgid "Verify SSL"
msgstr "" msgstr "Verify SSL"
msgid "Version" msgid "Version"
msgstr "" msgstr "Version"
msgid "Webhook" msgid "Webhook"
msgstr "" msgstr "Webhook"
msgid "Webhook ID" msgid "Webhook ID"
msgstr "" msgstr "Webhook ID"
msgid "Webhook Mode" msgid "Webhook Mode"
msgstr "" msgstr "Webhook Mode"
msgid "Webhook Token" msgid "Webhook Token"
msgstr "" msgstr "Webhook Token"
msgid "X-Axis" msgid "X-Axis"
msgstr "" msgstr "X-Axis"
msgid "XEP" msgid "XEP"
msgstr "" msgstr "XEP"
msgid "Y-Axis"
msgstr ""
#~ msgid "Access Key Secret"
#~ msgstr ""
msgid "Y-Axis"
msgstr "Y-Axis"

View File

@ -1,7 +1,7 @@
diff -Naur apprise-1.0.0/test/test_cli.py apprise-1.0.0.patched/test/test_cli.py diff -Naur apprise-1.4.5/test/test_apprise_cli.py apprise-1.4.5-patched/test/test_apprise_cli.py
--- apprise-1.0.0/test/test_cli.py 2022-07-15 14:52:13.000000000 -0400 --- apprise-1.4.5/test/test_apprise_cli.py 2023-08-20 11:26:43.000000000 -0400
+++ apprise-1.0.0.patched/test/test_cli.py 2022-08-06 13:32:50.796935607 -0400 +++ apprise-1.4.5-patched/test/test_apprise_cli.py 2023-08-20 16:37:42.922342103 -0400
@@ -1022,9 +1022,6 @@ @@ -1027,9 +1027,6 @@
# Absolute path to __init__.py is okay # Absolute path to __init__.py is okay
assert result.exit_code == 0 assert result.exit_code == 0
@ -11,7 +11,7 @@ diff -Naur apprise-1.0.0/test/test_cli.py apprise-1.0.0.patched/test/test_cli.py
# Clear our working variables so they don't obstruct the next test # Clear our working variables so they don't obstruct the next test
# This simulates an actual call from the CLI. Unfortunately through # This simulates an actual call from the CLI. Unfortunately through
# testing were occupying the same memory space so our singleton's # testing were occupying the same memory space so our singleton's
@@ -1044,9 +1041,6 @@ @@ -1049,9 +1046,6 @@
# an __init__.py is found on the inside of it # an __init__.py is found on the inside of it
assert result.exit_code == 0 assert result.exit_code == 0
@ -21,7 +21,7 @@ diff -Naur apprise-1.0.0/test/test_cli.py apprise-1.0.0.patched/test/test_cli.py
# Test double paths that are the same; this ensures we only # Test double paths that are the same; this ensures we only
# load the plugin once # load the plugin once
result = runner.invoke(cli.main, [ result = runner.invoke(cli.main, [
@@ -1179,15 +1173,6 @@ @@ -1183,15 +1177,6 @@
# Print our custom details to the screen # Print our custom details to the screen
'--details', '--details',
]) ])

View File

@ -3,11 +3,11 @@ universal = 0
[metadata] [metadata]
# ensure LICENSE is included in wheel metadata # ensure LICENSE is included in wheel metadata
license_file = LICENSE license_files = LICENSE
[flake8] [flake8]
# We exclude packages we don't maintain # We exclude packages we don't maintain
exclude = .eggs,.tox exclude = .eggs,.tox,.local
ignore = E741,E722,W503,W504,W605 ignore = E741,E722,W503,W504,W605
statistics = true statistics = true
builtins = _ builtins = _

View File

@ -90,7 +90,7 @@ setup(
], ],
}, },
install_requires=install_requires, install_requires=install_requires,
classifiers=( classifiers=[
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Intended Audience :: System Administrators', 'Intended Audience :: System Administrators',
@ -109,7 +109,7 @@ setup(
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries :: Python Modules',
'Topic :: Software Development :: Libraries :: Application Frameworks', 'Topic :: Software Development :: Libraries :: Application Frameworks',
), ],
entry_points={'console_scripts': console_scripts}, entry_points={'console_scripts': console_scripts},
python_requires='>=3.6', python_requires='>=3.6',
setup_requires=['babel', ], setup_requires=['babel', ],

View File

@ -53,6 +53,7 @@ from apprise import __version__
from apprise import URLBase from apprise import URLBase
from apprise import PrivacyMode from apprise import PrivacyMode
from apprise.AppriseLocale import LazyTranslation from apprise.AppriseLocale import LazyTranslation
from apprise.AppriseLocale import gettext_lazy as _
from apprise import common from apprise import common
from apprise.plugins import __load_matrix from apprise.plugins import __load_matrix
@ -1379,7 +1380,8 @@ def test_apprise_details():
assert 'details' in entry['requirements'] assert 'details' in entry['requirements']
assert 'packages_required' in entry['requirements'] assert 'packages_required' in entry['requirements']
assert 'packages_recommended' in entry['requirements'] assert 'packages_recommended' in entry['requirements']
assert isinstance(entry['requirements']['details'], str) assert isinstance(entry['requirements']['details'], (
str, LazyTranslation))
assert isinstance(entry['requirements']['packages_required'], list) assert isinstance(entry['requirements']['packages_required'], list)
assert isinstance(entry['requirements']['packages_recommended'], list) assert isinstance(entry['requirements']['packages_recommended'], list)
@ -1406,7 +1408,8 @@ def test_apprise_details():
assert 'details' in entry['requirements'] assert 'details' in entry['requirements']
assert 'packages_required' in entry['requirements'] assert 'packages_required' in entry['requirements']
assert 'packages_recommended' in entry['requirements'] assert 'packages_recommended' in entry['requirements']
assert isinstance(entry['requirements']['details'], str) assert isinstance(entry['requirements']['details'], (
str, LazyTranslation))
assert isinstance(entry['requirements']['packages_required'], list) assert isinstance(entry['requirements']['packages_required'], list)
assert isinstance(entry['requirements']['packages_recommended'], list) assert isinstance(entry['requirements']['packages_recommended'], list)

View File

@ -49,6 +49,8 @@ from apprise.utils import environ
from apprise.plugins import __load_matrix from apprise.plugins import __load_matrix
from apprise.plugins import __reset_matrix from apprise.plugins import __reset_matrix
from apprise.AppriseLocale import gettext_lazy as _
from importlib import reload from importlib import reload

View File

@ -0,0 +1,398 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os
import sys
from unittest import mock
import ctypes
import pytest
from apprise import AppriseLocale
from apprise.utils import environ
from importlib import reload
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
def test_apprise_trans():
"""
API: Test apprise locale object
"""
lazytrans = AppriseLocale.LazyTranslation('Token')
assert str(lazytrans) == 'Token'
@pytest.mark.skipif(
'gettext' not in sys.modules, reason="Requires gettext")
def test_apprise_trans_gettext_init():
"""
API: Handle gettext
"""
# Toggle
AppriseLocale.GETTEXT_LOADED = False
# Objects can still be created
al = AppriseLocale.AppriseLocale()
with al.lang_at('en') as _:
# functions still behave as normal
assert _ is None
# Restore the object
AppriseLocale.GETTEXT_LOADED = True
@pytest.mark.skipif(
'gettext' not in sys.modules, reason="Requires gettext")
@mock.patch('gettext.translation')
@mock.patch('locale.getlocale')
def test_apprise_trans_gettext_translations(
mock_getlocale, mock_gettext_trans):
"""
API: Apprise() Gettext translations
"""
# Set- our gettext.locale() return value
mock_getlocale.return_value = ('en_US', 'UTF-8')
mock_gettext_trans.side_effect = FileNotFoundError()
# This throws internally but we handle it gracefully
al = AppriseLocale.AppriseLocale()
with al.lang_at('en'):
# functions still behave as normal
pass
# This throws internally but we handle it gracefully
AppriseLocale.AppriseLocale(language="fr")
@pytest.mark.skipif(
hasattr(ctypes, 'windll'), reason="Unique Nux test cases")
@pytest.mark.skipif(
'gettext' not in sys.modules, reason="Requires gettext")
@mock.patch('locale.getlocale')
def test_apprise_trans_gettext_lang_at(mock_getlocale):
"""
API: Apprise() Gettext lang_at
"""
# Set- our gettext.locale() return value
mock_getlocale.return_value = ('en_CA', 'UTF-8')
# This throws internally but we handle it gracefully
al = AppriseLocale.AppriseLocale()
# Edge Cases
assert al.add('en', set_default=False) is True
assert al.add('en', set_default=True) is True
with al.lang_at('en'):
# functions still behave as normal
pass
# This throws internally but we handle it gracefully
AppriseLocale.AppriseLocale(language="fr")
with al.lang_at('en') as _:
# functions still behave as normal
assert callable(_)
with al.lang_at('es') as _:
# functions still behave as normal
assert callable(_)
with al.lang_at('fr') as _:
# functions still behave as normal
assert callable(_)
# Test our initialization when our fallback is a language we do
# not have. This is only done to test edge cases when for whatever
# reason the person who set up apprise does not have the languages
# installed.
fallback = AppriseLocale.AppriseLocale._default_language
mock_getlocale.return_value = None
with environ('LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG'):
# Our default language
AppriseLocale.AppriseLocale._default_language = 'zz'
# We will detect the zz since there were no environment variables to
# help us otherwise
assert AppriseLocale.AppriseLocale.detect_language() is None
al = AppriseLocale.AppriseLocale()
# No Language could be set becuause no locale directory exists for this
assert al.lang is None
with al.lang_at(None) as _:
# functions still behave as normal
assert callable(_)
with al.lang_at('en') as _:
# functions still behave as normal
assert callable(_)
with al.lang_at('es') as _:
# functions still behave as normal
assert callable(_)
with al.lang_at('fr') as _:
# functions still behave as normal
assert callable(_)
# We can still perform simple lookups; they access a dummy wrapper:
assert al.gettext('test') == 'test'
with environ('LANGUAGE', 'LC_CTYPE', LC_ALL='C.UTF-8', LANG="en_CA"):
# the UTF-8 entry is skipped over
AppriseLocale.AppriseLocale._default_language = 'fr'
# We will detect the english language (found in the LANG= environment
# variable which over-rides the _default
assert AppriseLocale.AppriseLocale.detect_language() == "en"
al = AppriseLocale.AppriseLocale()
assert al.lang == "en"
assert al.gettext('test') == 'test'
# Test case with set_default set to False (so we're still set to 'fr')
assert al.add('zy', set_default=False) is False
assert al.gettext('test') == 'test'
al.add('ab', set_default=True)
assert al.gettext('test') == 'test'
assert al.add('zy', set_default=False) is False
AppriseLocale.AppriseLocale._default_language = fallback
@pytest.mark.skipif(
'gettext' not in sys.modules, reason="Requires gettext")
def test_apprise_trans_add():
"""
API: Apprise() Gettext add
"""
# This throws internally but we handle it gracefully
al = AppriseLocale.AppriseLocale()
assert al.add('en') is True
# Double add (copy of above) to access logic that prevents adding it again
assert al.add('en') is True
# Invalid Language
assert al.add('bad') is False
@pytest.mark.skipif(
not hasattr(ctypes, 'windll'), reason="Unique Windows test cases")
@pytest.mark.skipif(
'gettext' not in sys.modules, reason="Requires gettext")
@mock.patch('locale.getlocale')
def test_apprise_trans_windows_users_win(mock_getlocale):
"""
API: Apprise() Windows Locale Testing (Win version)
"""
# Set- our gettext.locale() return value
mock_getlocale.return_value = ('fr_CA', 'UTF-8')
with mock.patch(
'ctypes.windll.kernel32.GetUserDefaultUILanguage') as ui_lang:
# 4105 = en_CA
ui_lang.return_value = 4105
with environ('LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG'):
# Our default language
AppriseLocale.AppriseLocale._default_language = 'zz'
# We will pick up the windll module and detect english
assert AppriseLocale.AppriseLocale.detect_language() == 'en'
# The below accesses the windows fallback code
with environ('LANGUAGE', 'LC_ALL', 'LC_CTYPE', LANG="es_AR"):
# Environment Variable Trumps
assert AppriseLocale.AppriseLocale.detect_language() == 'es'
# No environment variable, then the Windows environment is used
with environ('LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG'):
# Windows Environment
assert AppriseLocale.AppriseLocale.detect_language() == 'en'
assert AppriseLocale.AppriseLocale\
.detect_language(detect_fallback=False) is None
# 0 = IndexError
ui_lang.return_value = 0
with environ('LANGUAGE', 'LANG', 'LC_ALL', 'LC_CTYPE'):
# We fall back to posix locale
assert AppriseLocale.AppriseLocale.detect_language() == 'fr'
@pytest.mark.skipif(
hasattr(ctypes, 'windll'), reason="Unique Nux test cases")
@pytest.mark.skipif(
'gettext' not in sys.modules, reason="Requires gettext")
@mock.patch('locale.getlocale')
def test_apprise_trans_windows_users_nux(mock_getlocale):
"""
API: Apprise() Windows Locale Testing (Nux version)
"""
# Set- our gettext.locale() return value
mock_getlocale.return_value = ('fr_CA', 'UTF-8')
# Emulate a windows environment
windll = mock.Mock()
setattr(ctypes, 'windll', windll)
# 4105 = en_CA
windll.kernel32.GetUserDefaultUILanguage.return_value = 4105
with environ('LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG'):
# Our default language
AppriseLocale.AppriseLocale._default_language = 'zz'
# We will pick up the windll module and detect english
assert AppriseLocale.AppriseLocale.detect_language() == 'en'
# The below accesses the windows fallback code
with environ('LANGUAGE', 'LC_ALL', 'LC_CTYPE', LANG="es_AR"):
# Environment Variable Trumps
assert AppriseLocale.AppriseLocale.detect_language() == 'es'
# No environment variable, then the Windows environment is used
with environ('LANGUAGE', 'LC_ALL', 'LC_CTYPE', 'LANG'):
# Windows Environment
assert AppriseLocale.AppriseLocale.detect_language() == 'en'
assert AppriseLocale.AppriseLocale\
.detect_language(detect_fallback=False) is None
# 0 = IndexError
windll.kernel32.GetUserDefaultUILanguage.return_value = 0
with environ('LANGUAGE', 'LANG', 'LC_ALL', 'LC_CTYPE'):
# We fall back to posix locale
assert AppriseLocale.AppriseLocale.detect_language() == 'fr'
delattr(ctypes, 'windll')
@pytest.mark.skipif(sys.platform == "win32", reason="Unique Nux test cases")
@mock.patch('locale.getlocale')
def test_detect_language_using_env(mock_getlocale):
"""
Test the reading of information from an environment variable
"""
# Set- our gettext.locale() return value
mock_getlocale.return_value = ('en_CA', 'UTF-8')
# The below accesses the windows fallback code and fail
# then it will resort to the environment variables.
with environ('LANG', 'LANGUAGE', 'LC_ALL', 'LC_CTYPE'):
# Language can now be detected in this case
assert isinstance(
AppriseLocale.AppriseLocale.detect_language(), str)
# Detect French language.
with environ('LANGUAGE', 'LC_ALL', LC_CTYPE="garbage", LANG="fr_CA"):
assert AppriseLocale.AppriseLocale.detect_language() == 'fr'
# The following unsets all environment variables and sets LC_CTYPE
# This was causing Python 2.7 to internally parse UTF-8 as an invalid
# locale and throw an uncaught ValueError; Python v2 support has been
# dropped, but just to ensure this issue does not come back, we keep
# this test:
with environ(*list(os.environ.keys()), LC_CTYPE="UTF-8"):
assert isinstance(AppriseLocale.AppriseLocale.detect_language(), str)
# Test with absolutely no environment variables what-so-ever
with environ(*list(os.environ.keys())):
assert isinstance(AppriseLocale.AppriseLocale.detect_language(), str)
# Handle case where getlocale() can't be detected
mock_getlocale.return_value = None
with environ('LC_ALL', 'LC_CTYPE', 'LANG', 'LANGUAGE'):
assert AppriseLocale.AppriseLocale.detect_language() is None
mock_getlocale.return_value = (None, None)
with environ('LC_ALL', 'LC_CTYPE', 'LANG', 'LANGUAGE'):
assert AppriseLocale.AppriseLocale.detect_language() is None
# if detect_language and windows env fail us, then we don't
# set up a default language on first load
AppriseLocale.AppriseLocale()
@pytest.mark.skipif(
'gettext' not in sys.modules, reason="Requires gettext")
def test_apprise_trans_gettext_missing(tmpdir):
"""
Verify we can still operate without the gettext library
"""
# remove gettext from our system enviroment
del sys.modules["gettext"]
# Make our new path to a fake gettext (used to over-ride real one)
# have it fail right out of the gate
gettext_dir = tmpdir.mkdir("gettext")
gettext_dir.join("__init__.py").write("")
gettext_dir.join("gettext.py").write("""raise ImportError()""")
# Update our path to point path to head
sys.path.insert(0, str(gettext_dir))
# reload our module (forcing the import error when it tries to load gettext
reload(sys.modules['apprise.AppriseLocale'])
from apprise import AppriseLocale
assert AppriseLocale.GETTEXT_LOADED is False
# Now roll our changes back
sys.path.pop(0)
# Reload again (reverting back)
reload(sys.modules['apprise.AppriseLocale'])
from apprise import AppriseLocale
assert AppriseLocale.GETTEXT_LOADED is True

View File

@ -1,215 +0,0 @@
# -*- coding: utf-8 -*-
# BSD 3-Clause License
#
# Apprise - Push Notification Library.
# Copyright (c) 2023, Chris Caron <lead2gold@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import os
import sys
from unittest import mock
import ctypes
import pytest
from apprise import AppriseLocale
from apprise.utils import environ
from importlib import reload
# Disable logging for a cleaner testing output
import logging
logging.disable(logging.CRITICAL)
@mock.patch('gettext.install')
def test_apprise_locale(mock_gettext_install):
"""
API: Test apprise locale object
"""
lazytrans = AppriseLocale.LazyTranslation('Token')
assert str(lazytrans) == 'Token'
@mock.patch('gettext.install')
def test_gettext_init(mock_gettext_install):
"""
API: Mock Gettext init
"""
mock_gettext_install.side_effect = ImportError()
# Test our fall back to not supporting translations
reload(AppriseLocale)
# Objects can still be created
al = AppriseLocale.AppriseLocale()
with al.lang_at('en'):
# functions still behave as normal
pass
# restore the object
mock_gettext_install.side_effect = None
reload(AppriseLocale)
@mock.patch('gettext.translation')
def test_gettext_translations(mock_gettext_trans):
"""
API: Apprise() Gettext translations
"""
mock_gettext_trans.side_effect = IOError()
# This throws internally but we handle it gracefully
al = AppriseLocale.AppriseLocale()
with al.lang_at('en'):
# functions still behave as normal
pass
# This throws internally but we handle it gracefully
AppriseLocale.AppriseLocale(language="fr")
@mock.patch('gettext.translation')
def test_gettext_installs(mock_gettext_trans):
"""
API: Apprise() Gettext install
"""
mock_lang = mock.Mock()
mock_lang.install.return_value = True
mock_gettext_trans.return_value = mock_lang
# This throws internally but we handle it gracefully
al = AppriseLocale.AppriseLocale()
with al.lang_at('en'):
# functions still behave as normal
pass
# This throws internally but we handle it gracefully
AppriseLocale.AppriseLocale(language="fr")
# Force a few different languages
al._gtobjs['en'] = mock_lang
al._gtobjs['es'] = mock_lang
al.lang = 'en'
with al.lang_at('en'):
# functions still behave as normal
pass
with al.lang_at('es'):
# functions still behave as normal
pass
with al.lang_at('fr'):
# functions still behave as normal
pass
def test_detect_language_windows_users():
"""
API: Apprise() Detect language
"""
if hasattr(ctypes, 'windll'):
from ctypes import windll
else:
windll = mock.Mock()
# 4105 = en_CA
windll.kernel32.GetUserDefaultUILanguage.return_value = 4105
setattr(ctypes, 'windll', windll)
# The below accesses the windows fallback code
with environ('LANG', 'LANGUAGE', 'LC_ALL', 'LC_CTYPE', LANG="en_CA"):
assert AppriseLocale.AppriseLocale.detect_language() == 'en'
assert AppriseLocale.AppriseLocale\
.detect_language(detect_fallback=False) is None
# 0 = IndexError
windll.kernel32.GetUserDefaultUILanguage.return_value = 0
setattr(ctypes, 'windll', windll)
with environ('LANG', 'LC_ALL', 'LC_CTYPE', LANGUAGE="en_CA"):
assert AppriseLocale.AppriseLocale.detect_language() == 'en'
def test_detect_language_using_env():
"""
Test the reading of information from an environment variable
"""
# The below accesses the windows fallback code and fail
# then it will resort to the environment variables.
with environ('LANG', 'LANGUAGE', 'LC_ALL', 'LC_CTYPE'):
# Language can now be detected in this case
assert isinstance(
AppriseLocale.AppriseLocale.detect_language(), str)
# Detect French language.
with environ('LANGUAGE', 'LC_ALL', 'LC_CTYPE', LANG="fr_CA"):
assert AppriseLocale.AppriseLocale.detect_language() == 'fr'
# The following unsets all environment variables and sets LC_CTYPE
# This was causing Python 2.7 to internally parse UTF-8 as an invalid
# locale and throw an uncaught ValueError; Python v2 support has been
# dropped, but just to ensure this issue does not come back, we keep
# this test:
with environ(*list(os.environ.keys()), LC_CTYPE="UTF-8"):
assert isinstance(AppriseLocale.AppriseLocale.detect_language(), str)
# Test with absolutely no environment variables what-so-ever
with environ(*list(os.environ.keys())):
assert isinstance(AppriseLocale.AppriseLocale.detect_language(), str)
@pytest.mark.skipif(sys.platform == "win32", reason="Does not work on Windows")
@mock.patch('locale.getlocale')
def test_detect_language_locale(mock_getlocale):
"""
API: Apprise() Default locale detection
"""
# Handle case where getlocale() can't be detected
mock_getlocale.return_value = None
with environ('LC_ALL', 'LC_CTYPE', 'LANG', 'LANGUAGE'):
assert AppriseLocale.AppriseLocale.detect_language() is None
mock_getlocale.return_value = (None, None)
with environ('LC_ALL', 'LC_CTYPE', 'LANG', 'LANGUAGE'):
assert AppriseLocale.AppriseLocale.detect_language() is None
# if detect_language and windows env fail us, then we don't
# set up a default language on first load
AppriseLocale.AppriseLocale()