Skip to content

bpo-45171: Fix stacklevel handling in logging. (GH-28287) #28287

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 27, 2022

Conversation

joukewitteveen
Copy link
Contributor

@joukewitteveen joukewitteveen commented Sep 11, 2021

I identified a few problems with the stacklevel handling code in the logging module:

  • currentframe was not marked as 'private', which would be desirable.
  • The fallback implementation of currentframe was different from the default implementation in that it did not skip any frames.
  • The number of frames skipped did not account for any direct calls to findCaller (and it did not provide a measurable speed-up).
  • stacklevel skipping did not use the same logic as in the warnings module, despite the documentation describing the argument as "The name of this parameter mirrors the equivalent one in the warnings module."

Regarding the last point, I did not know what to do in case more frames are to be skipped than the height of the stack. I listed three possibilities in the code which I think would all be sensible. The current behavior of using the top of the stack seems very odd to me.

https://bugs.python.org/issue45171

@@ -160,9 +160,9 @@ def addLevelName(level, levelName):
_releaseLock()

if hasattr(sys, '_getframe'):
currentframe = lambda: sys._getframe(3)
_currentframe = lambda: sys._getframe(1)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if renaming this now is good because probably there is code out there using this, and this would break and force them up to add the infamous if python version < xxx.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree and restored the original name. However, I do think either renaming currentframe to _currentframe or documenting it as an official method should be considered.

## If we want to mimic the warnings module:
#return ("sys", 1, "(unknown function)", None)
## If we want to be pedantic:
#raise ValueError("call stack is not deep enough")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far, findCaller did not throw, so to keep the more backward-compatible I think it would be better if it did not throw.

# warnings._is_internal_frame
if filename == _srcfile or (
"importlib" in filename and "_bootstrap" in filename):
):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why start skipping importlib and _bootstrap? Does the warning module have a reason for that? Why would the logging module benefit from it? If so, I would it would be better just call their function _is_internal_frame instead of copying its contents.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially, the warnings module was used for it, but then I realized that no underscore methods of other modules are used in this file. Therefore, this implementation of logging could in principle be used in Python implementations that have a different implementation of warnings. For this reason, I chose to duplicate that piece of code. I noted that the currentframe code was initially lifted from inspect.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipping importlib._bootstrap was done to allow module-level warnings (here: log messages) with stacklevels that point to the importing module and not to the import mechanism. A useful thing, I'd say.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for explaining it. I am not sure if it would be a problem for the logging module to show the stack trace from importlib or _bootstrap as it was a problem for the warning module. What if I am using the logging module inside importlib? Disregarding this last scenario, I would wait to see what someone more experienced with Python library think about this.

Just out of curiosity, did you have trouble with the logging module showing stack traces of importlib or _bootstrap which lead you to bring this checking from the warning module or you just happened to know about their protection and thought it would be good to add it here too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not "importlib or _bootstrap" that is checked for, but "importlib and _bootstrap" (i.e. importlib._bootstrap). There is no logging in Lib/importlib/_bootstrap.py and that module doesn't import anything (for obvious reasons), so I think we can safely say that the logging module is not used in importlib._bootstrap :-).

I ran into the stacklevel oddity mentioned in bpo-45171 and read that stacklevel was supposed to work like "the equivalent [parameter] in the warnings module". Thus I read how warnings chases the stack and copied the logic to logging.

@joukewitteveen joukewitteveen force-pushed the logging-stacklevel branch 2 times, most recently from 214a707 to 188e6d9 Compare September 11, 2021 16:21
if not f:
f = orig_f
rv = "(unknown file)", 0, "(unknown function)", None
while hasattr(f, "f_code"):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the new code doesn't verify that frame object contain an f_code attribute. It does verify that the frame object is not None. The f_code attribute is prescribed by the CPython data model and the warnings module doesn't check for it's presence either (it does check for stack exhaustion/None, like the new code here).

@github-actions
Copy link

This PR is stale because it has been open for 30 days with no activity.

@github-actions github-actions bot added the stale Stale PR or inactive for long period of time. label Oct 14, 2021
@joukewitteveen
Copy link
Contributor Author

@vsajip: I'm sorry to bother you, but you seem to be the chief-of-logging. Is there anything I can do to advance this pull request?

@github-actions github-actions bot removed the stale Stale PR or inactive for long period of time. label Oct 15, 2021
@joukewitteveen
Copy link
Contributor Author

@vsajip, I still stand behind this MR since it solves a real bug (the current code does not pass the tests added in this MR). Could you comment on its feasibility?

@vsajip
Copy link
Member

vsajip commented Mar 15, 2022

@joukewitteveen Sorry, been busy lately with other commitments. Will get to looking at this as soon as I can.

@vsajip
Copy link
Member

vsajip commented Mar 25, 2022

I'll close and reopen to unblock stalled checks.

@vsajip vsajip closed this Mar 25, 2022
@vsajip vsajip reopened this Mar 25, 2022
@vsajip vsajip changed the title bpo-45171: Fix stacklevel handling in logging module bpo-45171: Fix stacklevel handling in logging. (GH-28287) Mar 27, 2022
@vsajip vsajip merged commit 5ca6d74 into python:main Mar 27, 2022
Comment on lines +1570 to +1577
if next_f is None:
##TODO: We've got options here
## If we want to use the last (deepest) frame:
break
## If we want to mimic the warnings module:
#return ("sys", 1, "(unknown function)", None)
## If we want to be pedantic:
#raise ValueError("call stack is not deep enough")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vsajip Thanks for the approval and merge! Note that there was a TODO here which I summarized in the original PR description as:

Regarding the last point, I did not know what to do in case more frames are to be skipped than the height of the stack. I listed three possibilities in the code which I think would all be sensible.

Having it merged like this is not wrong per se (it will use the deepest available frame if the stack is exhausted), but it would be good to at least remove the TODO: part on line 1571. Maybe all of the above could even be replaced with

            if next_f is None:
                # Not enough stack frames. We use the last (deepest) one.
                break

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe leave it as a reminder in case this needs revisiting for any reason?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, but in that case I would still remove the word TODO:, since nothing needs to be done.

Anyway, I really appreciate that you took a look at this PR. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants