2

I was trying to get the contents of sys.stdout in a string. I tried the obvious:

def get_stdout():
    import sys

    print('a')
    print('b')
    print('c')

    repr(sys.stdout)

    contents = ""
    #with open('some_file.txt','r') as f:
    #with open(sys.stdout) as f:
    for line in sys.stdout.readlines():
        contents += line
    print(contents)

but that gives the error:

Exception has occurred: UnsupportedOperation
not readable

So how do I just change the permissions of that already opened file?

I tried:

    sys.stdout.mode = 'r'

but that still gives the same error...

Other things that would work would be to just get me the name/path of stdout in a hardware independent way.


Another thing that would just work is letting me to put the contents of sys.stdout after I've run my main script in a string.


these might be relevant if you are getting bugs like I am: why __builtins__ is both module and dict Python: What's the difference between __builtin__ and __builtins__?

bug:

line 37, in my_print
    __builtins__["print"](*args, file=f)  # saves to file
TypeError: 'module' object is not subscriptable

Questions I've read that did not help:

Charlie Parker
  • 5,884
  • 57
  • 198
  • 323
  • 2
    You can't get the contents of stdout. All you can do is write to it, it doesn't save it for reading back. – Barmar Apr 07 '20 at 16:37
  • 2
    If you want to, you can set the stdout to a specific file that you made which has both reading and writing privilages. – trigangle Apr 07 '20 at 16:37
  • @trigangle that should work! As long as the standard python string function still works! – Charlie Parker Apr 07 '20 at 16:38
  • Something else that would be nice is to get a pointer to the print function itself, because I tried doing: ```out_str = '' def print(x='\n'): """My hacky print statement while I figure out how to redirect STDOUT to sending the email Keyword Arguments: x {str} -- [description] (default: {'\n'}) """ print(x) out_str = out_str + x``` But it obviously didn't work... – Charlie Parker Apr 07 '20 at 16:39
  • 1
    Just one thing: you can use `print("blob", stdout=)` in order to print to a specific file. – trigangle Apr 07 '20 at 16:40
  • @trigangle will my print function still print to my screen though? – Charlie Parker Apr 07 '20 at 16:42
  • 1
    It will not be able to do this. You will need to change the print function to print to both `sys.__stdout__` and your specific file. – trigangle Apr 07 '20 at 16:43
  • @trigangle do you have a small sample script? I am confused if I should use `sys.stdout` or `sys.__stdout__`... – Charlie Parker Apr 07 '20 at 16:44
  • these might be relevant if you are getting bugs like I am: https://stackoverflow.com/questions/1184016/why-builtins-is-both-module-and-dict https://stackoverflow.com/questions/11181519/python-whats-the-difference-between-builtin-and-builtins – Charlie Parker Sep 21 '20 at 19:41
  • I do want to note that the best is likely to use loggers e.g. https://stackoverflow.com/a/61255375/3167448 – Charlie Parker Sep 21 '20 at 20:03

4 Answers4

1

You can use the following code:

import sys
from builtins import print as builtin_print
myfile = "output.txt"
def print(*args):
    builtin_print(*args, file=sys.__stdout__)    # prints to terminal
    with open(myfile, "a+") as f:
        builtin_print(*args, file=f)    # saves in a file

This should redefine the print function so that it prints to stdout and to your file. You can then read from the file.

Noctis Skytower
  • 21,433
  • 16
  • 79
  • 117
trigangle
  • 193
  • 1
  • 9
  • what does this line do: `__builtins__.print(*args, file=sys.__stdout__)`? Why didn't you do something like `print = __builtins__.print(*args, file=sys.__stdout__)`? – Charlie Parker Apr 07 '20 at 17:06
  • 1
    `__builtins__` is calling the original print funciton. The reason is once I redefine the print function, the old function is gone. Thus, invoking the `__builtins__.print` allows me to call back the original print function. the `file=` handler tells the print function to print to the right file. `sys.__stdout__` is the original stdout for the terminal. Thus, when you print, you are printing to the terminal and saving everything in a file. – trigangle Apr 07 '20 at 17:12
  • This would be my approach as well - the only thing I might do differently here is `open`ing the file when the script runs and close it when it finishes. Depends how many times you call `print` though, I suppose. – r.ook Apr 07 '20 at 17:15
  • @trigangle +1 fantastic! I've been trying to get a pointer to the original print but got in loops or having `__future_` complain to me. Didn't know about `__builtins__ `. – Charlie Parker Apr 07 '20 at 17:17
  • @trigangle awesome! You remembered the `a+` so that the file is created if it does not exist already. – Charlie Parker Apr 07 '20 at 18:16
  • Note I think your code has a bug: ```Exception has occurred: AttributeError 'dict' object has no attribute 'print'``` Should be `__builtins__['print']` – Charlie Parker Apr 07 '20 at 18:40
  • just out of curiosity. If we would have used `sys.err` if point crashes, would it print to terminal normal? Would it print normally to the screen if we use `sys.err`? – Charlie Parker Apr 15 '20 at 17:24
  • You can write to the `sys.stderr`, but from what I have been seeing, it will look the same as `sys.stdout`. – trigangle Apr 16 '20 at 19:22
  • I do want to note that most likely the best practice is to use loggers. e.g. https://stackoverflow.com/a/61255375/3167448 – Charlie Parker Sep 21 '20 at 20:03
