-
Notifications
You must be signed in to change notification settings - Fork 2
Improved logging #252
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Improved logging #252
Changes from all commits
faf0680
c4270ea
ee58798
2f50619
38b0d63
bfee1b9
5f27b96
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| # Controlling Log Output | ||
|
|
||
| EasyScience uses Python's standard `logging` module for all | ||
| informational and warning messages. This gives you full control over | ||
| what output the library produces and where it goes. | ||
|
|
||
| ## Quick Start | ||
|
|
||
| The most common operation is suppressing all EasyScience messages. You | ||
| can do it in one line: | ||
|
|
||
| ```python | ||
| from easyscience import global_object | ||
|
|
||
| global_object.log.level = 'ERROR' | ||
| ``` | ||
|
|
||
| Or using the standard library directly: | ||
|
|
||
| ```python | ||
| import logging | ||
|
|
||
| logging.getLogger('easyscience').setLevel(logging.ERROR) | ||
| ``` | ||
|
|
||
| Both forms set the package-root logger to `ERROR`, which suppresses all | ||
| `WARNING`, `INFO`, and `DEBUG` messages from EasyScience. | ||
|
|
||
| ## Logger Hierarchy | ||
|
|
||
| EasyScience loggers are named hierarchically under the root | ||
| `easyscience` logger. This lets you suppress the whole package or | ||
| individual subsystems: | ||
|
|
||
| ``` | ||
| easyscience # root — controls everything | ||
| ├── easyscience.base_classes # EasyList runtime warnings | ||
| ├── easyscience.legacy # ObjBase / CollectionBase deprecations | ||
| ├── easyscience.deprecated # @deprecated decorator messages | ||
| ├── easyscience.fitting # import-availability warnings | ||
| │ ├── easyscience.fitting.bumps # Bumps fitting runtime messages | ||
| │ ├── easyscience.fitting.lmfit # LMFit fitting runtime messages | ||
| │ └── easyscience.fitting.dfo # DFO fitting runtime messages | ||
| ├── easyscience.variable # parameter warnings | ||
| └── easyscience.global_object # undo/redo, debugging diagnostics | ||
| ``` | ||
|
|
||
| Setting the level on a parent logger affects all its children, because | ||
| child loggers inherit the parent's effective level. | ||
|
|
||
| ```python | ||
| import logging | ||
|
|
||
| # Suppress everything from the fitting subsystem (including bumps/lmfit/dfo) | ||
| logging.getLogger('easyscience.fitting').setLevel(logging.ERROR) | ||
|
|
||
| # Suppress only Bumps runtime messages | ||
| logging.getLogger('easyscience.fitting.bumps').setLevel(logging.ERROR) | ||
|
|
||
| # Suppress only legacy deprecation notices | ||
| logging.getLogger('easyscience.legacy').setLevel(logging.ERROR) | ||
| ``` | ||
|
|
||
| ## Environment Variable | ||
|
|
||
| You can control the log level **before** importing EasyScience by | ||
| setting the `EASYSCIENCE_LOG_LEVEL` environment variable. This is useful | ||
| when a downstream library cannot configure logging in code before | ||
| EasyScience is imported. | ||
|
|
||
| **Accepted values:** `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`, or | ||
| a numeric level (e.g. `40`). | ||
|
|
||
| **Example — suppressing all import-time messages:** | ||
|
|
||
| ```powershell | ||
| # Windows PowerShell | ||
| $env:EASYSCIENCE_LOG_LEVEL = 'ERROR' | ||
| python -c "import easyscience" # silent | ||
| ``` | ||
|
|
||
| ```bash | ||
| # Linux / macOS | ||
| EASYSCIENCE_LOG_LEVEL=ERROR python -c "import easyscience" # silent | ||
| ``` | ||
|
|
||
| Or from Python, set the environment variable before the import: | ||
|
|
||
| ```python | ||
| import os | ||
|
|
||
| os.environ['EASYSCIENCE_LOG_LEVEL'] = 'ERROR' | ||
| import easyscience # now silent | ||
| ``` | ||
|
|
||
| ## Temporary Suppression (Context Manager) | ||
|
|
||
| When you only want to silence messages during a specific operation, use | ||
| the `at_level` context manager: | ||
|
|
||
| ```python | ||
| from easyscience import global_object | ||
| import logging | ||
|
|
||
| # Messages during the fit are suppressed, then the previous level is restored | ||
| with global_object.log.at_level(logging.ERROR): | ||
| results = fitter.fit(x, y, weights) | ||
| ``` | ||
|
|
||
| This is the recommended pattern for technique-specific libraries that | ||
| don't want EasyScience output to leak into their own users' consoles. | ||
|
|
||
| ## Convenience API | ||
|
|
||
| The `global_object.log` object exposes a `level` property plus | ||
| convenience methods that mirror `logging` module-level functions: | ||
|
|
||
| | Member | Description | | ||
| | ------------------ | ------------------------------------------------------------------------ | | ||
| | `.level` | Get/set the package-root logger level (`'WARNING'` or `logging.WARNING`) | | ||
| | `.debug(msg)` | Log a `DEBUG`-level message | | ||
| | `.info(msg)` | Log an `INFO`-level message | | ||
| | `.warning(msg)` | Log a `WARNING`-level message | | ||
| | `.error(msg)` | Log an `ERROR`-level message | | ||
| | `.critical(msg)` | Log a `CRITICAL`-level message | | ||
| | `.exception(msg)` | Log an `ERROR`-level message with traceback | | ||
| | `.getLogger(name)` | Get a child logger under `easyscience` | | ||
| | `.at_level(level)` | Context manager for temporary level change | | ||
| | `.suspend()` | Suppress all EasyScience output | | ||
| | `.resume()` | Restore the previously set level | | ||
|
|
||
| ## Recipes | ||
|
|
||
| ### Recipe 1: Keep your test output clean | ||
|
|
||
| When running tests that exercise EasyScience fitting, suppress the | ||
| library's messages so they don't clutter your test reports: | ||
|
|
||
| ```python | ||
| import logging | ||
| from easyscience import global_object | ||
|
|
||
| @pytest.fixture(autouse=True) | ||
| def _quiet_easyscience(): | ||
| # The test body runs *inside* the context manager (yield within `with`) | ||
| with global_object.log.at_level(logging.ERROR): | ||
| yield | ||
| ``` | ||
|
|
||
| ### Recipe 2: Library author embedding EasyScience | ||
|
|
||
| If you maintain a library that uses EasyScience internally, prevent | ||
| EasyScience from writing to your users' consoles: | ||
|
|
||
| ```python | ||
| # In your library's __init__.py, before any EasyScience import: | ||
| import os | ||
| os.environ.setdefault('EASYSCIENCE_LOG_LEVEL', 'ERROR') | ||
| ``` | ||
|
|
||
| ### Recipe 3: Debug mode for development | ||
|
|
||
| When developing or debugging, see all EasyScience internal diagnostics: | ||
|
|
||
| ```python | ||
| from easyscience import global_object | ||
|
|
||
| global_object.log.level = 'DEBUG' | ||
|
|
||
| # Your code here — all EasyScience messages will be visible | ||
| ``` | ||
|
|
||
| ### Recipe 4: See only error messages | ||
|
|
||
| Keep the console clean but still see critical problems: | ||
|
|
||
| ```python | ||
| from easyscience import global_object | ||
|
|
||
| global_object.log.level = 'ERROR' | ||
| ``` | ||
|
|
||
| ## Library-Safe Behaviour | ||
|
|
||
| EasyScience follows standard library-logging best practices: | ||
|
|
||
| - **No `logging.basicConfig()`** — EasyScience never reconfigures the | ||
| global logging setup. | ||
| - **No default stream handlers** — EasyScience does not attach handlers | ||
| that write to `stdout` or `stderr`. It only creates log records. | ||
| Applications and test frameworks decide where those records go. | ||
|
Comment on lines
+189
to
+191
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this bad? What about users that use
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Python's |
||
| - **Child loggers inherit** — Child loggers (e.g. | ||
| `easyscience.fitting.bumps`) are left at `logging.NOTSET` by default, | ||
| so they inherit level and handler configuration from the `easyscience` | ||
| package-root logger. Supressing the root suppresses everything. | ||
|
|
||
| This means you are in full control. If you don't configure any handlers, | ||
| Python's built-in `logging.lastResort` fallback still prints `WARNING` | ||
| and above to `stderr`, so standalone users see important messages out of | ||
| the box. `INFO` and `DEBUG` messages remain hidden until you opt in. If | ||
| you do configure handlers (as `pytest` does via its logging plugin, or | ||
| as an application might via `basicConfig`), you control what is shown | ||
| and where. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,13 +7,12 @@ | |
| Please update your imports. | ||
| """ | ||
|
|
||
| import warnings | ||
| from easyscience import global_object | ||
|
|
||
| from ..legacy.collection_base import CollectionBase # noqa: F401 | ||
|
|
||
| warnings.warn( | ||
| global_object.log.warning( | ||
| 'easyscience.base_classes.collection_base is deprecated. ' | ||
| 'Please import from easyscience.legacy.collection_base instead.', | ||
| DeprecationWarning, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So with the new logger, we lose the ability to categorize warnings?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is an issue. |
||
| stacklevel=2, | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,11 @@ | ||
| # SPDX-FileCopyrightText: 2024 EasyScience contributors <https://github.com/easyscience> | ||
| # SPDX-License-Identifier: BSD-3-Clause | ||
|
|
||
| import warnings | ||
| from dataclasses import dataclass | ||
| from enum import Enum | ||
|
|
||
| from easyscience import global_object | ||
|
|
||
| # Change to importlib.metadata when Python 3.10 is the minimum version | ||
| # import importlib.metadata | ||
| # installed_packages = [x.name for x in importlib.metadata.distributions()] | ||
|
|
@@ -15,11 +16,8 @@ | |
|
|
||
| lmfit_engine_available = True | ||
| except ImportError: | ||
| # TODO make this a proper message (use logging?) | ||
| warnings.warn( | ||
| 'LMFit minimization is not available. Probably lmfit has not been installed.', | ||
| ImportWarning, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another category lost |
||
| stacklevel=2, | ||
| global_object.log.getLogger('fitting').warning( | ||
| 'LMFit minimization is not available. Probably lmfit has not been installed.' | ||
| ) | ||
|
|
||
| bumps_engine_available = False | ||
|
|
@@ -28,11 +26,8 @@ | |
|
|
||
| bumps_engine_available = True | ||
| except ImportError: | ||
| # TODO make this a proper message (use logging?) | ||
| warnings.warn( | ||
| 'Bumps minimization is not available. Probably bumps has not been installed.', | ||
| ImportWarning, | ||
| stacklevel=2, | ||
| global_object.log.getLogger('fitting').warning( | ||
| 'Bumps minimization is not available. Probably bumps has not been installed.' | ||
| ) | ||
|
|
||
| dfo_engine_available = False | ||
|
|
@@ -41,11 +36,8 @@ | |
|
|
||
| dfo_engine_available = True | ||
| except ImportError: | ||
| # TODO make this a proper message (use logging?) | ||
| warnings.warn( | ||
| 'DFO minimization is not available. Probably dfols has not been installed.', | ||
| ImportWarning, | ||
| stacklevel=2, | ||
| global_object.log.getLogger('fitting').warning( | ||
| 'DFO minimization is not available. Probably dfols has not been installed.' | ||
| ) | ||
|
|
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this work? You're only muting the log in the context, so how would this actually mute the log in a test? Wouldn't
Be better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
autousefixtureyields insidewith at_level(logging.ERROR), so the test body runs within the context and is muted. Yourset_level('ERROR')also works and is marginally simpler, but the code is correct