从另一个命令调用的 Django 命令中的重定向会导致无关的换行符。我怎样才能解决这个问题?

Redirection in Django command called from another command results in extraneous newlines. How can I fix this?

设置

为了说明问题,我在我的项目中创建了这些命令:

foo.py:

from django.core.management.base import BaseCommand
from django.core.management import call_command

class Command(BaseCommand):

    def handle(self, *args, **options):
        self.stdout.write("foo")
        # I pass `self.stdout` here explicitly because if `foo` is
        # redirected, I want `baz` redirected too.
        call_command('baz', stdout=self.stdout)

baz.py:

from django.core.management.base import BaseCommand
from django.core.management import call_command

class Command(BaseCommand):

    def handle(self, *args, **options):
        # This could be reduced to one call to self.stdout.write
        # but this code is meant to minimally reproduce what happens in a 
        # complex command where multiple self.stdout.write calls are
        # made. If the code here were replaced with a single call, it 
        # would cease to reproduce the issue.
        self.stdout.write("baz ", ending='')

        # Imagine a lot of stuff happening here with conditionals and
        # loops.
        self.stdout.write("baz")

实际行为

我运行foo是这样的:

./manage.py foo

我将此输出到控制台:

foo
baz 
baz

期望的行为

我想要的是控制台的输出是:

foo
baz baz

请注意,当我直接使用 ./manage.py baz 调用 baz 时,我得到以下输出:

baz baz

两者之间没有换行"baz"。当通过 foo.

调用 baz 时,我想要相同的布局

问题

它不起作用的原因是 Django 使用 OutputWrapper 对象来包装作为 Commandstdout 参数传递的任何内容。该对象在命令的方法中变为 self.stdout。 (这在 Django 1.8 中是正确的,据我所知,早在 Django 1.5 中。)

OutputWrapper 有一个 write 方法,默认情况下会在写入输出的内容中添加换行符。您可以使用 ending='' 将其关闭,这就是您所做的并且在直接调用 baz 时工作正常。但是,OutputWrapper 对象并不期望它会包装另一个 OutputWrapper 对象。当您的 baz 代码通过 foo 调用并执行 self.stdout.write("baz ", ending='') 时,它会在它包装的对象上调用 write,但不会转发 ending='' 参数。因此,为 foo 创建的 OutputWrapper 在没有 ending='' 的情况下被调用,并且换行符输出到控制台。

解决方案

我更喜欢的解决方案是在我的代码中完全复制 Django 在决定 OutputWrapper 应该包装什么时所做的事情:

class Command(BaseCommand):

    def handle(self, *args, **options):
        self.stdout.write("foo\n")
        call_command('baz', stdout=options.get('stdout', sys.stdout))

如果 foo 没有 stdout 关键字参数,stdout=options.get('stdout', sys.stdout) 位将通过 sys.stdout。否则,它转发 stdout 关键字参数。您可以通过将 stdout 的所有实例更改为 stderr.

来对 stderr 执行相同的操作

另一种方法是将 OutputWrapper 的结尾设置为 '',如下所示:

class Command(BaseCommand):

    def handle(self, *args, **options):
        self.stdout.ending = ''
        self.stdout.write("foo\n")
        call_command('baz')

然后您必须在编写命令的同时牢记您始终必须显式输出换行符:这就是为什么我们现在有 self.stdout.write("foo\n"),在字符串末尾有一个换行符。这样做的好处是 baz 输出的任何内容都会立即出现在控制台上,因此如果它在某些输出后挂起,您至少可以使用一些东西。但是,OutputWrapper 不是已记录为 Django 项目直接使用的 class。这个解决方案基本上使用了一个 API,它可以在较新版本的 Django 中毫无警告地改变。