1

I want to share the code that I am using, as inspired by accepted answer:

def my_print(*args, filepath="~/my_stdout.txt"):
    """Modified print statement that prints to terminal/scree AND to a given file (or default).

    Note: import it as follows:

    from utils.utils import my_print as print

    to overwrite builtin print function

    Keyword Arguments:
        filepath {str} -- where to save contents of printing (default: {'~/my_stdout.txt'})
    """
    import sys
    from builtins import print as builtin_print
    filepath = Path(filepath).expanduser()
    # do normal print
    builtin_print(*args, file=sys.__stdout__)  # prints to terminal
    # open my stdout file in update mode
    with open(filepath, "a+") as f:
        # save the content we are trying to print
        builtin_print(*args, file=f)  # saves to file

Note the a+ to be able to create the file if it already does NOT exist.

Note that if you want to delete the old contents of your custom my_stdout.txt you need to delete the file and check if it exists:

    # remove my stdout if it exists
    os.remove(Path('~/my_stdout.txt').expanduser()) if os.path.isfile(Path('~/my_stdout.txt').expanduser()) else None

I think that should be all.


edit:

I was getting an error:

line 37, in my_print
    __builtins__["print"](*args, file=f)  # saves to file
TypeError: 'module' object is not subscriptable

I looked into more details:

and learned that __builtins__ seems to be unreliable (due to python implementation details).

It seems the most reliable way to access the built in functions is with the import so I returned this to the code the original answerer gave me.

Charlie Parker
  • 5,884
  • 57
  • 198
  • 323
1

You can temporarily redirect stdout to an object of your choosing. The example shown below stores printed data in a StringIO instance. Once the context manager block ends, normal printing resumes and allows showing some debugging information:

#! /usr/bin/env python3
import contextlib
import io


def main():
    file = io.StringIO()
    with contextlib.redirect_stdout(file):
        print('a')
        print('b')
        print('c')
    print(f'{file!r}\n{file.getvalue()!r}\n{file.getvalue()!s}')


if __name__ == '__main__':
    main()

Addendum:

If you wish to use stdout like normal and still capture what is printed to it, you might want to use the following example instead. The Apply class can wrap several instances and duplicate method calls across all of them. Therefore, the call to redirect_stdout has been slightly modified:

#! /usr/bin/env python3
import contextlib
import io
import sys


def main():
    file = io.StringIO()
    with contextlib.redirect_stdout(Apply(sys.stdout, file)):
        print('a')
        print('b')
        print('c')
    print(f'{file!r}\n{file.getvalue()!r}\n{file.getvalue()!s}')


class Apply:

    def __init__(self, *args):
        self.__objects = args

    def __getattr__(self, name):
        attr = _Attribute(getattr(obj, name) for obj in self.__objects)
        setattr(self, name, attr)
        return attr


class _Attribute:

    def __init__(self, iterable):
        self.__attributes = tuple(filter(callable, iterable))

    def __call__(self, *args, **kwargs):
        return [attr(*args, **kwargs) for attr in self.__attributes]


if __name__ == '__main__':
    main()
Noctis Skytower
  • 21,433
  • 16
  • 79
  • 117
  • I think what I wanted was to be able to redirect AND also print to the terminal normally. Does this work like that? – Charlie Parker Apr 07 '20 at 20:15
  • @Pinocchio The original code did not operate as you asked. However, if you read the addendum added below the original answer, new code has been provided that allow for redirecting and printing to the terminal normally. – Noctis Skytower Apr 07 '20 at 20:37
