Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 200 additions & 0 deletions docs/docs/user-guide/controlling-log-output.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# 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.set_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 convenience methods that mirror
`logging` module-level functions:

| Method | Description |
| ------------------- | -------------------------------------------------------------------- |
| `.set_level(level)` | 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():
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
import logging

global_object.log.set_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.set_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.
- **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,
EasyScience messages will not appear. 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.
6 changes: 4 additions & 2 deletions docs/docs/user-guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ icon: material/book-open-variant

# :material-book-open-variant: User Guide

This section is currently under development. Please check back later for
updates.
- [:material-volume-high: Controlling Log Output](controlling-log-output.md)
– How to suppress, filter, or redirect the messages that EasyScience
produces. Covers the `EASYSCIENCE_LOG_LEVEL` environment variable, the
logger hierarchy, context managers, and common recipes.
1 change: 1 addition & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ nav:
- Installation & Setup: installation-and-setup/index.md
- User Guide:
- User Guide: user-guide/index.md
- Controlling Log Output: user-guide/controlling-log-output.md
- Tutorials:
- Tutorials: tutorials/index.md
- Workshops & Schools:
Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -211,8 +211,7 @@ select = [
# Ignore specific rules globally
ignore = [
'COM812', # https://docs.astral.sh/ruff/rules/missing-trailing-comma/
# The following is replaced by 'D'/[tool.ruff.lint.pydocstyle] and [tool.pydoclint]
'DOC', # https://docs.astral.sh/ruff/rules/#pydoclint-doc
# The following is replaced by 'D'/[tool.ruff.lint.pydocstyle] and [tool.pydoclint] 'DOC', # https://docs.astral.sh/ruff/rules/#pydoclint-doc
# Disable, as [tool.format_docstring] split one-line docstrings into the canonical multi-line layout
'D200', # https://docs.astral.sh/ruff/rules/unnecessary-multiline-docstring/
]
Expand Down
8 changes: 3 additions & 5 deletions src/easyscience/base_classes/collection_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@
Please update your imports.
"""

import warnings
import logging

from ..legacy.collection_base import CollectionBase # noqa: F401

warnings.warn(
logging.getLogger('easyscience').warning(
'easyscience.base_classes.collection_base is deprecated. '
'Please import from easyscience.legacy.collection_base instead.',
DeprecationWarning,
stacklevel=2,
'Please import from easyscience.legacy.collection_base instead.'
)
8 changes: 4 additions & 4 deletions src/easyscience/base_classes/easy_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from __future__ import annotations

import copy
import warnings
import logging
from collections.abc import MutableSequence
from typing import Any
from typing import Callable
Expand Down Expand Up @@ -159,7 +159,7 @@ def __setitem__(
if not isinstance(value, tuple(self._protected_types)):
raise TypeError(f'Items must be one of {self._protected_types}, got {type(value)}')
if value is not self._data[idx] and value in self:
warnings.warn(
logging.getLogger('easyscience.base_classes').warning(
f'Item with unique name "{self._get_key(value)}" already in EasyList, it will be ignored'
)
return
Expand All @@ -177,7 +177,7 @@ def __setitem__(
if not isinstance(v, tuple(self._protected_types)):
raise TypeError(f'Items must be one of {self._protected_types}, got {type(v)}')
if v in self and replaced[i] is not v:
warnings.warn(
logging.getLogger('easyscience.base_classes').warning(
f'Item with unique name "{v.unique_name}" already in EasyList, it will be ignored'
)
new_values[i] = replaced[
Expand Down Expand Up @@ -240,7 +240,7 @@ def insert(self, index: int, value: ProtectedType_) -> None:
elif not isinstance(value, tuple(self._protected_types)):
raise TypeError(f'Items must be one of {self._protected_types}, got {type(value)}')
if value in self:
warnings.warn(
logging.getLogger('easyscience.base_classes').warning(
f'Item with unique name "{self._get_key(value)}" already in EasyList, it will be ignored'
)
return
Expand Down
8 changes: 3 additions & 5 deletions src/easyscience/base_classes/obj_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@
Please update your imports.
"""

import warnings
import logging

from ..legacy.obj_base import ObjBase # noqa: F401

warnings.warn(
logging.getLogger('easyscience').warning(
'easyscience.base_classes.obj_base is deprecated. '
'Please import from easyscience.legacy.obj_base instead.',
DeprecationWarning,
stacklevel=2,
'Please import from easyscience.legacy.obj_base instead.'
)
23 changes: 7 additions & 16 deletions src/easyscience/fitting/available_minimizers.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: 2024 EasyScience contributors <https://github.com/easyscience>
# SPDX-License-Identifier: BSD-3-Clause

import warnings
import logging
from dataclasses import dataclass
from enum import Enum

Expand All @@ -15,11 +15,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,
stacklevel=2,
logging.getLogger('easyscience.fitting').warning(

Check warning on line 18 in src/easyscience/fitting/available_minimizers.py

View check run for this annotation

Codecov / codecov/patch

src/easyscience/fitting/available_minimizers.py#L18

Added line #L18 was not covered by tests
'LMFit minimization is not available. Probably lmfit has not been installed.'
)

bumps_engine_available = False
Expand All @@ -28,11 +25,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,
logging.getLogger('easyscience.fitting').warning(

Check warning on line 28 in src/easyscience/fitting/available_minimizers.py

View check run for this annotation

Codecov / codecov/patch

src/easyscience/fitting/available_minimizers.py#L28

Added line #L28 was not covered by tests
'Bumps minimization is not available. Probably bumps has not been installed.'
)

dfo_engine_available = False
Expand All @@ -41,11 +35,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,
logging.getLogger('easyscience.fitting').warning(

Check warning on line 38 in src/easyscience/fitting/available_minimizers.py

View check run for this annotation

Codecov / codecov/patch

src/easyscience/fitting/available_minimizers.py#L38

Added line #L38 was not covered by tests
'DFO minimization is not available. Probably dfols has not been installed.'
)


Expand Down
9 changes: 7 additions & 2 deletions src/easyscience/fitting/calculators/interface_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from __future__ import annotations

import logging
from typing import TYPE_CHECKING
from typing import Any
from typing import Callable
Expand Down Expand Up @@ -96,12 +97,16 @@ def switch(self, new_interface: str, fitter: Optional[Type[Fitter]] = None) -> N
if hasattr(obj, 'update_bindings'):
obj.update_bindings()
except Exception as e:
print(f'Unable to auto generate bindings.\n{e}')
logging.getLogger('easyscience.fitting.calculators').warning(
'Unable to auto generate bindings.\n%s', e
)
elif hasattr(fitter, 'generate_bindings'):
try:
fitter.generate_bindings()
except Exception as e:
print(f'Unable to auto generate bindings.\n{e}')
logging.getLogger('easyscience.fitting.calculators').warning(
'Unable to auto generate bindings.\n%s', e
)

@property
def available_interfaces(self) -> List[str]:
Expand Down
Loading
Loading