Fixed #27978 -- Allowed loaddata to read data from stdin.

Thanks Squareweave for the django-loaddata-stdin project from which this
is adapted.
This commit is contained in:
Pavel Kulikov 2017-03-26 22:29:05 +03:00 committed by Tim Graham
parent c930c241f8
commit af1fa5e7da
5 changed files with 78 additions and 0 deletions

View File

@ -617,6 +617,7 @@ answer newbie questions, and generally made Django that much better:
Paulo Poiati <paulogpoiati@gmail.com> Paulo Poiati <paulogpoiati@gmail.com>
Paulo Scardine <paulo@scardine.com.br> Paulo Scardine <paulo@scardine.com.br>
Paul Smith <blinkylights23@gmail.com> Paul Smith <blinkylights23@gmail.com>
Pavel Kulikov <kulikovpavel@gmail.com>
pavithran s <pavithran.s@gmail.com> pavithran s <pavithran.s@gmail.com>
Pavlo Kapyshin <i@93z.org> Pavlo Kapyshin <i@93z.org>
permonik@mesias.brnonet.cz permonik@mesias.brnonet.cz

View File

@ -2,6 +2,7 @@ import functools
import glob import glob
import gzip import gzip
import os import os
import sys
import warnings import warnings
import zipfile import zipfile
from itertools import product from itertools import product
@ -25,6 +26,8 @@ try:
except ImportError: except ImportError:
has_bz2 = False has_bz2 = False
READ_STDIN = '-'
class Command(BaseCommand): class Command(BaseCommand):
help = 'Installs the named fixture(s) in the database.' help = 'Installs the named fixture(s) in the database.'
@ -52,6 +55,10 @@ class Command(BaseCommand):
'-e', '--exclude', dest='exclude', action='append', default=[], '-e', '--exclude', dest='exclude', action='append', default=[],
help='An app_label or app_label.ModelName to exclude. Can be used multiple times.', help='An app_label or app_label.ModelName to exclude. Can be used multiple times.',
) )
parser.add_argument(
'--format', action='store', dest='format', default=None,
help='Format of serialized data when reading from stdin.',
)
def handle(self, *fixture_labels, **options): def handle(self, *fixture_labels, **options):
self.ignore = options['ignore'] self.ignore = options['ignore']
@ -59,6 +66,7 @@ class Command(BaseCommand):
self.app_label = options['app_label'] self.app_label = options['app_label']
self.verbosity = options['verbosity'] self.verbosity = options['verbosity']
self.excluded_models, self.excluded_apps = parse_apps_and_model_labels(options['exclude']) self.excluded_models, self.excluded_apps = parse_apps_and_model_labels(options['exclude'])
self.format = options['format']
with transaction.atomic(using=self.using): with transaction.atomic(using=self.using):
self.loaddata(fixture_labels) self.loaddata(fixture_labels)
@ -85,6 +93,7 @@ class Command(BaseCommand):
None: (open, 'rb'), None: (open, 'rb'),
'gz': (gzip.GzipFile, 'rb'), 'gz': (gzip.GzipFile, 'rb'),
'zip': (SingleZipReader, 'r'), 'zip': (SingleZipReader, 'r'),
'stdin': (lambda *args: sys.stdin, None),
} }
if has_bz2: if has_bz2:
self.compression_formats['bz2'] = (bz2.BZ2File, 'r') self.compression_formats['bz2'] = (bz2.BZ2File, 'r')
@ -201,6 +210,9 @@ class Command(BaseCommand):
@functools.lru_cache(maxsize=None) @functools.lru_cache(maxsize=None)
def find_fixtures(self, fixture_label): def find_fixtures(self, fixture_label):
"""Find fixture files for a given label.""" """Find fixture files for a given label."""
if fixture_label == READ_STDIN:
return [(READ_STDIN, None, READ_STDIN)]
fixture_name, ser_fmt, cmp_fmt = self.parse_name(fixture_label) fixture_name, ser_fmt, cmp_fmt = self.parse_name(fixture_label)
databases = [self.using, None] databases = [self.using, None]
cmp_fmts = list(self.compression_formats.keys()) if cmp_fmt is None else [cmp_fmt] cmp_fmts = list(self.compression_formats.keys()) if cmp_fmt is None else [cmp_fmt]
@ -288,6 +300,11 @@ class Command(BaseCommand):
""" """
Split fixture name in name, serialization format, compression format. Split fixture name in name, serialization format, compression format.
""" """
if fixture_name == READ_STDIN:
if not self.format:
raise CommandError('--format must be specified when reading from stdin.')
return READ_STDIN, self.format, 'stdin'
parts = fixture_name.rsplit('.', 2) parts = fixture_name.rsplit('.', 2)
if len(parts) > 1 and parts[-1] in self.compression_formats: if len(parts) > 1 and parts[-1] in self.compression_formats:

