From 70f1dc6e7842876512a627fc91f879fe077b2b8f Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Sun, 11 Oct 2026 13:50:01 +0100 Subject: [PATCH] Avoid installing root logging handler when importing dm_control --- dm_control/_render/__init__.py | 16 +++-- dm_control/_render/import_logging_test.py | 61 +++++++++++++++++++ .../mujoco/wrapper/mjbindings/__init__.py | 8 ++- 3 files changed, 76 insertions(+), 9 deletions(-) create mode 100644 dm_control/_render/import_logging_test.py diff --git a/dm_control/_render/__init__.py b/dm_control/_render/__init__.py index d1dd4446..1fcb2c77 100644 --- a/dm_control/_render/__init__.py +++ b/dm_control/_render/__init__.py @@ -25,9 +25,13 @@ import collections import os -from absl import logging +import logging from dm_control._render import constants +# Import-time status logging must not install a handler on the application root +# logger. A named stdlib logger leaves logging.basicConfig() available to users. +_LOGGER = logging.getLogger(__name__) + BACKEND = os.environ.get(constants.MUJOCO_GL) @@ -85,11 +89,11 @@ def no_renderer(*args, **kwargs): raise RuntimeError( 'Environment variable {} must be one of {!r}: got {!r}.' .format(constants.MUJOCO_GL, sorted(all_names), BACKEND)) - logging.info('MUJOCO_GL=%s, attempting to import specified OpenGL backend.', + _LOGGER.info('MUJOCO_GL=%s, attempting to import specified OpenGL backend.', BACKEND) Renderer = import_func() else: - logging.info('MUJOCO_GL is not set, so an OpenGL backend will be chosen ' + _LOGGER.info('MUJOCO_GL is not set, so an OpenGL backend will be chosen ' 'automatically.') # Otherwise try importing them in descending order of priority until # successful. @@ -97,12 +101,12 @@ def no_renderer(*args, **kwargs): try: Renderer = import_func() BACKEND = names[0] - logging.info('Successfully imported OpenGL backend: %s', names[0]) + _LOGGER.info('Successfully imported OpenGL backend: %s', names[0]) break except ImportError: - logging.info('Failed to import OpenGL backend: %s', names[0]) + _LOGGER.info('Failed to import OpenGL backend: %s', names[0]) if BACKEND is None: - logging.info('No OpenGL backend could be imported. Attempting to create a ' + _LOGGER.info('No OpenGL backend could be imported. Attempting to create a ' 'rendering context will result in a RuntimeError.') Renderer = _no_renderer() diff --git a/dm_control/_render/import_logging_test.py b/dm_control/_render/import_logging_test.py new file mode 100644 index 00000000..4d904a9e --- /dev/null +++ b/dm_control/_render/import_logging_test.py @@ -0,0 +1,61 @@ +# Copyright 2026 The dm_control Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================ + +"""Regression tests for application logging when importing dm_control.""" + +import subprocess +import sys + +from absl.testing import absltest + + +class ImportLoggingTest(absltest.TestCase): + + def test_mjcf_import_leaves_root_logger_unconfigured(self): + script = """ +import logging +assert not logging.root.handlers +from dm_control import mjcf +assert not logging.root.handlers, logging.root.handlers +logging.basicConfig(level=logging.INFO, format='APP LOG: %(message)s') +logging.info('configured after import') +""" + result = subprocess.run( + [sys.executable, "-c", script], check=False, + capture_output=True, text=True, timeout=30) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("APP LOG: configured after import", result.stderr) + + def test_mjcf_import_preserves_existing_handler(self): + script = """ +import logging +import sys +handler = logging.StreamHandler(sys.stderr) +handler.setFormatter(logging.Formatter('BEFORE: %(message)s')) +logging.root.addHandler(handler) +logging.root.setLevel(logging.INFO) +from dm_control import mjcf +assert logging.root.handlers == [handler], logging.root.handlers +logging.info('handler still configured') +""" + result = subprocess.run( + [sys.executable, "-c", script], check=False, + capture_output=True, text=True, timeout=30) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("BEFORE: handler still configured", result.stderr) + + +if __name__ == "__main__": + absltest.main() diff --git a/dm_control/mujoco/wrapper/mjbindings/__init__.py b/dm_control/mujoco/wrapper/mjbindings/__init__.py index 57621b20..cc7360c6 100644 --- a/dm_control/mujoco/wrapper/mjbindings/__init__.py +++ b/dm_control/mujoco/wrapper/mjbindings/__init__.py @@ -16,20 +16,22 @@ """Import core names of MuJoCo ctypes bindings.""" -from absl import logging +import logging from dm_control.mujoco.wrapper.mjbindings import constants from dm_control.mujoco.wrapper.mjbindings import enums from dm_control.mujoco.wrapper.mjbindings import sizes +_LOGGER = logging.getLogger(__name__) + # Internal analytics import. # pylint: disable=g-import-not-at-top try: from dm_control.mujoco.wrapper.mjbindings import functions from dm_control.mujoco.wrapper.mjbindings.functions import mjlib - logging.info('MuJoCo library version is: %s', mjlib.mj_versionString()) + _LOGGER.info('MuJoCo library version is: %s', mjlib.mj_versionString()) # Internal analytics. except (IOError, OSError): - logging.warning('mjbindings failed to import mjlib and other functions. ' + _LOGGER.warning('mjbindings failed to import mjlib and other functions. ' 'libmujoco.so may not be accessible.')