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
Prevent apps from crashing when sys.stderr
is None
(pythonw and pyinstaller 5.7)
#8345
Conversation
Kivy apps will crash if run from pythonw or if they are built with pyinstaller 5.7 In both cases sys.stderr is set to None, causing a recursion error in the python logger. This fix sets the KIVY_NO_CONSOLELOG environment if sys.stderr is None. This preserves the behavior prior to pyinstaller 5.7, and allows correct operation under pythonw.exe
fix pythonw and pyinstaller 5.7 logger recursion error
Why not test if I don't think Kivy should set environment variables as that should be left to users. |
Fair enough -that was my original implementation. I changed it for 2 reasons.
This is not a strongly held opinion. If you would like me to do in the other way, I'm happy to update the PR. Let me know your thoughts. |
Here is an idea to capture the "benefits" of my approach, without setting the environment variable. Create a new global variable that mirrors the name of the environment variable and use this in the add_kivy_handlers() function. KIVY_NO_CONSOLE_LOG = not sys.stderr or 'KIVY_NO_CONSOLELOG' in os.environ and then in add_kivy_handler(), at line 598 if not KIVY_NO_CONSOLELOG:
# load console log handler What do you think? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is a bit of overkill to use os.environ to signal a parameter to a piece of code 20 lines higher.
Why not move the check up into add_kivy_handlers
instead.
i.e. change line 597-598 to say:
# Don't output to stderr if it doesn't exist (e.g. no console window) or if overridden
if (sys.stderr is not None) and ('KIVY_NO_CONSOLELOG' not in os.environ):
(Other than that, looks good.)
Moved changed to the add_kivy_handlers() function
Requested change completed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not an approved reviewer, but I was the last to spend much time in this area of the code, and it LGTM.
@ElliotGarbus Parenthesis are redundant and and you could have used my suggestion #8345 (comment) which is shorter. |
Thanks for the feedback. |
kivy/logger.py
Outdated
if 'KIVY_NO_CONSOLELOG' not in os.environ: | ||
# Don't output to stderr if it is set to None | ||
# stderr is set to None by pythonw and pyinstaller 5.7+ | ||
if (sys.stderr is not None) and ('KIVY_NO_CONSOLELOG' not in os.environ): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the shorter version from pythonic64 as it looks cleaner, but more importantly, can we add a test that covers this case so it does not happen again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll make the change and figure out the testing...
@Julian-O I can see you have done some work with the kivy logger. I don't quite see how to test this fix. UPDATED: I have a clue. Here is my WIP: import sys
def test_logger_fix_8345():
"""
The test checks that the ConsoleHandler is not in the Logger
handlers list if stderr is None. Test sets stderr to None.
If the Console handler is found, the test fails.
Pythonw and Pyinstaller 5.7+ (with console set to false) set stderr
to None.
:return:
"""
from kivy.logger import Logger, add_kivy_handlers, ConsoleHandler
sys.stderr = None
console_handler_found = False
add_kivy_handlers(Logger)
for handler in Logger.handlers:
if isinstance(handler, ConsoleHandler):
console_handler_found = True
assert console_handler_found is False Still need to integrate the test. I assume I need to save and restore the state of sys.stderr - is that correct? |
or even better, make the assert self-commenting if it fails.
|
@Julian-O You mentioned: "I think you need to set stderr to None before the import of logger, because that is when the check is done, isn't it?" There are 2 actions on import. sys.stderr is reassigned, then add_kivy_handlers() is called. For the test, I will need to do the import then set sys,.stderr to None, then call add_kivy_handlers(). I may need to remove the handlers prior to calling add_kivy_handlers(), I'll know better when I put this test into test_logger.py and see the behavior. Thanks again for your help! |
I am worried we might be talking about different things, and confusing each other. I see three different levels that you could test at. You could do an integration test. This would be to call the code with pythonw.exe or pyinstaller, and show it works now where it didn't before. That would be a solid test, and would test your understanding of the cause of the problem, but will be tricky to produce in pytest. You could do a low-level unit test. This would be to test just the code in I thought you were doing a mid-level unit-test, where you test the way the whole module behaves. In that case, you would test it in Kivy and Mixed modes. You would redirect sys.stderr to None first, before the import, and then immediately after the import check that the implicit call to I think I would prefer a mid-level test, but I would accept a low-level one. Doing both a low-level and mid-level test is a bit "belt-and-braces" - I would suggest only doing one, unless you really want to be sure. Finally: The order of the code (except in PYTHON mode) is add the handlers first and then redirect stderr (so any output to stderr is captured by the logging systems). I don't think this is very important for this discussion. |
@Julian-O And looking back at the logger.py source code I see I misspoke about the order of event 😞. Thanks for catching that. I'll get back to this later today. |
@Julian-O I'm using a pip installed kivy, and jumping into the venv, and editing the test file. I'm current invoking: Here is my test, with some added logging output: def test_logger_fix_8345():
"""
The test checks that the ConsoleHandler is not in the Logger
handlers list if stderr is None. Test sets stderr to None,
if the Console handler is found, the test fails.
Pythonw and Pyinstaller 5.7+ (with console set to false) set stderr
to None.
"""
from kivy.logger import Logger, add_kivy_handlers, ConsoleHandler
Logger.info(f'Initial state: {Logger.handlers=} {LOG_MODE=} {os.environ.get("KIVY_LOG_MODE")=}')
original_sys_stderr = sys.stderr
sys.stderr = None
add_kivy_handlers(Logger)
Logger.info(f'{Logger.handlers=} {LOG_MODE=}')
sys.stderr = original_sys_stderr # restore sys.stderr
console_handler_found = any(isinstance(handler, ConsoleHandler) for handler in Logger.handlers)
assert not console_handler_found, "Console handler added, despite sys.stderr being None" Here is the relevant log outputs:
I'm surprised to see KIVY_LOG_MODE is None, |
That's working as designed. You haven't set the environment variable, so it is None. There is a line in
i.e. if no environment variable is set, default to KIVY mode. [Defaulting to KIVY mode was to make sure existing Kivy apps still worked like they used to.]
The automated tests (which run when you push to GitHub or do a pull request) runs the test three times. (Look at First it leaves KIVY_LOG_MODE unset and runs all the tests (except those covered with a skip) Because you don't want to run the test in KIVY mode, you should add a decorator:
You can run Because you do want to the test to be run in the second pass, you should add another decorator:
Now, to invoke it, you need to first set the environment variable. I see you are on Windows so the command should be Running the tests now with I am happy with what I am seeing, but one thing confuses me. Because you ran your existing test in KIVY mode, |
I am happy with what I am seeing, but one thing confuses me. Because you ran your existing test in KIVY mode, add_kivy_handlers() was presumably called twice - once implicitly on import (including a Console Log), and once explicitly by your test (without adding another ConsoleLog). I would have expected the test to fail, but you don't mention that it did. The environment variable is not set to 'KIVY', it is None. Note the message from the logger, this is after kivy.logger is imported:
The Logger evaluates the environment variable - and in this case effectively defaults to PYTHON. We can see none of the handlers are loaded. How/where does pyinstaller set the environment variable? Thanks again for your guidance. |
Yes. (Well, pedantically it is not set at all, and the
It shouldn't be the case that it defaults to PYTHON. There is a line in
i.e. if the environment variable is not set, default to KIVY.
That has me flustered. I wonder why not.
I have almost no knowledge about pyinstaller. I doubt it sets the KIVY_LOG_MODE environment variable. Does it set NO_CONSOLE_LOG one? |
Slaps forehead Of course! I see the problem! In KIVY mode (the default), a logger called "Kivy" is created, but the handlers are added to the root logger. Kivy sends log messages to the Kivy logger, but it has no handlers, so the event escalate up the tree to the root logger, and is handled there. Any custom logger that a client creates will also be handled by root logger, and go through the Kivy handlers. In MIXED mode, a logger called "Kivy" is created and the handlers are added directly to that. So now, the events raised by Kivy are handled by the Kivy loggers, and events send to other loggers can be handled however the user wants. In PYTHON mode, a logger called "Kivy" is created, but no handlers are added. A user may choose to add Kivy handlers, or not, wherever they like. So, in the case of this test:
This explains why it is running now. |
Agreed! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PEP8 checks are failing with:
./kivy/tests/test_logger.py:511:81: E501 line too long (99 > 80 characters)
Can you please rebase on top of latest master and make sure it passes the PEP8 checks?
Co-authored-by: Mirko Galimberti <me@mirkogalimberti.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Thank you!
sys.stderr
is None
(pythonw and pyinstaller 5.7)
Kivy apps will crash if run from pythonw or if they are built with pyinstaller 5.7. The reason is that these runtimes set sys.stderr to None, causing a recursion error in the python logger. This fix will not load the ConsoleHandler if sys.stderr is None. This preserves the behavior prior to pyinstaller 5.7, and allows correct operation under pythonw.exe
Edit: Updated comment to reflect the solution.
This will fix this issue: #8074
Here is a small test I used developing the fix. The design of the logger redirects messages written to stderr to the logfile. That behavior is retained. If this code is executed with pythonw (or is built with pyinstaller 5.7) on a version of kivy without the fix - it will fail to run.
Maintainer merge checklist
Component: xxx
label.api-deprecation
orapi-break
label.release-highlight
label to be highlighted in release notes.versionadded
,versionchanged
as needed.