Skip to content

Commit d7cf0fc

Browse files
authored
fix: correctly setup initial logging (#3747)
1 parent c63a575 commit d7cf0fc

File tree

4 files changed

+44
-14
lines changed

4 files changed

+44
-14
lines changed

docs/release-notes/3747.bugfix.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix setup of initial log stream {smaller}`P Angerer`

src/scanpy/_settings/__init__.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from .. import logging
1111
from .._compat import deprecated, old_positionals
12-
from .._singleton import SingletonMeta
12+
from .._singleton import SingletonMeta, documenting
1313
from ..logging import _RootLogger, _set_log_file, _set_log_level
1414
from .verbosity import Verbosity
1515

@@ -69,7 +69,7 @@ def wrapped(self: S, var: T, *args: P.args, **kwargs: P.kwargs) -> R:
6969
class SettingsMeta(SingletonMeta):
7070
# logging
7171
_root_logger: _RootLogger
72-
_logfile: TextIO | None
72+
_logfile: TextIO
7373
_verbosity: Verbosity
7474
# rest
7575
_n_pcs: int
@@ -288,15 +288,15 @@ def logpath(cls) -> Path | None:
288288
@_type_check_arg2(Path | str)
289289
def logpath(cls, logpath: Path | str | None) -> None:
290290
if logpath is None:
291-
cls._logfile = None
291+
cls.logfile = None
292292
cls._logpath = None
293293
return
294294
# set via “file object” branch of logfile.setter
295295
cls.logfile = Path(logpath).open("a") # noqa: SIM115
296296
cls._logpath = Path(logpath)
297297

298298
@property
299-
def logfile(cls) -> TextIO | None:
299+
def logfile(cls) -> TextIO:
300300
"""The open file to write logs to.
301301
302302
Set it to a :class:`~pathlib.Path` or :class:`str` to open a new one.
@@ -310,7 +310,7 @@ def logfile(cls) -> TextIO | None:
310310
@logfile.setter
311311
def logfile(cls, logfile: Path | str | TextIO | None) -> None:
312312
if not logfile: # "" or None
313-
logfile = sys.stdout if cls._is_run_from_ipython() else sys.stderr
313+
logfile = cls._default_logfile()
314314
if isinstance(logfile, Path | str):
315315
cls.logpath = logfile
316316
return
@@ -441,6 +441,10 @@ def _is_run_from_ipython() -> bool:
441441

442442
return getattr(builtins, "__IPYTHON__", False)
443443

444+
@classmethod
445+
def _default_logfile(cls) -> TextIO:
446+
return sys.stdout if cls._is_run_from_ipython() else sys.stderr
447+
444448
def __str__(cls) -> str:
445449
return "\n".join(
446450
f"{k} = {v!r}"
@@ -456,8 +460,8 @@ def __new__(cls) -> type[Self]:
456460
return cls
457461

458462
# logging
459-
_root_logger: ClassVar = _RootLogger(logging.INFO)
460-
_logfile: ClassVar = None
463+
_root_logger: ClassVar = _RootLogger(logging.WARNING)
464+
_logfile: ClassVar = SettingsMeta._default_logfile()
461465
_logpath: ClassVar = None
462466
_verbosity: ClassVar = Verbosity.warning
463467
# rest
@@ -481,3 +485,8 @@ def __new__(cls) -> type[Self]:
481485
_start: ClassVar = time()
482486
_previous_time: ClassVar = _start
483487
_previous_memory_usage: ClassVar = -1
488+
489+
490+
if not documenting(): # finish initialization
491+
_set_log_level(settings, settings.verbosity.level)
492+
_set_log_file(settings)

src/testing/scanpy/_pytest/__init__.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import os
66
import sys
7+
from types import MappingProxyType
78
from typing import TYPE_CHECKING
89

910
import pytest
@@ -12,16 +13,18 @@
1213
from .marks import needs
1314

1415
if TYPE_CHECKING:
15-
from collections.abc import Generator, Iterable
16+
from collections.abc import Generator, Iterable, Mapping
17+
18+
_original_settings: Mapping[str, object] | None = None
1619

1720

1821
# Defining it here because it’s autouse.
1922
@pytest.fixture(autouse=True)
20-
def _global_test_context(
23+
def original_settings(
2124
request: pytest.FixtureRequest,
2225
cache: pytest.Cache,
2326
tmp_path_factory: pytest.TempPathFactory,
24-
) -> Generator[None, None, None]:
27+
) -> Generator[Mapping[str, object], None, None]:
2528
"""Switch to agg backend, reset settings, and close all figures at teardown."""
2629
# make sure seaborn is imported and did its thing
2730
import seaborn as sns # noqa: F401
@@ -30,6 +33,10 @@ def _global_test_context(
3033

3134
import scanpy as sc
3235

36+
global _original_settings # noqa: PLW0603
37+
if _original_settings is None:
38+
_original_settings = MappingProxyType(sc.settings.__dict__.copy())
39+
3340
setup()
3441
sc.settings.logfile = sys.stderr
3542
sc.settings.verbosity = "hint"
@@ -44,7 +51,7 @@ def _global_test_context(
4451
if isinstance(request.node, pytest.DoctestItem):
4552
_modify_doctests(request)
4653

47-
yield
54+
yield _original_settings
4855

4956
plt.close("all")
5057

tests/test_logging.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from contextlib import redirect_stdout
66
from datetime import datetime
77
from io import StringIO
8+
from logging import StreamHandler
89
from typing import TYPE_CHECKING
910

1011
import pytest
@@ -15,14 +16,26 @@
1516
from scanpy import settings as s
1617

1718
if TYPE_CHECKING:
19+
from collections.abc import Mapping
1820
from pathlib import Path
1921

2022

21-
def test_defaults():
22-
assert s.logpath is None
23+
def test_defaults(
24+
caplog: pytest.LogCaptureFixture, original_settings: Mapping[str, object]
25+
) -> None:
26+
assert s.logpath is original_settings["_logpath"] is None
27+
assert s.logfile is original_settings["_logfile"] is sys.stderr
28+
# we override s.verbosity, so we only check the default here:
29+
assert original_settings["_verbosity"] is Verbosity.warning
30+
31+
# check logging handler file and level
32+
[handler] = (h for h in s._root_logger.handlers if h is not caplog.handler)
33+
assert isinstance(handler, StreamHandler)
34+
assert handler.stream is s.logfile
35+
assert s._root_logger.level == s.verbosity.level
2336

2437

25-
def test_records(caplog: pytest.LogCaptureFixture):
38+
def test_records(caplog: pytest.LogCaptureFixture) -> None:
2639
s.verbosity = Verbosity.debug
2740
log.error("0")
2841
log.warning("1")

0 commit comments

Comments
 (0)