Repository: IAMconsortium/pyam
Pull request #288 (2019-11-10 03:08:30)

Update logging
# Please confirm that this PR has done the following:

- [x] Tests Added
- [ ] Documentation Added
- [x] Description in RELEASE_NOTES.md Added

## Adding to RELEASE_NOTES.md (remove section after adding to RELEASE_NOTES.md)

Please add a single line in the release notes similar to the following:

```
- (#XX)[http://link-to-pr.com] Added feature which does something
```

# Description of PR

This PR updates pyam's logging so that it no longer clashes with the root logger and is properly namespaced. At the moment, pyam uses the root logger, which means you can't only turn Pyam's logging off rather you have to adjust all logging or none (see the new tests). With this change, pyam's loggers are put in their own namespace so they can be adjusted by libraries which use them without having to touch the root logger. The more detailed explanation of why this should be changed is explained [here](https://docs.python-guide.org/writing/logging/#logging-in-a-library) ([this](https://opensource.com/article/17/9/python-logging) could also be a useful resource if we want to write something about this in the docs).


znicholls: @gidden does this need documentation?
coveralls: 
[![Coverage Status](https://coveralls.io/builds/26883538/badge)](https://coveralls.io/builds/26883538)

Coverage decreased (-0.3%) to 85.051% when pulling **f03ce360072a426d08bb9ef45a11a9837d867185 on znicholls:update-logging** into **a4e266c93f531f94285311fd354aaa881aa403c4 on IAMconsortium:master**.


Patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index bf6064d..faefb12 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,6 +1,8 @@
 
 # Next Release
 
+- [#288](https://github.com/IAMconsortium/pyam/pull/288) Put `pyam` logger in its own namespace (see [here](https://docs.python-guide.org/writing/logging/#logging-in-a-library>))
+
 # Release v0.3.0
 
 ## Highlights
@@ -23,7 +25,7 @@
 - [#243](https://github.com/IAMconsortium/pyam/pull/243) Update `pyam.iiasa.Connection` to support all public and private database connections. DEPRECATED: the argument 'iamc15' has been deprecated in favor of names as queryable directly from the REST API.
 - [#241](https://github.com/IAMconsortium/pyam/pull/241) Add `set_meta_from_data` feature
 - [#236](https://github.com/IAMconsortium/pyam/pull/236) Add `swap_time_for_year` method and confirm datetime column is compatible with pyam features
-- [#273](https://github.com/IAMconsortium/pyam/pull/273) Fix several issues accessing IXMP API (passing correct credentials, improve reliability for optional fields in result payload) 
+- [#273](https://github.com/IAMconsortium/pyam/pull/273) Fix several issues accessing IXMP API (passing correct credentials, improve reliability for optional fields in result payload)
 
 # Release v0.2.0
 
diff --git a/RELEASE_PROCEDURE.md b/RELEASE_PROCEDURE.md
index acb02d1..57b2427 100644
--- a/RELEASE_PROCEDURE.md
+++ b/RELEASE_PROCEDURE.md
@@ -1,38 +1,51 @@
 
-# Steps required for a release
+# Release procedure
 
-1. Make a release candidate branch and pull request it into master with the following updates:
-   1. Deprecate any stated portion of the API (you can find them by searching the code base for "deprecate")
-   1. Update `RELEASE_NOTES.md` (see the examples of other releases)
-	  - create new section under "# Next Release" with "# Release v<release version>"
-	  - For this new release, add a "Highlights section" with some of the most important updates
-      - Make a new heading "## Individual Updates" before the PR listing
+## Required accounts and admin privileges
+
+- pip: https://pypi.org/project/pyam-iamc/ and https://test.pypi.org/project/pyam-iamc/
+- conda: https://github.com/conda-forge/pyam-feedstock/
+- rtd: https://readthedocs.org/projects/pyam-iamc/
+
+## Steps for publishing a new release
+
+1. Make a release candidate branch (e.g., `rc_v<release version>`)
+   and pull request it into `master` with the following updates:
+   1. Deprecate any stated portion of the API
+      (you can find them by searching the code base for "deprecate")
+   1. Update `RELEASE_NOTES.md` (see the examples of previous releases)
+	  - replace "# Next Release" with "# Release v<release version>"
+	  - for this new release, add a "## Highlights" section with the most important updates & changes
+      - add a new heading "## Individual Updates" before the PR listing
   1. Confirm that the PR passes all tests and checks
   1. Tag the release number: `git tag v<release version>`, e.g., `git tag v1.2.0`
      - **THIS IS NOT THE TAGGED COMMIT WE WILL DISTRIBUTE, IT IS ONLY FOR TESTING**
 	 - **DO NOT PUSH THIS TAG TO UPSTREAM**
   1. Run `make publish-on-testpypi`
-     - this should "just work" if it does not, fix any issues, retag (`git tag
-       -d` then `git tag`), and try again
-	 - note, you need an account on https://test.pypi.org
-  1. Once successful, delete the tag, and merge the candidate PR into master on Github
-1. Switch to now updated master branch: `git checkout master` and `git pull upstream master`
+     - this should "just work" - if it does not, fix any issues,
+       retag (`git tag -d` then `git tag`), and try again
+  1. Once successful, delete the tag, and merge the candidate PR into `master` on Github
+1. Switch to the now-updated master branch: `git checkout master` and `git pull upstream master`
 1. Tag the release number: `git tag v<release version>`, e.g., `git tag v1.2.0`
    - `versioneer` automatically updates the version number based on the tag
    - this is now the official tagged commit
 1. Push the tag upstream: `git push upstream --tags`
 1. Run `make publish-on-pypi`
-   - note, you need an account on https://pypi.org
-   - this will make wheels that all us to be installed via `pip install`
+   - this will make wheels that allow `pyam` to be installed via `pip install`
+   - check that the new version is available at https://pypi.org/project/pyam-iamc/
 1. Make a new release on Github
-   - Make sure that you choose the same tag name as was used earlier
-   - Copy the markdown from `RELEASE_NOTES.md` into the release description box
-1. Update on conda-forge:
-   - A PR should automatically be opened by the bot after the Github release
+   - make sure that you choose the tag name defined above
+   - copy the release summary from `RELEASE_NOTES.md` into the description box
+1. Update on `conda-forge`
+   - a PR should automatically be opened by the bot after the Github release
    - confirm that any new depedencies are added there
+   - merge the PR
+   - check that the new version is available at https://anaconda.org/conda-forge/pyam
+1. Confirm that the doc pages are updated on https://pyam-iamc.readthedocs.io/
+   - both the **latest** and the **stable** versions point to the new release
+   - the new release has been added to the list of available versions
+1. Add a new line "# Next Release" at the top of `RELEASE_NOTES.md` and commit to `master`
 1. Announce it on our mailing list: https://groups.io/g/pyam
-   - Again, just copy the now rendered HTML from the Github release directly in
-     the email
-1. Confirm that the doc page is updated to the latest release: https://pyam-iamc.readthedocs.io/
+   - again, copy the rendered HTML from the Github release directly in the email
 
 And that's it! Whew...
diff --git a/pyam/__init__.py b/pyam/__init__.py
index 5300ac7..e412360 100644
--- a/pyam/__init__.py
+++ b/pyam/__init__.py
@@ -1,9 +1,11 @@
+import logging
+
 from pyam.core import *
 from pyam.utils import *
 from pyam.statistics import *
 from pyam.timeseries import *
 from pyam.read_ixmp import *
-from pyam.logger import *
+from pyam.logging import *
 from pyam.run_control import *
 from pyam.iiasa import read_iiasa  # noqa: F401
 
@@ -23,3 +25,5 @@ except Exception:
 from ._version import get_versions
 __version__ = get_versions()['version']
 del get_versions
+
+logging.getLogger(__name__).addHandler(logging.NullHandler())
diff --git a/pyam/core.py b/pyam/core.py
index c912587..d5787f1 100644
--- a/pyam/core.py
+++ b/pyam/core.py
@@ -1,6 +1,7 @@
 import copy
 import importlib
 import itertools
+import logging
 import os
 import sys
 
@@ -15,7 +16,7 @@ except ImportError:
 
 from pyam import plotting
 
-from pyam.logger import logger, adjust_log_level
+from pyam.logging import adjust_log_level
 from pyam.run_control import run_control
 from pyam.utils import (
     write_sheet,
@@ -41,6 +42,8 @@ from pyam.utils import (
 from pyam.read_ixmp import read_ix
 from pyam.timeseries import fill_series
 
+logger = logging.getLogger(__name__)
+
 
 class IamDataFrame(object):
     """This class is a wrapper for dataframes following the IAMC format.
@@ -512,14 +515,14 @@ class IamDataFrame(object):
         idx = _meta_idx(rows)
 
         if len(idx) == 0:
-            logger().info("No scenarios satisfy the criteria")
+            logger.info("No scenarios satisfy the criteria")
             return  # EXIT FUNCTION
 
         # update metadata dataframe
         self._new_meta_column(name)
         self.meta.loc[idx, name] = value
         msg = '{} scenario{} categorized as `{}: {}`'
-        logger().info(msg.format(len(idx), '' if len(idx) == 1 else 's',
+        logger.info(msg.format(len(idx), '' if len(idx) == 1 else 's',
                                  name, value))
 
     def _new_meta_column(self, name):
@@ -555,7 +558,7 @@ class IamDataFrame(object):
 
         n = len(idx)
         if n == 0:
-            logger().info('All scenarios have the required variable `{}`'
+            logger.info('All scenarios have the required variable `{}`'
                           .format(variable))
             return
 
@@ -566,7 +569,7 @@ class IamDataFrame(object):
             self.meta.loc[idx, 'exclude'] = True
             msg += ', marked as `exclude: True` in metadata'
 
-        logger().info(msg.format(n, variable))
+        logger.info(msg.format(n, variable))
         return pd.DataFrame(index=idx).reset_index()
 
     def validate(self, criteria={}, exclude_on_fail=False):
@@ -590,7 +593,7 @@ class IamDataFrame(object):
 
         if not df.empty:
             msg = '{} of {} data points do not satisfy the criteria'
-            logger().info(msg.format(len(df), len(self.data)))
+            logger.info(msg.format(len(df), len(self.data)))
 
             if exclude_on_fail and len(df) > 0:
                 self._exclude_on_fail(df)
@@ -751,7 +754,7 @@ class IamDataFrame(object):
 
         if not len(components):
             msg = 'cannot aggregate variable `{}` because it has no components'
-            logger().info(msg.format(variable))
+            logger.info(msg.format(variable))
 
             return
 
@@ -796,7 +799,7 @@ class IamDataFrame(object):
 
         if len(diff):
             msg = '`{}` - {} of {} rows are not aggregates of components'
-            logger().info(msg.format(variable, len(diff), len(df_variable)))
+            logger.info(msg.format(variable, len(diff), len(df_variable)))
 
             if exclude_on_fail:
                 self._exclude_on_fail(diff.index.droplevel([2, 3, 4]))
@@ -832,7 +835,7 @@ class IamDataFrame(object):
         if not len(subregions):
             msg = 'cannot aggregate variable `{}` to `{}` because it does not'\
                   ' exist in any subregion'
-            logger().info(msg.format(variable, region))
+            logger.info(msg.format(variable, region))
 
             return
 
@@ -843,7 +846,7 @@ class IamDataFrame(object):
 
         # add components at the `region` level, defaults to all variables one
         # level below `variable` that are only present in `region`
-        with adjust_log_level():
+        with adjust_log_level(logger):
             region_df = self.filter(region=region)
 
         rdf_comps = region_df._variable_components(variable, level=None)
@@ -891,7 +894,7 @@ class IamDataFrame(object):
         rows = self._apply_filters(region=region, variable=variable)
         if not rows.any():
             msg = 'variable `{}` does not exist in region `{}`'
-            logger().info(msg.format(variable, region))
+            logger.info(msg.format(variable, region))
             return
 
         df_region, df_subregions = (
@@ -906,7 +909,7 @@ class IamDataFrame(object):
             msg = (
                 '`{}` - {} of {} rows are not aggregates of subregions'
             )
-            logger().info(msg.format(variable, len(diff), len(df_region)))
+            logger.info(msg.format(variable, len(diff), len(df_region)))
 
             if exclude_on_fail:
                 self._exclude_on_fail(diff.index.droplevel([2, 3]))
@@ -958,7 +961,7 @@ class IamDataFrame(object):
         """Assign a selection of scenarios as `exclude: True` in meta"""
         idx = df if isinstance(df, pd.MultiIndex) else _meta_idx(df)
         self.meta.loc[idx, 'exclude'] = True
-        logger().info('{} non-valid scenario{} will be excluded'
+        logger.info('{} non-valid scenario{} will be excluded'
                       .format(len(idx), '' if len(idx) == 1 else 's'))
 
     def filter(self, keep=True, inplace=False, **kwargs):
@@ -995,7 +998,7 @@ class IamDataFrame(object):
 
         idx = _make_index(ret.data)
         if len(idx) == 0:
-            logger().warning('Filtered IamDataFrame is empty!')
+            logger.warning('Filtered IamDataFrame is empty!')
         ret.meta = ret.meta.loc[idx]
         if not inplace:
             return ret
@@ -1177,7 +1180,7 @@ class IamDataFrame(object):
         n_invalid = len(df) - len(idx)
         if n_invalid > 0:
             msg = 'Ignoring {} scenario{} from imported metadata'
-            logger().info(msg.format(n_invalid, 's' if n_invalid > 1 else ''))
+            logger.info(msg.format(n_invalid, 's' if n_invalid > 1 else ''))
 
         if idx.empty:
             raise ValueError('No valid scenarios in imported metadata file!')
@@ -1186,7 +1189,7 @@ class IamDataFrame(object):
 
         # Merge in imported metadata
         msg = 'Importing metadata for {} scenario{} (for total of {})'
-        logger().info(msg.format(len(df), 's' if len(df) > 1 else '',
+        logger.info(msg.format(len(df), 's' if len(df) > 1 else '',
                                  len(self.meta)))
 
         for col in df.columns:
@@ -1342,7 +1345,7 @@ class IamDataFrame(object):
                 # find duplicates
                 where_dup = _map['region'].duplicated(keep=False)
                 dups = _map[where_dup]
-                logger().warning("""
+                logger.warning("""
                 Duplicate entries found for the following regions.
                 Mapping will occur only for the most common instance.
                 {}""".format(dups['region'].unique()))
diff --git a/pyam/iiasa.py b/pyam/iiasa.py
index 2039336..d52015a 100644
--- a/pyam/iiasa.py
+++ b/pyam/iiasa.py
@@ -15,9 +15,9 @@ except ImportError:
     from functools32 import lru_cache
 
 from pyam.core import IamDataFrame
-from pyam.logger import logger
 from pyam.utils import META_IDX, islistable, isstr, pattern_match
 
+logger = logging.getLogger(__name__)
 # quiet this fool
 logging.getLogger('requests').setLevel(logging.WARNING)
 
@@ -139,7 +139,7 @@ class Connection(object):
         # TODO: request the full citation to be added to this metadata intead
         # of linking to the about page
         about = '/'.join([response[idxs['uiUrl']]['value'], '#', 'about'])
-        logger().info(_CITE_MSG.format(name, about))
+        logger.info(_CITE_MSG.format(name, about))
 
         self._connected = name
 
@@ -325,13 +325,13 @@ class Connection(object):
         }
         data = json.dumps(self._query_post_data(**kwargs))
         url = '/'.join([self._base_url, 'runs/bulk/ts'])
-        logger().debug('Querying timeseries data '
+        logger.debug('Querying timeseries data '
                        'from {} with filter {}'.format(url, data))
         r = requests.post(url, headers=headers, data=data)
         _check_response(r)
         # refactor returned json object to be castable to an IamDataFrame
         df = pd.read_json(r.content, orient='records')
-        logger().debug('Response size is {0} bytes, '
+        logger.debug('Response size is {0} bytes, '
                        '{1} records'.format(len(r.content), len(df)))
         columns = ['model', 'scenario', 'variable', 'unit',
                    'region', 'year', 'value', 'time', 'meta',
diff --git a/pyam/logger.py b/pyam/logger.py
deleted file mode 100644
index 4ed5432..0000000
--- a/pyam/logger.py
+++ /dev/null
@@ -1,25 +0,0 @@
-import logging
-from contextlib import contextmanager
-
-# globally accessible logger
-_LOGGER = None
-
-
-def logger():
-    """Access global logger"""
-    global _LOGGER
-    if _LOGGER is None:
-        logging.basicConfig()
-        _LOGGER = logging.getLogger()
-        _LOGGER.setLevel('INFO')
-    return _LOGGER
-
-
-@contextmanager
-def adjust_log_level(level='ERROR'):
-    """Context manager to change log level"""
-    _logger = logger()
-    old_level = _logger.getEffectiveLevel()
-    _logger.setLevel(level)
-    yield
-    _logger.setLevel(old_level)
diff --git a/pyam/logging.py b/pyam/logging.py
new file mode 100644
index 0000000..effcc56
--- /dev/null
+++ b/pyam/logging.py
@@ -0,0 +1,10 @@
+from contextlib import contextmanager
+
+
+@contextmanager
+def adjust_log_level(logger, level='ERROR'):
+    """Context manager to change log level"""
+    old_level = logger.getEffectiveLevel()
+    logger.setLevel(level)
+    yield
+    logger.setLevel(old_level)
diff --git a/pyam/plotting.py b/pyam/plotting.py
index 97d3472..93c4136 100644
--- a/pyam/plotting.py
+++ b/pyam/plotting.py
@@ -1,4 +1,5 @@
 import itertools
+import logging
 import warnings
 
 import matplotlib.pyplot as plt
@@ -16,7 +17,6 @@ try:
 except ImportError:
     from functools32 import lru_cache
 
-from pyam.logger import logger
 from pyam.run_control import run_control
 from pyam.utils import requires_package, IAMC_IDX, SORT_IDX, isstr
 # TODO: this is a hotfix for changes in pandas 0.25.0, per discussions on the
@@ -24,6 +24,8 @@ from pyam.utils import requires_package, IAMC_IDX, SORT_IDX, isstr
 # standard feature in their library
 from pyam._style import _get_standard_colors
 
+logger = logging.getLogger(__name__)
+
 # line colors, markers, and styles that are cycled through when not
 # explicitly declared
 _DEFAULT_PROPS = None
@@ -805,7 +807,7 @@ def line_plot(df, x='year', y='value', ax=None, legend=None, title=True,
 
 def _add_legend(ax, handles, labels, legend):
     if legend is None and len(labels) >= MAX_LEGEND_LABELS:
-        logger().info('>={} labels, not applying legend'.format(
+        logger.info('>={} labels, not applying legend'.format(
             MAX_LEGEND_LABELS))
     else:
         legend = {} if legend in [True, None] else legend
diff --git a/pyam/timeseries.py b/pyam/timeseries.py
index ef2d8ed..83f724f 100644
--- a/pyam/timeseries.py
+++ b/pyam/timeseries.py
@@ -1,9 +1,10 @@
 # -*- coding: utf-8 -*-
-
+import logging
 import numpy as np
-from pyam.logger import logger
 from pyam.utils import isstr, to_int
 
+logger = logging.getLogger(__name__)
+
 # %%
 
 
@@ -51,11 +52,11 @@ def cumulative(x, first_year, last_year):
     # if the timeseries does not cover the range `[first_year, last_year]`,
     # return nan to avoid erroneous aggregation
     if min(x.index) > first_year:
-        logger().warning('the timeseries `{}` does not start by {}'.format(
+        logger.warning('the timeseries `{}` does not start by {}'.format(
             x.name or x, first_year))
         return np.nan
     if max(x.index) < last_year:
-        logger().warning('the timeseries `{}` does not extend until {}'
+        logger.warning('the timeseries `{}` does not extend until {}'
                          .format(x.name or x, last_year))
         return np.nan
 
diff --git a/pyam/utils.py b/pyam/utils.py
index 3d725f3..75b9c18 100644
--- a/pyam/utils.py
+++ b/pyam/utils.py
@@ -1,5 +1,6 @@
 import os
 import itertools
+import logging
 import string
 import six
 import re
@@ -17,7 +18,7 @@ try:
 except ImportError:
     pass
 
-from pyam.logger import logger
+logger = logging.getLogger(__name__)
 
 # common indicies
 META_IDX = ['model', 'scenario']
@@ -117,7 +118,7 @@ def read_file(fname, *args, **kwargs):
     if not isstr(fname):
         raise ValueError('reading multiple files not supported, '
                          'please use `pyam.IamDataFrame.append()`')
-    logger().info('Reading `{}`'.format(fname))
+    logger.info('Reading `{}`'.format(fname))
     format_kwargs = {}
     # extract kwargs that are intended for `format_data`
     for c in [i for i in IAMC_IDX + ['year', 'time', 'value'] if i in kwargs]:
@@ -185,7 +186,7 @@ def format_data(df, **kwargs):
     df.rename(columns={c: str(c).lower() for c in str_cols}, inplace=True)
 
     if 'notes' in df.columns:  # this came from the database
-        logger().info('Ignoring notes column in dataframe')
+        logger.info('Ignoring notes column in dataframe')
         df.drop(columns='notes', inplace=True)
         col = df.columns[0]  # first column has database copyright notice
         df = df[~df[col].str.contains('database', case=False)]


Test patch:
diff --git a/tests/test_feature_aggregate.py b/tests/test_feature_aggregate.py
index 506cd40..31f4cd3 100644
--- a/tests/test_feature_aggregate.py
+++ b/tests/test_feature_aggregate.py
@@ -90,7 +90,7 @@ def test_check_internal_consistency_no_world_for_variable(
     test_df = check_aggregate_df.filter(
         variable='Emissions|CH4', region='World', keep=False
     )
-    caplog.set_level(logging.INFO)
+    caplog.set_level(logging.INFO, logger="pyam.core")
     test_df.check_internal_consistency()
     warn_idx = caplog.messages.index("variable `Emissions|CH4` does not exist "
                                      "in region `World`")
@@ -306,6 +306,7 @@ def test_aggregate_region_components_handling(check_aggregate_regional_df,
 
 def test_check_aggregate_region_no_world(check_aggregate_regional_df, caplog):
     test_df = check_aggregate_regional_df.filter(region='World', keep=False)
+    caplog.set_level(logging.INFO, logger="pyam.core")
     test_df.check_aggregate_region('Emissions|N2O', region='World')
     warn_idx = caplog.messages.index("variable `Emissions|N2O` does not exist "
                                      "in region `World`")
diff --git a/tests/test_logger.py b/tests/test_logger.py
index 3c52cf0..120d393 100644
--- a/tests/test_logger.py
+++ b/tests/test_logger.py
@@ -1,18 +1,38 @@
+import logging
 import sys
 
-from pyam.logger import logger, adjust_log_level
 
+def test_logger_namespacing(test_df, caplog):
+    with caplog.at_level(logging.INFO, logger="pyam.core"):
+        test_df.filter(model="junk")
 
-def test_context_adjust_log_level():
-    assert logger().getEffectiveLevel() == 20
-    with adjust_log_level():
-        assert logger().getEffectiveLevel() == 40
-    assert logger().getEffectiveLevel() == 20
+    assert caplog.record_tuples == [(
+        "pyam.core",  # namespacing
+        logging.WARNING,  # level
+        "Filtered IamDataFrame is empty!",  # message
+    )]
 
 
-def test_adjusting_level_for_not_initialized_logger():
-    # de-initialize logger to simulate adjust_log_level called before logger
-    pyam_logger = sys.modules['pyam.logger']
-    pyam_logger._LOGGER = None
-    with adjust_log_level():
-        pass
+def test_adjusting_logger_level(test_df, caplog):
+    def throw_warning():
+        logging.warning("This is a root warning")
+
+    with caplog.at_level(logging.INFO, logger="pyam.core"):
+        test_df.filter(model="junk")
+        throw_warning()
+
+    assert caplog.record_tuples == [
+        ("pyam.core", logging.WARNING, "Filtered IamDataFrame is empty!"),
+        ("root", logging.WARNING, "This is a root warning"),
+    ]
+
+    caplog.clear()
+    with caplog.at_level(logging.ERROR, logger="pyam.core"):
+        test_df.filter(model="junk")
+        throw_warning()
+
+    # only the root warning should come through now i.e. we can silence pyam
+    # without silencing everything
+    assert caplog.record_tuples == [
+        ("root", logging.WARNING, "This is a root warning"),
+    ]
diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py
index decda35..90262b3 100644
--- a/tests/test_timeseries.py
+++ b/tests/test_timeseries.py
@@ -1,9 +1,9 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
+import logging
 
 import numpy as np
 import pandas as pd
-from pyam.logger import logger
 from pyam import fill_series, cumulative, cross_threshold, to_int
 import pytest
 
@@ -32,11 +32,11 @@ def test_cumulative():
 
 def test_cumulative_out_of_range():
     # set logger level to exclude warnings in unit test output
-    logger().setLevel('ERROR')
+    logging.getLogger("pyam").setLevel('ERROR')
     # note that the series is not order and the index is defined as float
     y = pd.Series(data=[np.nan, 1, 3, 1], index=[2002., 2005., 2007., 2013.])
     assert cumulative(y, 2008, 2015) is np.nan
-    logger().setLevel('NOTSET')
+    logging.getLogger("pyam").setLevel('NOTSET')
 
 
 def test_cross_treshold():