View File

@ -416,6 +416,14 @@ originally generated.
Specifies a single app to look for fixtures in rather than looking in all apps. Specifies a single app to look for fixtures in rather than looking in all apps.
.. django-admin-option:: --format FORMAT
.. versionadded:: 2.0
Specifies the :ref:`serialization format <serialization-formats>` (e.g.,
``json`` or ``xml``) for fixtures :ref:`read from stdin
<loading-fixtures-stdin>`.
.. django-admin-option:: --exclude EXCLUDE, -e EXCLUDE .. django-admin-option:: --exclude EXCLUDE, -e EXCLUDE
.. versionadded:: 1.11 .. versionadded:: 1.11
@ -552,6 +560,27 @@ defined, name the fixture ``mydata.master.json`` or
``mydata.master.json.gz`` and the fixture will only be loaded when you ``mydata.master.json.gz`` and the fixture will only be loaded when you
specify you want to load data into the ``master`` database. specify you want to load data into the ``master`` database.
.. _loading-fixtures-stdin:
Loading fixtures from ``stdin``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionadded:: 2.0
You can use a dash as the fixture name to load input from ``sys.stdin``. For
example::
django-admin loaddata --format=json -
When reading from ``stdin``, the :option:`--format <loaddata --format>` option
is required to specify the :ref:`serialization format <serialization-formats>`
of the input (e.g., ``json`` or ``xml``).
Loading from ``stdin`` is useful with standard input and output redirections.
For example::
django-admin dumpdata --format=json --database=test app_label.ModelName | django-admin loaddata --format=json --database=prod -
``makemessages`` ``makemessages``
---------------- ----------------

View File

@ -185,6 +185,8 @@ Management Commands
* The new :option:`makemessages --add-location` option controls the comment * The new :option:`makemessages --add-location` option controls the comment
format in PO files. format in PO files.
* :djadmin:`loaddata` can now :ref:`read from stdin <loading-fixtures-stdin>`.
Migrations Migrations
~~~~~~~~~~ ~~~~~~~~~~

View File

@ -680,6 +680,35 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase):
with self.assertRaisesMessage(management.CommandError, msg): with self.assertRaisesMessage(management.CommandError, msg):
management.call_command('loaddata', 'fixture1', exclude=['fixtures.FooModel'], verbosity=0) management.call_command('loaddata', 'fixture1', exclude=['fixtures.FooModel'], verbosity=0)
def test_stdin_without_format(self):
"""Reading from stdin raises an error if format isn't specified."""
msg = '--format must be specified when reading from stdin.'
with self.assertRaisesMessage(management.CommandError, msg):
management.call_command('loaddata', '-', verbosity=0)
def test_loading_stdin(self):
"""Loading fixtures from stdin with json and xml."""
tests_dir = os.path.dirname(__file__)
fixture_json = os.path.join(tests_dir, 'fixtures', 'fixture1.json')
fixture_xml = os.path.join(tests_dir, 'fixtures', 'fixture3.xml')
with mock.patch('django.core.management.commands.loaddata.sys.stdin', open(fixture_json, 'r')):
management.call_command('loaddata', '--format=json', '-', verbosity=0)
self.assertEqual(Article.objects.count(), 2)
self.assertQuerysetEqual(Article.objects.all(), [
'<Article: Time to reform copyright>',
'<Article: Poker has no place on ESPN>',
])
with mock.patch('django.core.management.commands.loaddata.sys.stdin', open(fixture_xml, 'r')):
management.call_command('loaddata', '--format=xml', '-', verbosity=0)
self.assertEqual(Article.objects.count(), 3)
self.assertQuerysetEqual(Article.objects.all(), [
'<Article: XML identified as leading cause of cancer>',
'<Article: Time to reform copyright>',
'<Article: Poker on TV is great!>',
])
class NonexistentFixtureTests(TestCase): class NonexistentFixtureTests(TestCase):
""" """