Kaydet (Commit) af1fa5e7 authored tarafından Pavel Kulikov's avatar Pavel Kulikov Kaydeden (comit) Tim Graham

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

Thanks Squareweave for the django-loaddata-stdin project from which this
is adapted.
üst c930c241
...@@ -617,6 +617,7 @@ answer newbie questions, and generally made Django that much better: ...@@ -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
......
...@@ -2,6 +2,7 @@ import functools ...@@ -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: ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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:
......
...@@ -416,6 +416,14 @@ originally generated. ...@@ -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 ...@@ -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``
---------------- ----------------
......
...@@ -185,6 +185,8 @@ Management Commands ...@@ -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
~~~~~~~~~~ ~~~~~~~~~~
......
...@@ -680,6 +680,35 @@ class FixtureLoadingTests(DumpDataAssertMixin, TestCase): ...@@ -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):
""" """
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment