17

Running Django v1.10 on Python 3.5.0:

from django.core.management.base import BaseCommand

class Command(BaseCommand):
    def handle(self, *args, **options):
        print('hello ', end='', file=self.stdout)
        print('world', file=self.stdout)

Expected output:

hello world

Actual output:

hello 

world

How do I correctly pass the ending character? I currently use a workaround of setting explicitly:

 self.stdout.ending = ''

But this hack means you don't get all the features of the print function, you must use self.stdout.write and prepare the bytes manually.

wim
  • 338,267
  • 99
  • 616
  • 750
  • Is your use-case more long the lines of building up a whole text string based on coniditionals etc. The reason I ask is, if the whole string is known in advance, such as in the case of "hello world" you could have easily written out as a the full line while using print without stdout.write to avail the control over the newline ending character. So assuming that you do not know the text ahead of time, can you not build the text by string appends until the full text is ready for printing. – Bobby Chowdhury May 23 '16 at 21:53
  • 1
    Related: http://stackoverflow.com/questions/31926422/redirection-in-django-command-called-from-another-command-results-in-extraneous . And if you don't find a solution, I recommend writing a decorator or context manager temporarily patching `print` and/or `self.stdout` to behave the way you want. – Alex Hall May 23 '16 at 21:59
  • I don't get how using `self.stdout.ending = ''` "prevents [you] from using the print function and all its nice features". You can still use `print`, and the output will be what you expect so what's the problem? – Louis May 23 '16 at 22:34
  • @Louis No, did you actually try it? It still adds extra newlines and seems to ignores the `end` kwarg. – wim May 24 '16 at 03:59
  • This issue prevents me from using a console-based progress bar for long-running management commands. – wim Aug 24 '16 at 16:00

3 Answers3

12

As is mentioned in Django 1.10's Custom Management Commands document:

When you are using management commands and wish to provide console output, you should write to self.stdout and self.stderr, instead of printing to stdout and stderr directly. By using these proxies, it becomes much easier to test your custom command. Note also that you don’t need to end messages with a newline character, it will be added automatically, unless you specify the ending parameter:

self.stdout.write("Unterminated line", ending='')

Hence, in order to print in your Command class, you should define your handle() function as:

from django.core.management.base import BaseCommand

class Command(BaseCommand):
    def handle(self, *args, **options):
        self.stdout.write("hello ", ending='')
        self.stdout.write("world", ending='')

# prints: hello world

Also, by explicitly setting self.stdout.ending = '', you are modifying the property of self.stdout object. But you may not want this to be reflected in future calls of self.stdout.write(). Hence it will be better to use ending parameter within self.stdout.write() function (as demonstrated in sample code above).

As you mentioned "But this hack means you don't get all the features of the print function, you must use self.stdout.write and prepare the bytes manually." No, you do not have to worry about creating the bytes or other features of print(), as self.stdout.write() function belonging to OutputWrapper class expects data to be in str format. Also I would like to mention that print() and OutputWrapper.write() behaves quite similar both acting as a wrapper around sys.stdout.write().

The only difference between print() and OutputWrapper.write() is:

  • print() accepts message string as *args with separator parameter to join the the multiple strings, whereas
  • OutputWrapper.write() accepts single message string

But this difference could be easily handled by explicitly joining the strings with separator and passing it to OutputWrapper.write().

Conclusion: You do not have to worry about the additional features provided by print() as there are none, and should go ahead with using self.stdout.write() as suggested in this answer's quoted content from Custom Management Commands document.

If you are interested, you may check the source code of BaseCommand and OutputWrapper classes available at: Source code for django.core.management.base. It might help in clearing some of your doubts. You may also check PEP-3105 related to Python 3's print().

Moinuddin Quadri
  • 46,825
  • 13
  • 96
  • 126
  • 2
    Thanks, your claims check out! This design of `OutputWrapper` was a poor decision, it causes `self.stdout` to violate the principle of least surprise. You expect it to behave like `sys.stdout`, but it doesn't, meaning you can't use `self.stdout` together with the print function properly unless you RTFM. This has caught me up in testing a few times, now I finally understand the reasons! Imho the interface should have matched `sys.stdout`. Either that, or we should have a wrapper named `self.print` instead. – wim Oct 09 '16 at 16:49
  • I already had an idea that you are considering `self.stdout` as `sys.stdout`, that's why I mentioned *"self.stdout.write() function belonging to OutputWrapper class expects data to be in str format. Also I would like to mention that print() and OutputWrapper.write() behaves quite similar both acting as a wrapper around sys.stdout.write()."* – Moinuddin Quadri Oct 09 '16 at 22:23
8

When setting self.stdout.ending explicitly, the print command works as expected.

The line ending needs to be set in self.stdout.ending when file=self.stdout, because that is an instance of django.core.management.base.OutputWrapper.

class Command(BaseCommand):
    def handle(self, *args, **options):
        self.stdout.ending = ''
        print('hello ', end='', file=self.stdout)
        print('world', file=self.stdout)

Returns

hello world
C14L
  • 12,153
  • 4
  • 39
  • 52
6

First of all, self.stdout is an instance of django.core.management.base.OutputWrapper command. Its write expects an str, not bytes, thus you can use

self.stdout.write('hello ', ending='')
self.stdout.write('world')

Actually self.stdout.write does accept bytes but only whenever the ending is an empty string - that's because its write method is defined

def write(self, msg, style_func=None, ending=None):
    ending = self.ending if ending is None else ending
    if ending and not msg.endswith(ending):
        msg += ending
    style_func = style_func or self.style_func
    self._out.write(force_str(style_func(msg)))

If ending is true, then msg.endswith(ending) will fail if msg is a bytes instance and ending is a str.

Furthermore, print with self.stdout does work correctly when I set the self.stdout.ending = '' explicitly; however doing this might mean that other code that uses self.stdout.write expecting it to insert newlines, would fail.


In your case, what I'd do is to define a print method for the Command:

from django.core.management.base import OutputWrapper

class PrintHelper:
    def __init__(self, wrapped):
        self.wrapped = wrapped

    def write(self, s):
        if isinstance(self.wrapped, OutputWrapper):
            self.wrapped.write(s, ending='')
        else:
            self.wrapped.write(s)

class Command(BaseCommand):
    def print(self, *args, file=None, **kwargs):
        if file is None:
            file = self.stdout
        print(*args, file=PrintHelper(file), **kwargs)

    def handle(self, *args, **options):
        self.print('hello ', end='')
        self.print('world')

You can make this into your own BaseCommand subclass - and you can use it with different files too:

    def handle(self, *args, **options):
        for c in '|/-\\' * 100:
            self.print('\rhello world: ' + c, end='', file=self.stderr)
            time.sleep(0.1)
        self.print('\bOK')