diff --git a/README.md b/README.md index 84b914bda..b65de4211 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ System Administrators and DevOps who wish to send a notification now no longer n * [Configuration Files](#cli-configuration-files) * [File Attachments](#cli-file-attachments) * [Loading Custom Notifications/Hooks](#cli-loading-custom-notificationshooks) + * [Environment Variables](#cli-environment-variables) * [Developer API Usage](#developer-api-usage) * [Configuration Files](#api-configuration-files) * [File Attachments](#api-file-attachments) @@ -352,6 +353,17 @@ apprise -vv --title 'custom override' \ You can read more about creating your own custom notifications and/or hooks [here](https://github.com/caronc/apprise/wiki/decorator_notify). +## CLI Environment Variables + +Those using the Command Line Interface (CLI) can also leverage environment variables to pre-set the default settings: + +| Variable | Description | +|------------------------ | ----------------- | +| `APPRISE_URLS` | Specify the default URLs to notify IF none are otherwise specified on the command line explicitly. If the `--config` (`-c`) is specified, then this will over-rides any reference to this variable. Use white space and/or a comma (`,`) to delimit multiple entries. +| `APPRISE_CONFIG_PATH` | Explicitly specify the config search path to use (over-riding the default). The path(s) defined here must point to the absolute filename to open/reference. Use a semi-colon (`;`), line-feed (`\n`), and/or carriage return (`\r`) to delimit multiple entries. +| `APPRISE_PLUGIN_PATH` | Explicitly specify the custom plugin search path to use (over-riding the default). Use a semi-colon (`;`), line-feed (`\n`), and/or carriage return (`\r`) to delimit multiple entries. +| `APPRISE_STORAGE_PATH` | Explicitly specify the persistent storage path to use (over-riding the default). + # Developer API Usage To send a notification from within your python application, just do the following: diff --git a/apprise/cli.py b/apprise/cli.py index 78a9853c2..3c7f19365 100644 --- a/apprise/cli.py +++ b/apprise/cli.py @@ -68,6 +68,21 @@ DEFAULT_STORAGE_UID_LENGTH = \ int(os.environ.get('APPRISE_STORAGE_UID_LENGTH', 8)) +# Defines the envrionment variable to parse if defined. This is ONLY +# Referenced if: +# - No Configuration Files were found/loaded/specified +# - No URLs were provided directly into the CLI Call +DEFAULT_ENV_APPRISE_URLS = 'APPRISE_URLS' + +# Defines the over-ride path for the configuration files read +DEFAULT_ENV_APPRISE_CONFIG_PATH = 'APPRISE_CONFIG_PATH' + +# Defines the over-ride path for the plugins to load +DEFAULT_ENV_APPRISE_PLUGIN_PATH = 'APPRISE_PLUGIN_PATH' + +# Defines the over-ride path for the persistent storage +DEFAULT_ENV_APPRISE_STORAGE_PATH = 'APPRISE_STORAGE_PATH' + # Defines our click context settings adding -h to the additional options that # can be specified to get the help menu to come up CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @@ -496,11 +511,53 @@ def main(ctx, body, title, config, attach, urls, notification_type, theme, tag, # issue. For consistency, we also return a 2 ctx.exit(2) - if not plugin_path: - # Prepare a default set of plugin path - plugin_path = \ - [path for path in DEFAULT_PLUGIN_PATHS - if exists(path_decode(path))] + # + # Apply Environment Over-rides if defined + # + _config_paths = DEFAULT_CONFIG_PATHS + if 'APPRISE_CONFIG' in os.environ: + # Deprecate (this was from previous versions of Apprise <= 1.9.1) + logger.deprecate( + 'APPRISE_CONFIG environment variable has been changed to ' + f'{DEFAULT_ENV_APPRISE_CONFIG_PATH}') + logger.debug( + 'Loading provided APPRISE_CONFIG (deprecated) environment ' + 'variable') + _config_paths = (os.environ.get('APPRISE_CONFIG', '').strip(), ) + + elif DEFAULT_ENV_APPRISE_CONFIG_PATH in os.environ: + logger.debug( + f'Loading provided {DEFAULT_ENV_APPRISE_CONFIG_PATH} ' + 'environment variable') + _config_paths = re.split( + r'[\r\n;]+', os.environ.get( + DEFAULT_ENV_APPRISE_CONFIG_PATH).strip()) + + _plugin_paths = DEFAULT_PLUGIN_PATHS + if DEFAULT_ENV_APPRISE_PLUGIN_PATH in os.environ: + logger.debug( + f'Loading provided {DEFAULT_ENV_APPRISE_PLUGIN_PATH} environment ' + 'variable') + _plugin_paths = re.split( + r'[\r\n;]+', os.environ.get( + DEFAULT_ENV_APPRISE_PLUGIN_PATH).strip()) + + if DEFAULT_ENV_APPRISE_STORAGE_PATH in os.environ: + logger.debug( + f'Loading provided {DEFAULT_ENV_APPRISE_STORAGE_PATH} environment ' + 'variable') + storage_path = \ + os.environ.get(DEFAULT_ENV_APPRISE_STORAGE_PATH).strip() + + # + # Continue with initialization process + # + + # Prepare a default set of plugin paths to scan; anything specified + # on the CLI always trumps + plugin_paths = \ + [path for path in _plugin_paths if exists(path_decode(path))] \ + if not plugin_path else plugin_path if storage_uid_length < 2: click.echo( @@ -533,7 +590,7 @@ def main(ctx, body, title, config, attach, urls, notification_type, theme, tag, async_mode=disable_async is not True, # Load our plugins - plugin_paths=plugin_path, + plugin_paths=plugin_paths, # Load our persistent storage path storage_path=path_decode(storage_path), @@ -636,8 +693,7 @@ def main(ctx, body, title, config, attach, urls, notification_type, theme, tag, # 1. URLs by command line # 2. Configuration by command line # 3. URLs by environment variable: APPRISE_URLS - # 4. Configuration by environment variable: APPRISE_CONFIG - # 5. Default Configuration File(s) (if found) + # 4. Default Configuration File(s) # elif urls and not storage_action: if tag: @@ -662,8 +718,10 @@ def main(ctx, body, title, config, attach, urls, notification_type, theme, tag, a.add(AppriseConfig( paths=config, asset=asset, recursion=recursion_depth)) - elif os.environ.get('APPRISE_URLS', '').strip(): - logger.debug('Loading provided APPRISE_URLS environment variable') + elif os.environ.get(DEFAULT_ENV_APPRISE_URLS, '').strip(): + logger.debug( + f'Loading provided {DEFAULT_ENV_APPRISE_URLS} environment ' + 'variable') if tag: # Ignore any tags specified logger.warning( @@ -671,19 +729,12 @@ def main(ctx, body, title, config, attach, urls, notification_type, theme, tag, tag = None # Attempt to use our APPRISE_URLS environment variable (if populated) - a.add(os.environ['APPRISE_URLS'].strip()) - - elif os.environ.get('APPRISE_CONFIG', '').strip(): - logger.debug('Loading provided APPRISE_CONFIG environment variable') - # Fall back to config environment variable (if populated) - a.add(AppriseConfig( - paths=os.environ['APPRISE_CONFIG'].strip(), - asset=asset, recursion=recursion_depth)) + a.add(os.environ[DEFAULT_ENV_APPRISE_URLS].strip()) else: # Load default configuration a.add(AppriseConfig( - paths=[f for f in DEFAULT_CONFIG_PATHS if isfile(path_decode(f))], + paths=[f for f in _config_paths if isfile(path_decode(f))], asset=asset, recursion=recursion_depth)) if not dry_run and not (a or storage_action): diff --git a/apprise/utils.py b/apprise/utils.py index b33ad63a5..a45c6bbd2 100644 --- a/apprise/utils.py +++ b/apprise/utils.py @@ -29,10 +29,8 @@ import re import sys import json -import contextlib import os import binascii -import locale import platform import typing import base64 @@ -1518,39 +1516,6 @@ def cwe312_url(url): ) -@contextlib.contextmanager -def environ(*remove, **update): - """ - Temporarily updates the ``os.environ`` dictionary in-place. - - The ``os.environ`` dictionary is updated in-place so that the modification - is sure to work in all situations. - - :param remove: Environment variable(s) to remove. - :param update: Dictionary of environment variables and values to - add/update. - """ - - # Create a backup of our environment for restoration purposes - env_orig = os.environ.copy() - loc_orig = locale.getlocale() - try: - os.environ.update(update) - [os.environ.pop(k, None) for k in remove] - yield - - finally: - # Restore our snapshot - os.environ = env_orig.copy() - try: - # Restore locale - locale.setlocale(locale.LC_ALL, loc_orig) - - except locale.Error: - # Handle this case - pass - - def apply_template(template, app_mode=TemplateType.RAW, **kwargs): """ Takes a template in a str format and applies all of the keywords diff --git a/packaging/man/apprise.md b/packaging/man/apprise.md index 241407254..e9b44b84c 100644 --- a/packaging/man/apprise.md +++ b/packaging/man/apprise.md @@ -159,6 +159,9 @@ visit the [Apprise GitHub page][serviceurls] and see what's available. [serviceurls]: https://github.com/caronc/apprise/wiki#notification-services +The **environment variable** of `APPRISE_URLS` (comma/space delimited) can be specified to +provide the default set of URLs you wish to notify if none are otherwise specified. + ## EXAMPLES Send a notification to as many servers as you want to specify as you can @@ -215,8 +218,13 @@ files and loads them: ~/.config/apprise/plugins /var/lib/apprise/plugins +The **environment variable** of `APPRISE_PLUGIN_PATH` can be specified to override +the list identified above with one of your own. use a semi-colon (`;`), line-feed (`\n`), +and/or carriage return (`\r`) to delimit multiple entries. + Simply create your own python file with the following bare minimum content in it: + from apprise.decorators import notify # This example assumes you want your function to trigger on foobar:// @@ -263,6 +271,10 @@ in the following local locations for configuration files and loads them: The **configuration files** specified above can also be identified with a `.yml` extension or even just entirely removing the `.conf` extension altogether. +The **environment variable** of `APPRISE_CONFIG_PATH` can be specified to override +the list identified above with one of your own. use a semi-colon (`;`), line-feed (`\n`), +and/or carriage return (`\r`) to delimit multiple entries. + If a default configuration file is referenced in any way by the **apprise** tool, you no longer need to provide it a Service URL. Usage of the **apprise** tool simplifies to: @@ -281,6 +293,23 @@ configuration that you want and only specifically notify a subset of them: [tagging]: https://github.com/caronc/apprise/wiki/CLI_Usage#label-leverage-tagging [pstorage]: https://github.com/caronc/apprise/wiki/persistent_storage +## ENVIRONMENT VARIABLES + `APPRISE_URLS`: + Specify the default URLs to notify IF none are otherwise specified on the command line + explicitly. If the `--config` (`-c`) is specified, then this will over-rides any + reference to this variable. Use white space and/or a comma (`,`) to delimit multiple entries. + + `APPRISE_CONFIG_PATH`: + Explicitly specify the config search path to use (over-riding the default). + Use a semi-colon (`;`), line-feed (`\n`), and/or carriage return (`\r`) to delimit multiple entries. + + `APPRISE_PLUGIN_PATH`: + Explicitly specify the custom plugin search path to use (over-riding the default). + Use a semi-colon (`;`), line-feed (`\n`), and/or carriage return (`\r`) to delimit multiple entries. + + `APPRISE_STORAGE_PATH`: + Explicitly specify the persistent storage path to use (over-riding the default). + ## BUGS If you find any bugs, please make them known at: diff --git a/test/helpers/__init__.py b/test/helpers/__init__.py index 936f1a6a5..ce0d94305 100644 --- a/test/helpers/__init__.py +++ b/test/helpers/__init__.py @@ -29,9 +29,11 @@ from .rest import AppriseURLTester from .asyncio import OuterEventLoop from .module import reload_plugin +from .environment import environ __all__ = [ 'AppriseURLTester', 'OuterEventLoop', 'reload_plugin', + 'environ', ] diff --git a/test/helpers/environment.py b/test/helpers/environment.py new file mode 100644 index 000000000..3907023f8 --- /dev/null +++ b/test/helpers/environment.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron +# +# 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. +# +# 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 contextlib +import locale + +# Disable logging for a cleaner testing output +import logging +logging.disable(logging.CRITICAL) + + +@contextlib.contextmanager +def environ(*remove, **update): + """ + Temporarily updates the ``os.environ`` dictionary in-place. + + The ``os.environ`` dictionary is updated in-place so that the modification + is sure to work in all situations. + + :param remove: Environment variable(s) to remove. + :param update: Dictionary of environment variables and values to + add/update. + """ + + # Create a backup of our environment for restoration purposes + env_orig = os.environ.copy() + loc_orig = locale.getlocale() + try: + os.environ.update(update) + [os.environ.pop(k, None) for k in remove] + yield + + finally: + # Restore our snapshot + os.environ = env_orig.copy() + try: + # Restore locale + locale.setlocale(locale.LC_ALL, loc_orig) + + except locale.Error: + # Handle this case + pass diff --git a/test/test_apprise_cli.py b/test/test_apprise_cli.py index d671b8b7f..95e018153 100644 --- a/test/test_apprise_cli.py +++ b/test/test_apprise_cli.py @@ -40,7 +40,7 @@ from apprise import NotifyBase from apprise import NotificationManager from click.testing import CliRunner -from apprise.utils import environ +from helpers import environ from apprise.locale import gettext_lazy as _ from importlib import reload @@ -515,6 +515,22 @@ def url(self, *args, **kwargs): assert result.exit_code == 0 with environ(APPRISE_CONFIG=str(t2)): + # Deprecated test case + result = runner.invoke(cli.main, [ + '-b', 'has myTag', + '--tag', 'myTag', + ]) + assert result.exit_code == 0 + + with environ(APPRISE_CONFIG_PATH=str(t2)): + # Our configuration file will load from our environmment variable + result = runner.invoke(cli.main, [ + '-b', 'has myTag', + '--tag', 'myTag', + ]) + assert result.exit_code == 0 + + with environ(APPRISE_CONFIG_PATH=str(t2) + ';/another/path'): # Our configuration file will load from our environmment variable result = runner.invoke(cli.main, [ '-b', 'has myTag', @@ -677,6 +693,17 @@ def mywrapper(body, title, notify_type, *args, **kwargs): assert result.exit_code == 0 + with environ( + APPRISE_PLUGIN_PATH=str(notify_cmod) + ';' + str(notify_cmod2)): + # Leverage our environment variables to specify the plugin path + result = runner.invoke(cli.main, [ + '-b', 'body', + 'climod://', + 'climod2://', + ]) + + assert result.exit_code == 0 + @pytest.mark.skipif( sys.platform == "win32", reason="Unreliable results to be determined") diff --git a/test/test_apprise_helpers.py b/test/test_apprise_helpers.py new file mode 100644 index 000000000..047c91f04 --- /dev/null +++ b/test/test_apprise_helpers.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +# BSD 2-Clause License +# +# Apprise - Push Notification Library. +# Copyright (c) 2024, Chris Caron +# +# 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. +# +# 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 +import helpers + +# Disable logging for a cleaner testing output +import logging +logging.disable(logging.CRITICAL) + +# Ensure we don't create .pyc files for these tests +sys.dont_write_bytecode = True + + +def test_environ_temporary_change(): + """helpers: environ() testing + """ + # This is a helper function; but it does enough that we want to verify + # our usage of it works correctly; yes... we're testing a test + + e_key1 = 'APPRISE_TEMP1' + e_key2 = 'APPRISE_TEMP2' + e_key3 = 'APPRISE_TEMP3' + + e_val1 = 'ABCD' + e_val2 = 'DEFG' + e_val3 = 'HIJK' + + os.environ[e_key1] = e_val1 + os.environ[e_key2] = e_val2 + os.environ[e_key3] = e_val3 + + # Ensure our environment variable stuck + assert e_key1 in os.environ + assert e_val1 in os.environ[e_key1] + assert e_key2 in os.environ + assert e_val2 in os.environ[e_key2] + assert e_key3 in os.environ + assert e_val3 in os.environ[e_key3] + + with helpers.environ(e_key1, e_key3): + # Eliminates Environment Variable 1 and 3 + assert e_key1 not in os.environ + assert e_key2 in os.environ + assert e_val2 in os.environ[e_key2] + assert e_key3 not in os.environ + + # after with is over, environment is restored to normal + assert e_key1 in os.environ + assert e_val1 in os.environ[e_key1] + assert e_key2 in os.environ + assert e_val2 in os.environ[e_key2] + assert e_key3 in os.environ + assert e_val3 in os.environ[e_key3] + + d_key = 'APPRISE_NOT_SET' + n_key = 'APPRISE_NEW_KEY' + n_val = 'NEW_VAL' + + # Verify that our temporary variables (defined above) are not pre-existing + # environemnt variables as we'll be setting them below + assert n_key not in os.environ + assert d_key not in os.environ + + # makes it easier to pass in the arguments + updates = { + e_key1: e_val3, + e_key2: e_val1, + n_key: n_val, + } + with helpers.environ(d_key, e_key3, **updates): + # Attempt to eliminate an undefined key (silently ignored) + # Eliminates Environment Variable 3 + # Environment Variable 1 takes on the value of Env 3 + # Environment Variable 2 takes on the value of Env 1 + # Set a brand new variable that previously didn't exist + assert e_key1 in os.environ + assert e_val3 in os.environ[e_key1] + assert e_key2 in os.environ + assert e_val1 in os.environ[e_key2] + assert e_key3 not in os.environ + + # Can't delete a variable that doesn't exist; so we're in the same + # state here. + assert d_key not in os.environ + + # Our temporary variables will be found now + assert n_key in os.environ + assert n_val in os.environ[n_key] + + # after with is over, environment is restored to normal + assert e_key1 in os.environ + assert e_val1 in os.environ[e_key1] + assert e_key2 in os.environ + assert e_val2 in os.environ[e_key2] + assert e_key3 in os.environ + assert e_val3 in os.environ[e_key3] + + # Even our temporary variables are now missing + assert n_key not in os.environ + assert d_key not in os.environ diff --git a/test/test_apprise_translations.py b/test/test_apprise_translations.py index 8988fb1bd..15f56d0c3 100644 --- a/test/test_apprise_translations.py +++ b/test/test_apprise_translations.py @@ -34,7 +34,7 @@ import pytest from apprise import locale -from apprise.utils import environ +from helpers import environ from importlib import reload # Disable logging for a cleaner testing output diff --git a/test/test_apprise_utils.py b/test/test_apprise_utils.py index aa7be1792..17cd1b158 100644 --- a/test/test_apprise_utils.py +++ b/test/test_apprise_utils.py @@ -2545,93 +2545,6 @@ def test_apprise_validate_regex(): "- abcd -", r'-(?P[ABCD]+)-', None, fmt="{value}") is None -def test_environ_temporary_change(): - """utils: environ() testing - """ - - e_key1 = 'APPRISE_TEMP1' - e_key2 = 'APPRISE_TEMP2' - e_key3 = 'APPRISE_TEMP3' - - e_val1 = 'ABCD' - e_val2 = 'DEFG' - e_val3 = 'HIJK' - - os.environ[e_key1] = e_val1 - os.environ[e_key2] = e_val2 - os.environ[e_key3] = e_val3 - - # Ensure our environment variable stuck - assert e_key1 in os.environ - assert e_val1 in os.environ[e_key1] - assert e_key2 in os.environ - assert e_val2 in os.environ[e_key2] - assert e_key3 in os.environ - assert e_val3 in os.environ[e_key3] - - with utils.environ(e_key1, e_key3): - # Eliminates Environment Variable 1 and 3 - assert e_key1 not in os.environ - assert e_key2 in os.environ - assert e_val2 in os.environ[e_key2] - assert e_key3 not in os.environ - - # after with is over, environment is restored to normal - assert e_key1 in os.environ - assert e_val1 in os.environ[e_key1] - assert e_key2 in os.environ - assert e_val2 in os.environ[e_key2] - assert e_key3 in os.environ - assert e_val3 in os.environ[e_key3] - - d_key = 'APPRISE_NOT_SET' - n_key = 'APPRISE_NEW_KEY' - n_val = 'NEW_VAL' - - # Verify that our temporary variables (defined above) are not pre-existing - # environemnt variables as we'll be setting them below - assert n_key not in os.environ - assert d_key not in os.environ - - # makes it easier to pass in the arguments - updates = { - e_key1: e_val3, - e_key2: e_val1, - n_key: n_val, - } - with utils.environ(d_key, e_key3, **updates): - # Attempt to eliminate an undefined key (silently ignored) - # Eliminates Environment Variable 3 - # Environment Variable 1 takes on the value of Env 3 - # Environment Variable 2 takes on the value of Env 1 - # Set a brand new variable that previously didn't exist - assert e_key1 in os.environ - assert e_val3 in os.environ[e_key1] - assert e_key2 in os.environ - assert e_val1 in os.environ[e_key2] - assert e_key3 not in os.environ - - # Can't delete a variable that doesn't exist; so we're in the same - # state here. - assert d_key not in os.environ - - # Our temporary variables will be found now - assert n_key in os.environ - assert n_val in os.environ[n_key] - - # after with is over, environment is restored to normal - assert e_key1 in os.environ - assert e_val1 in os.environ[e_key1] - assert e_key2 in os.environ - assert e_val2 in os.environ[e_key2] - assert e_key3 in os.environ - assert e_val3 in os.environ[e_key3] - - # Even our temporary variables are now missing - assert n_key not in os.environ - assert d_key not in os.environ - - def test_apply_templating(): """utils: apply_template() testing """