Repository: pypa/setuptools

Issue:
Recommended practice for logging in plugins?
I'm currently adding a `setuptools.finalize_distribution_options` hook to one of my packages, and I felt it would be appropriate if it emitted some informational messages when invoked through `egg_info`, `sdist`, or `bdist_wheel`.  I thus added several calls to `disutils.log.info()` to the code, but whenever I run `egg_info` etc., the messages are not emitted, even though the rest of the hook function does its thing and even though the output that is emitted appears to also be at `INFO` level.  The messages show up if I output them with `print()`, but that's clearly the wrong thing to do, as then they also show up when the `--quiet` option is given and when running things like `setup.py --version`.

Just how should setuptools plugins perform logging?  Should they even do it at all?


Discussion:
My guess is that `finalize_distribution_options` happens before the logging has been set up, though I haven't verified. I don't have any strong opinions about how this should behave, and my instinct aligns with yours. Would you be willing to investigate further?
You appear to be right.  The default logging level for `distutils.log` is `WARN`, and it's only set to `INFO` by a call to `log.set_verbosity()` in `distutils.dist.Distribution.parse_command_line()`.  Meanwhile, `Distribution.finalize_options()` is called earlier, at the end of `distutils.dist.Distribution.__init__`.  Essentially, `finalize_distribution_options` hooks are processed before command line options, and it's the command line options that determine what the logging threshold should be.

I'm not sure how best to change this, if that's what we want.

* Changing the threshold to `INFO` before calling `finalize_options()` would mean that too much would be output when the `--quiet` option is given.

* Moving the processing of `finalize_distribution_options` to after command-line options are processed would (I think) mean that things like `setup.py --version` would not take the hooks into account.

* Arguably, the best place (as far as I can tell) to process `finalize_distribution_options` would be in distutils at https://github.com/python/cpython/blob/3.8/Lib/distutils/dist.py#L478, right after setting the log threshold and before processing display options like `--version`.  I don't know whether the amount of code duplication necessary to accomplish this in setuptools would be considered worth it.

**Edit**: A downside I just realized: distutils sends `INFO` messages to stdout for some reason, and so if the logging threshold allows `INFO` messages when hooks are run, then the hooks' logs will get mixed in with output from display options like `--version`, which is bad.  That seems like an argument against supporting logging (or at least `INFO` logging) in hooks.

Resolving patch:
diff --git a/setuptools/__init__.py b/setuptools/__init__.py
--- a/setuptools/__init__.py
+++ b/setuptools/__init__.py
@@ -18,6 +18,7 @@
 from setuptools.dist import Distribution
 from setuptools.depends import Require
 from . import monkey
+from . import logging
 
 
 __all__ = [
@@ -149,6 +150,7 @@ def finalize_options(self):
 
 def setup(**attrs):
     # Make sure we have any requirements needed to interpret 'attrs'.
+    logging.configure()
     _install_setup_requires(attrs)
     return distutils.core.setup(**attrs)
 
diff --git a/setuptools/logging.py b/setuptools/logging.py
new file mode 100644
--- /dev/null
+++ b/setuptools/logging.py
@@ -0,0 +1,30 @@
+import sys
+import logging
+import distutils.log
+from . import monkey
+
+
+def _not_warning(record):
+    return record.levelno < logging.WARNING
+
+
+def configure():
+    """
+    Configure logging to emit warning and above to stderr
+    and everything else to stdout. This behavior is provided
+    for compatibilty with distutils.log but may change in
+    the future.
+    """
+    err_handler = logging.StreamHandler()
+    err_handler.setLevel(logging.WARNING)
+    out_handler = logging.StreamHandler(sys.stdout)
+    out_handler.addFilter(_not_warning)
+    handlers = err_handler, out_handler
+    logging.basicConfig(
+        format="{message}", style='{', handlers=handlers, level=logging.DEBUG)
+    monkey.patch_func(set_threshold, distutils.log, 'set_threshold')
+
+
+def set_threshold(level):
+    logging.root.setLevel(level*10)
+    return set_threshold.unpatched(level)


Test patch: