Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spurious newlines added in Django management commands

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.

like image 580
wim Avatar asked May 23 '16 21:05

wim


People also ask

What are management commands in Django?

Django management commands are included as part of Django apps and are designed to fulfill repetitive or complex tasks through a one keyword command line instruction. Every Django management command is backed by a script that contains the step-by-step Python logic to fulfill its duties.

How do you pass arguments in Django management command?

The parameter parser is an instance of argparse. ArgumentParser (see the docs). Now you can add as many arguments as you want by calling parser 's add_argument method. In the code above, you are expecting a parameter n of type int which is gotten in the handle method from options .

What is BaseCommand Django?

BaseCommand is a Django object for creating new Django admin commands that can be invoked with the manage.py script. The Django project team as usual provides fantastic documentation for creating your own commands.


3 Answers

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().

like image 111
Moinuddin Quadri Avatar answered Sep 17 '22 01:09

Moinuddin Quadri


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
like image 35
C14L Avatar answered Sep 18 '22 01:09

C14L


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')