Skip to content

Commit 2b7592f

Browse files
Fix logger verbose level not restored after unpickling
HierarchicalClassifier obtains its logger via logging.getLogger(name), a process-global singleton that pickles by name only, so the verbose level set at fit time is lost on unpickle: the restored logger inherits whatever level the current process's singleton happens to have. Add __setstate__ to recreate the logger from the restored self.verbose for fitted models (guarded on logger_ being present so unfitted models are unaffected). Add a regression test that poisons the shared logger and asserts the level survives a pickle round-trip. Closes #146
1 parent 6384239 commit 2b7592f

2 files changed

Lines changed: 29 additions & 0 deletions

File tree

hiclass/HierarchicalClassifier.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,14 @@ def _create_logger(self):
333333
# Add ch to logger
334334
self.logger_.addHandler(ch)
335335

336+
def __setstate__(self, state):
337+
self.__dict__.update(state)
338+
# Loggers are pickled by name only, so the verbose level configured at
339+
# fit time is not restored (the underlying logger is a process-global
340+
# singleton). Recreate the logger to reapply self.verbose.
341+
if "logger_" in state:
342+
self._create_logger()
343+
336344
def _disambiguate(self, y):
337345
self.separator_ = "::HiClass::Separator::"
338346
if y.ndim == 2:

tests/test_LocalClassifiers.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
import os
23
import pickle
34

@@ -128,3 +129,23 @@ def test_tmp_dir(classifier):
128129
assert expected_name == name
129130
check_is_fitted(classifier)
130131
clf.fit(x, y)
132+
133+
134+
@pytest.mark.parametrize("classifier", classifiers)
135+
def test_logger_level_preserved_after_pickle(classifier):
136+
# https://github.com/scikit-learn-contrib/hiclass/issues/146
137+
clf = classifier(local_classifier=LogisticRegression(), verbose=logging.WARNING)
138+
x = np.array([[1, 2], [3, 4], [5, 6], [7, 8]])
139+
y = np.array([["a", "b"], ["a", "c"], ["d", "e"], ["d", "f"]])
140+
clf.fit(x, y)
141+
assert clf.logger_.getEffectiveLevel() == logging.WARNING
142+
143+
data = pickle.dumps(clf)
144+
# Loggers pickle by name, so the level lives on a process-global singleton.
145+
# Change it to simulate another process/instance touching the same logger;
146+
# without the fix the unpickled model would inherit this level.
147+
logging.getLogger(clf.classifier_abbreviation).setLevel(logging.DEBUG)
148+
149+
restored = pickle.loads(data)
150+
assert restored.verbose == logging.WARNING
151+
assert restored.logger_.getEffectiveLevel() == logging.WARNING

0 commit comments

Comments
 (0)