-1

My previous answer to this question is not as good as I thought (https://stackoverflow.com/a/61087617/3167448). I think the real answer to this question is to simply use a logger. I did not know what loggers were until recently but they are much better.

It's much better to create a logger object that sends your strings to a log file AND to the stdout. It even allows you to route the messages more finely according to the threshold level. Here is the code:

def logger_SO_print_and_write_to_my_stdout():
    """My sample logger code to print to screen and write to file (the same thing).

    Note: trying to replace this old answer of mine using a logger: 
    - https://github.com/CoreyMSchafer/code_snippets/tree/master/Logging-Advanced

    Credit: 
    - https://www.youtube.com/watch?v=jxmzY9soFXg&t=468s
    - https://github.com/CoreyMSchafer/code_snippets/tree/master/Logging-Advanced
    - https://stackoverflow.com/questions/21494468/about-notset-in-python-logging/21494716#21494716

    Other resources:
    - https://docs.python-guide.org/writing/logging/
    - https://docs.python.org/3/howto/logging.html#logging-basic-tutorial
    """
    from pathlib import Path
    import logging
    import os
    import sys
    from datetime import datetime

    ## create directory (& its parents) if it does not exist otherwise do nothing :)
    # get current time
    current_time = datetime.now().strftime('%b%d_%H-%M-%S') 
    logs_dirpath = Path(f'~/logs/python_playground_logs_{current_time}/').expanduser()
    logs_dirpath.mkdir(parents=True, exist_ok=True)
    my_stdout_filename = logs_dirpath / Path('my_stdout.log')
    # remove my_stdout if it exists (note you can also just create a new log dir/file each time or append to the end of the log file your using)
    #os.remove(my_stdout_filename) if os.path.isfile(my_stdout_filename) else None

    ## create top logger
    logger = logging.getLogger(__name__) # loggers are created in hierarchy using dot notation, thus __name__ ensures no name collisions.
    logger.setLevel(logging.DEBUG) # note: use logging.DEBUG, CAREFUL with logging.UNSET: https://stackoverflow.com/questions/21494468/about-notset-in-python-logging/21494716#21494716

    ## log to my_stdout.log file
    file_handler = logging.FileHandler(filename=my_stdout_filename)
    #file_handler.setLevel(logging.INFO) # not setting it means it inherits the logger. It will log everything from DEBUG upwards in severity to this handler.
    log_format = "{asctime}:{levelname}:{lineno}:{name}:{message}" # see for logrecord attributes https://docs.python.org/3/library/logging.html#logrecord-attributes
    formatter = logging.Formatter(fmt=log_format, style='{') # set the logging format at for this handler
    file_handler.setFormatter(fmt=formatter)

    ## log to stdout/screen
    stdout_stream_handler = logging.StreamHandler(stream=sys.stdout) # default stderr, though not sure the advatages of logging to one or the other
    #stdout_stream_handler.setLevel(logging.INFO) # Note: having different set levels means that we can route using a threshold what gets logged to this handler
    log_format = "{name}:{levelname}:-> {message}" # see for logrecord attributes https://docs.python.org/3/library/logging.html#logrecord-attributes
    formatter = logging.Formatter(fmt=log_format, style='{') # set the logging format at for this handler
    stdout_stream_handler.setFormatter(fmt=formatter)

    logger.addHandler(hdlr=file_handler) # add this file handler to top logger
    logger.addHandler(hdlr=stdout_stream_handler) # add this file handler to top logger

    logger.log(logging.NOTSET, 'notset')
    logger.debug('debug')
    logger.info('info')
    logger.warning('warning')
    logger.error('error')
    logger.critical('critical')

log content:

2020-04-16 11:28:24,987:DEBUG:154:__main__:debug
2020-04-16 11:28:24,988:INFO:155:__main__:info
2020-04-16 11:28:24,988:WARNING:156:__main__:warning
2020-04-16 11:28:24,988:ERROR:157:__main__:error
2020-04-16 11:28:24,988:CRITICAL:158:__main__:critical

terminal stdout output:

__main__:DEBUG:-> debug
__main__:INFO:-> info
__main__:WARNING:-> warning
__main__:ERROR:-> error
__main__:CRITICAL:-> critical

I feel this is a particular important question/answer to reference just in case you have issues with UNSET: About NOTSET in python logging thank god for the answer and question there.

Charlie Parker
  • 5,884
  • 57
  • 198
  • 323